diff --git a/config.example.yaml b/config.example.yaml index 625a4e9..fd05e77 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -10,7 +10,7 @@ keybindings: emacs # Choose keybinding style (emacs, vi) editor: null # Specifies the command used to edit input buffer or session. (e.g. vim, emacs, nano). wrap: no # Controls text wrapping (no, auto, ) wrap_code: false # Enables or disables wrapping of code blocks -password_file: null # Path to a file containing the password for the Loki vault +vault_password_file: null # Path to a file containing the password for the Loki vault # ---- function-calling ---- function_calling: true # Enables or disables function calling (Globally). diff --git a/src/cli/completer.rs b/src/cli/completer.rs new file mode 100644 index 0000000..f696acb --- /dev/null +++ b/src/cli/completer.rs @@ -0,0 +1,80 @@ +use crate::client::{list_models, ModelType}; +use crate::config::{list_agents, Config}; +use clap_complete::CompletionCandidate; +use std::ffi::OsStr; + +pub(super) fn model_completer(current: &OsStr) -> Vec { + let cur = current.to_string_lossy(); + match Config::init_bare() { + Ok(config) => list_models(&config, ModelType::Chat) + .into_iter() + .filter(|&m| m.id().starts_with(&*cur)) + .map(|m| CompletionCandidate::new(m.id())) + .collect(), + Err(_) => vec![], + } +} + +pub(super) fn role_completer(current: &OsStr) -> Vec { + let cur = current.to_string_lossy(); + Config::list_roles(true) + .into_iter() + .filter(|r| r.starts_with(&*cur)) + .map(CompletionCandidate::new) + .collect() +} + +pub(super) fn agent_completer(current: &OsStr) -> Vec { + let cur = current.to_string_lossy(); + list_agents() + .into_iter() + .filter(|a| a.starts_with(&*cur)) + .map(CompletionCandidate::new) + .collect() +} + +pub(super) fn rag_completer(current: &OsStr) -> Vec { + let cur = current.to_string_lossy(); + Config::list_rags() + .into_iter() + .filter(|r| r.starts_with(&*cur)) + .map(CompletionCandidate::new) + .collect() +} + +pub(super) fn macro_completer(current: &OsStr) -> Vec { + let cur = current.to_string_lossy(); + Config::list_macros() + .into_iter() + .filter(|m| m.starts_with(&*cur)) + .map(CompletionCandidate::new) + .collect() +} + +pub(super) fn session_completer(current: &OsStr) -> Vec { + let cur = current.to_string_lossy(); + match Config::init_bare() { + Ok(config) => config + .list_sessions() + .into_iter() + .filter(|s| s.starts_with(&*cur)) + .map(CompletionCandidate::new) + .collect(), + Err(_) => vec![], + } +} + +pub(super) fn secrets_completer(current: &OsStr) -> Vec { + let cur = current.to_string_lossy(); + match Config::init_bare() { + Ok(config) => config + .vault + .list_secrets(false) + .unwrap_or_default() + .into_iter() + .filter(|s| s.starts_with(&*cur)) + .map(CompletionCandidate::new) + .collect(), + Err(_) => vec![], + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index ac29417..4a81219 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,15 +1,14 @@ -mod secrets; +mod completer; -use crate::cli::secrets::secrets_completer; -use crate::client::{list_models, ModelType}; -use crate::config::{list_agents, Config}; +use crate::cli::completer::{ + agent_completer, macro_completer, model_completer, rag_completer, role_completer, + secrets_completer, session_completer, +}; use anyhow::{Context, Result}; use clap::ValueHint; use clap::{crate_authors, crate_description, crate_name, crate_version, Parser}; use clap_complete::ArgValueCompleter; -use clap_complete::CompletionCandidate; use is_terminal::IsTerminal; -use std::ffi::OsStr; use std::io::{stdin, Read}; #[derive(Parser, Debug)] @@ -174,64 +173,3 @@ impl Cli { } } } - -fn model_completer(current: &OsStr) -> Vec { - let cur = current.to_string_lossy(); - match Config::init_bare() { - Ok(config) => list_models(&config, ModelType::Chat) - .into_iter() - .filter(|&m| m.id().starts_with(&*cur)) - .map(|m| CompletionCandidate::new(m.id())) - .collect(), - Err(_) => vec![], - } -} - -fn role_completer(current: &OsStr) -> Vec { - let cur = current.to_string_lossy(); - Config::list_roles(true) - .into_iter() - .filter(|r| r.starts_with(&*cur)) - .map(CompletionCandidate::new) - .collect() -} - -fn agent_completer(current: &OsStr) -> Vec { - let cur = current.to_string_lossy(); - list_agents() - .into_iter() - .filter(|a| a.starts_with(&*cur)) - .map(CompletionCandidate::new) - .collect() -} - -fn rag_completer(current: &OsStr) -> Vec { - let cur = current.to_string_lossy(); - Config::list_rags() - .into_iter() - .filter(|r| r.starts_with(&*cur)) - .map(CompletionCandidate::new) - .collect() -} - -fn macro_completer(current: &OsStr) -> Vec { - let cur = current.to_string_lossy(); - Config::list_macros() - .into_iter() - .filter(|m| m.starts_with(&*cur)) - .map(CompletionCandidate::new) - .collect() -} - -fn session_completer(current: &OsStr) -> Vec { - let cur = current.to_string_lossy(); - match Config::init_bare() { - Ok(config) => config - .list_sessions() - .into_iter() - .filter(|s| s.starts_with(&*cur)) - .map(CompletionCandidate::new) - .collect(), - Err(_) => vec![], - } -} diff --git a/src/cli/secrets.rs b/src/cli/secrets.rs deleted file mode 100644 index c1aa976..0000000 --- a/src/cli/secrets.rs +++ /dev/null @@ -1,226 +0,0 @@ -use crate::cli::Cli; -use crate::config::{ensure_parent_exists, Config}; -use anyhow::{anyhow, Context, Result}; -use clap_complete::CompletionCandidate; -use gman::providers::local::LocalProvider; -use gman::providers::SecretProvider; -use inquire::validator::Validation; -use inquire::{min_length, required, Confirm, Password, PasswordDisplayMode, Text}; -use std::ffi::OsStr; -use std::io; -use std::io::{IsTerminal, Read, Write}; -use std::path::PathBuf; -use tokio::runtime::Handle; - -impl Cli { - pub async fn handle_secret_flag(&self, mut config: Config) -> Result<()> { - ensure_password_file_initialized(&mut config)?; - - let local_provider = match config.secrets_provider { - Some(lc) => lc, - None => { - return Err(anyhow!( - "Local secrets provider is not configured. Please ensure a password file is configured and try again." - )) - } - }; - - if let Some(secret_name) = &self.add_secret { - let plaintext = - read_all_stdin().with_context(|| "unable to read plaintext from stdin")?; - local_provider - .set_secret(secret_name, plaintext.trim_end()) - .await?; - println!("✓ Secret '{secret_name}' added to the vault."); - } - if let Some(secret_name) = &self.get_secret { - let secret = local_provider.get_secret(secret_name).await?; - println!("{}", secret); - } - if let Some(secret_name) = &self.update_secret { - let plaintext = - read_all_stdin().with_context(|| "unable to read plaintext from stdin")?; - local_provider - .update_secret(secret_name, plaintext.trim_end()) - .await?; - println!("✓ Secret '{secret_name}' updated in the vault."); - } - if let Some(secret_name) = &self.delete_secret { - local_provider.delete_secret(secret_name).await?; - println!("✓ Secret '{secret_name}' deleted from the vault."); - } - if self.list_secrets { - let secrets = local_provider.list_secrets().await?; - if secrets.is_empty() { - println!("The vault is empty."); - } else { - for key in &secrets { - println!("{}", key); - } - } - } - - Ok(()) - } -} - -fn ensure_password_file_initialized(config: &mut Config) -> Result<()> { - let secrets_password_file = config.secrets_password_file(); - if secrets_password_file.exists() { - { - let file_contents = std::fs::read_to_string(&secrets_password_file)?; - if !file_contents.trim().is_empty() { - return Ok(()); - } - } - - let ans = Confirm::new( - format!( - "The configured password file '{}' is empty. Create a password?", - secrets_password_file.display() - ) - .as_str(), - ) - .with_default(true) - .prompt()?; - - if !ans { - return Err(anyhow!("The configured password file '{}' is empty. Please populate it with a password and try again.", secrets_password_file.display())); - } - - let password = Password::new("Enter a password to encrypt all vault secrets:") - .with_validator(required!()) - .with_validator(min_length!(10)) - .with_display_mode(PasswordDisplayMode::Masked) - .prompt(); - - match password { - Ok(pw) => { - std::fs::write(&secrets_password_file, pw.as_bytes())?; - load_secrets_provider(config); - println!( - "✓ Password file '{}' updated.", - secrets_password_file.display() - ); - } - Err(_) => { - return Err(anyhow!( - "Failed to read password from input. Password file not updated." - )); - } - } - } else { - let ans = Confirm::new("No password file configured. Do you want to create one now?") - .with_default(true) - .prompt()?; - - if !ans { - return Err(anyhow!("A password file is required to utilize secrets. Please configure a password file in your config file and try again.")); - } - - let password_file: PathBuf = Text::new("Enter the path to the password file to create:") - .with_default(&secrets_password_file.display().to_string()) - .with_validator(required!("Password file path is required")) - .with_validator(|input: &str| { - let path = PathBuf::from(input); - if path.exists() { - Ok(Validation::Invalid( - "File already exists. Please choose a different path.".into(), - )) - } else if let Some(parent) = path.parent() { - if !parent.exists() { - Ok(Validation::Invalid( - "Parent directory does not exist.".into(), - )) - } else { - Ok(Validation::Valid) - } - } else { - Ok(Validation::Valid) - } - }) - .prompt()? - .into(); - - if password_file != secrets_password_file { - println!("Note: The default password file path is '{}'. You have chosen to create a different path: '{}'. Please ensure your configuration is updated accordingly.", secrets_password_file.display(), password_file.display()); - } - - ensure_parent_exists(&password_file)?; - - let password = Password::new("Enter a password to encrypt all vault secrets:") - .with_display_mode(PasswordDisplayMode::Masked) - .with_validator(required!()) - .with_validator(min_length!(10)) - .prompt(); - - match password { - Ok(pw) => { - std::fs::write(&password_file, pw.as_bytes())?; - config.password_file = Some(password_file); - load_secrets_provider(config); - println!( - "✓ Password file '{}' created.", - secrets_password_file.display() - ); - } - Err(_) => { - return Err(anyhow!( - "Failed to read password from input. Password file not created." - )); - } - } - } - - Ok(()) -} - -fn load_secrets_provider(config: &mut Config) { - let password_file = Some(config.secrets_password_file()); - config.secrets_provider = Some(LocalProvider { - password_file, - git_branch: None, - ..LocalProvider::default() - }); -} - -fn read_all_stdin() -> Result { - if io::stdin().is_terminal() { - #[cfg(not(windows))] - eprintln!("Enter the text to encrypt, then press Ctrl-D twice to finish input"); - #[cfg(windows)] - eprintln!("Enter the text to encrypt, then press Ctrl-Z to finish input"); - io::stderr().flush()?; - } - let mut buf = String::new(); - let stdin_tty = io::stdin().is_terminal(); - let stdout_tty = io::stdout().is_terminal(); - io::stdin().read_to_string(&mut buf)?; - - if stdin_tty && stdout_tty && !buf.ends_with('\n') { - let mut out = io::stdout().lock(); - out.write_all(b"\n")?; - out.flush()?; - } - Ok(buf) -} - -pub fn secrets_completer(current: &OsStr) -> Vec { - let cur = current.to_string_lossy(); - match Config::init_bare() { - Ok(config) => { - let local_provider = match config.secrets_provider { - Some(pc) => pc, - None => return vec![], - }; - let h = Handle::current(); - tokio::task::block_in_place(|| h.block_on(local_provider.list_secrets())) - .unwrap_or_default() - .into_iter() - .filter(|s| s.starts_with(&*cur)) - .map(CompletionCandidate::new) - .collect() - } - Err(_) => vec![], - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs index fa8299b..919c620 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -24,8 +24,8 @@ use crate::utils::*; use crate::mcp::{ McpRegistry, MCP_INVOKE_META_FUNCTION_NAME_PREFIX, MCP_LIST_META_FUNCTION_NAME_PREFIX, }; +use crate::vault::Vault; use anyhow::{anyhow, bail, Context, Result}; -use gman::providers::local::LocalProvider; use indexmap::IndexMap; use inquire::{list_option::ListOption, validator::Validation, Confirm, MultiSelect, Select, Text}; use log::LevelFilter; @@ -121,7 +121,7 @@ pub struct Config { pub editor: Option, pub wrap: Option, pub wrap_code: bool, - pub(crate) password_file: Option, + vault_password_file: Option, pub function_calling: bool, pub mapping_tools: IndexMap, @@ -162,8 +162,8 @@ pub struct Config { pub clients: Vec, - #[serde(skip)] - pub secrets_provider: Option, + #[serde(skip)] + pub vault: Vault, #[serde(skip)] pub macro_flag: bool, @@ -207,7 +207,7 @@ impl Default for Config { editor: None, wrap: None, wrap_code: false, - password_file: None, + vault_password_file: None, function_calling: true, mapping_tools: Default::default(), @@ -247,7 +247,7 @@ impl Default for Config { clients: vec![], - secrets_provider: None, + vault: Default::default(), macro_flag: false, info_flag: false, @@ -312,7 +312,6 @@ impl Config { config.working_mode = working_mode; config.info_flag = info_flag; - config.load_secrets_provider(); let setup = async |config: &mut Self| -> Result<()> { config.load_envs(); @@ -329,6 +328,7 @@ impl Config { config.setup_model()?; config.setup_document_loaders(); config.setup_user_agent(); + config.vault = Vault::init(config); Ok(()) }; let ret = setup(&mut config).await; @@ -370,8 +370,8 @@ impl Config { } } - pub fn secrets_password_file(&self) -> PathBuf { - match &self.password_file { + pub fn vault_password_file(&self) -> PathBuf { + match &self.vault_password_file { Some(path) => match path.exists() { true => path.clone(), false => gman::config::Config::local_provider_password_file(), @@ -707,6 +707,10 @@ impl Config { ("macros_dir", display_path(&Self::macros_dir())), ("functions_dir", display_path(&Self::functions_dir())), ("messages_file", display_path(&self.messages_file())), + ( + "vault_password_file", + display_path(&self.vault_password_file()), + ), ]; if let Ok((_, Some(log_path))) = Self::log_config(self.working_mode.is_serve()) { items.push(("log_path", display_path(&log_path))); @@ -2050,6 +2054,14 @@ impl Config { ".delete" => { map_completion_values(vec!["role", "session", "rag", "macro", "agent-data"]) } + ".vault" => { + let mut values = vec!["add", "get", "update", "delete", "list"]; + values.sort_unstable(); + values + .into_iter() + .map(|v| (format!("{v} "), None)) + .collect() + } _ => vec![], }; } else if cmd == ".set" && args.len() == 2 { @@ -2139,6 +2151,14 @@ impl Config { _ => vec![], }; values = candidates.into_iter().map(|v| (v, None)).collect(); + } else if cmd == ".vault" && args.len() == 2 { + values = self + .vault + .list_secrets(false) + .unwrap_or_default() + .into_iter() + .map(|v| (v, None)) + .collect(); } else if cmd == ".agent" { if args.len() == 2 { let dir = Self::agent_data_dir(args[0]).join(SESSIONS_DIR_NAME); @@ -2523,23 +2543,6 @@ impl Config { Ok(config) } - fn load_secrets_provider(&mut self) { - let secrets_password_file = self.secrets_password_file(); - if !secrets_password_file.exists() { - eprintln!( - "Warning: secrets password file '{}' does not exist.", - secrets_password_file.display() - ); - return; - } - - self.secrets_provider = Some(LocalProvider { - password_file: Some(secrets_password_file), - git_branch: None, - ..LocalProvider::default() - }); - } - fn load_envs(&mut self) { if let Ok(v) = env::var(get_env_name("model")) { self.model_id = v; diff --git a/src/main.rs b/src/main.rs index aeeb978..41da59d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod serve; mod utils; mod mcp; mod parsers; +mod vault; #[macro_use] extern crate log; @@ -26,6 +27,7 @@ use crate::repl::Repl; use crate::utils::*; use crate::cli::Cli; +use crate::vault::Vault; use anyhow::{bail, Result}; use clap::{CommandFactory, Parser}; use clap_complete::CompleteEnv; @@ -67,7 +69,7 @@ async fn main() -> Result<()> { || cli.list_rags || cli.list_macros || cli.list_sessions; - let secrets_flags = cli.add_secret.is_some() + let vault_flags = cli.add_secret.is_some() || cli.get_secret.is_some() || cli.update_secret.is_some() || cli.delete_secret.is_some() @@ -75,8 +77,8 @@ async fn main() -> Result<()> { let log_path = setup_logger(working_mode.is_serve())?; - if secrets_flags { - return cli.handle_secret_flag(Config::init_bare()?).await; + if vault_flags { + return Vault::handle_vault_flags(cli, Config::init_bare()?); } let abort_signal = create_abort_signal(); diff --git a/src/repl/mod.rs b/src/repl/mod.rs index b6ed4d8..7a5dff9 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -32,7 +32,7 @@ use std::{env, mem, process}; const MENU_NAME: &str = "completion_menu"; -static REPL_COMMANDS: LazyLock<[ReplCommand; 36]> = LazyLock::new(|| { +static REPL_COMMANDS: LazyLock<[ReplCommand; 37]> = LazyLock::new(|| { [ ReplCommand::new(".help", "Show this help guide", AssertState::pass()), ReplCommand::new(".info", "Show system info", AssertState::pass()), @@ -185,6 +185,11 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 36]> = LazyLock::new(|| { AssertState::pass(), ), ReplCommand::new(".exit", "Exit REPL", AssertState::pass()), + ReplCommand::new( + ".vault", + "View or modify the Loki vault", + AssertState::pass(), + ), ] }); static COMMAND_RE: LazyLock = LazyLock::new(|| Regex::new(r"^\s*(\.\S*)\s*").unwrap()); @@ -767,6 +772,42 @@ pub async fn run_repl_command( } _ => unknown_command()?, }, + ".vault" => match split_first_arg(args) { + Some(("add", name)) => { + if let Some(name) = name { + config.read().vault.add_secret(name)?; + } else { + println!("Usage: .vault add "); + } + } + Some(("get", name)) => { + if let Some(name) = name { + config.read().vault.get_secret(name)?; + } else { + println!("Usage: .vault get "); + } + } + Some(("update", name)) => { + if let Some(name) = name { + config.read().vault.update_secret(name)?; + } else { + println!("Usage: .vault update "); + } + } + Some(("delete", name)) => { + if let Some(name) = name { + config.read().vault.delete_secret(name)?; + } else { + println!("Usage: .vault delete "); + } + } + Some(("list", _)) => { + config.read().vault.list_secrets(true)?; + } + None | Some(_) => { + println!("Usage: .vault [name]") + } + }, _ => unknown_command()?, }, None => { diff --git a/src/vault/mod.rs b/src/vault/mod.rs new file mode 100644 index 0000000..6d5d799 --- /dev/null +++ b/src/vault/mod.rs @@ -0,0 +1,125 @@ +mod utils; + +use crate::cli::Cli; +use crate::config::Config; +use crate::vault::utils::ensure_password_file_initialized; +use anyhow::{Context, Result}; +use gman::providers::local::LocalProvider; +use gman::providers::SecretProvider; +use inquire::{required, Password, PasswordDisplayMode}; +use tokio::runtime::Handle; + +#[derive(Debug, Default, Clone)] +pub struct Vault { + local_provider: LocalProvider, +} + +impl Vault { + pub fn init(config: &Config) -> Self { + let vault_password_file = config.vault_password_file(); + let mut local_provider = LocalProvider { + password_file: Some(vault_password_file), + git_branch: None, + ..LocalProvider::default() + }; + + ensure_password_file_initialized(&mut local_provider) + .expect("Failed to initialize password file"); + + Self { local_provider } + } + + pub fn add_secret(&self, secret_name: &str) -> Result<()> { + let secret_value = Password::new("Enter the secret value:") + .with_validator(required!()) + .with_display_mode(PasswordDisplayMode::Masked) + .prompt() + .with_context(|| "unable to read secret from input")?; + + let h = Handle::current(); + tokio::task::block_in_place(|| { + h.block_on(self.local_provider.set_secret(secret_name, &secret_value)) + })?; + println!("✓ Secret '{secret_name}' added to the vault."); + + Ok(()) + } + + pub fn get_secret(&self, secret_name: &str) -> Result<()> { + let h = Handle::current(); + let secret = tokio::task::block_in_place(|| { + h.block_on(self.local_provider.get_secret(secret_name)) + })?; + println!("{}", secret); + + Ok(()) + } + + pub fn update_secret(&self, secret_name: &str) -> Result<()> { + let secret_value = Password::new("Enter the secret value:") + .with_validator(required!()) + .with_display_mode(PasswordDisplayMode::Masked) + .prompt() + .with_context(|| "unable to read secret from input")?; + let h = Handle::current(); + tokio::task::block_in_place(|| { + h.block_on( + self.local_provider + .update_secret(secret_name, &secret_value), + ) + })?; + println!("✓ Secret '{secret_name}' updated in the vault."); + + Ok(()) + } + + pub fn delete_secret(&self, secret_name: &str) -> Result<()> { + let h = Handle::current(); + tokio::task::block_in_place(|| h.block_on(self.local_provider.delete_secret(secret_name)))?; + println!("✓ Secret '{secret_name}' deleted from the vault."); + + Ok(()) + } + + pub fn list_secrets(&self, display_output: bool) -> Result> { + let h = Handle::current(); + let secrets = + tokio::task::block_in_place(|| h.block_on(self.local_provider.list_secrets()))?; + + if display_output { + if secrets.is_empty() { + println!("The vault is empty."); + } else { + for key in &secrets { + println!("{}", key); + } + } + } + + Ok(secrets) + } + + pub fn handle_vault_flags(cli: Cli, config: Config) -> Result<()> { + if let Some(secret_name) = cli.add_secret { + config.vault.add_secret(&secret_name)?; + } + + if let Some(secret_name) = cli.get_secret { + config.vault.get_secret(&secret_name)?; + } + + if let Some(secret_name) = cli.update_secret { + config.vault.update_secret(&secret_name)?; + } + + if let Some(secret_name) = cli.delete_secret { + config.vault.delete_secret(&secret_name)?; + } + + if cli.list_secrets { + config.vault.list_secrets(true)?; + } + + Ok(()) + } +} diff --git a/src/vault/utils.rs b/src/vault/utils.rs new file mode 100644 index 0000000..f4a1928 --- /dev/null +++ b/src/vault/utils.rs @@ -0,0 +1,132 @@ +use crate::config::ensure_parent_exists; +use anyhow::anyhow; +use anyhow::Result; +use gman::providers::local::LocalProvider; +use indoc::formatdoc; +use inquire::validator::Validation; +use inquire::{min_length, required, Confirm, Password, PasswordDisplayMode, Text}; +use std::path::PathBuf; + +pub fn ensure_password_file_initialized(local_provider: &mut LocalProvider) -> Result<()> { + let vault_password_file = local_provider + .password_file + .clone() + .ok_or_else(|| anyhow!("Password file is not configured"))?; + + if vault_password_file.exists() { + { + let file_contents = std::fs::read_to_string(&vault_password_file)?; + if !file_contents.trim().is_empty() { + return Ok(()); + } + } + + let ans = Confirm::new( + format!( + "The configured password file '{}' is empty. Create a password?", + vault_password_file.display() + ) + .as_str(), + ) + .with_default(true) + .prompt()?; + + if !ans { + return Err(anyhow!("The configured password file '{}' is empty. Please populate it with a password and try again.", vault_password_file.display())); + } + + let password = Password::new("Enter a password to encrypt all vault secrets:") + .with_validator(required!()) + .with_validator(min_length!(10)) + .with_display_mode(PasswordDisplayMode::Masked) + .prompt(); + + match password { + Ok(pw) => { + std::fs::write(&vault_password_file, pw.as_bytes())?; + println!( + "✓ Password file '{}' updated.", + vault_password_file.display() + ); + } + Err(_) => { + return Err(anyhow!( + "Failed to read password from input. Password file not updated." + )); + } + } + } else { + let ans = Confirm::new("No password file configured. Do you want to create one now?") + .with_default(true) + .prompt()?; + + if !ans { + return Err(anyhow!("A password file is required to utilize the Loki vault. Please configure a password file in your config file and try again.")); + } + + let password_file: PathBuf = Text::new("Enter the path to the password file to create:") + .with_default(&vault_password_file.display().to_string()) + .with_validator(required!("Password file path is required")) + .with_validator(|input: &str| { + let path = PathBuf::from(input); + if path.exists() { + Ok(Validation::Invalid( + "File already exists. Please choose a different path.".into(), + )) + } else if let Some(parent) = path.parent() { + if !parent.exists() { + Ok(Validation::Invalid( + "Parent directory does not exist.".into(), + )) + } else { + Ok(Validation::Valid) + } + } else { + Ok(Validation::Valid) + } + }) + .prompt()? + .into(); + + if password_file != vault_password_file { + println!( + "{}", + formatdoc!( + " + Note: The default password file path is '{}'. + You have chosen to create a different path: '{}'. + Please ensure your configuration is updated accordingly. + ", + vault_password_file.display(), + password_file.display() + ) + ); + } + + ensure_parent_exists(&password_file)?; + + let password = Password::new("Enter a password to encrypt all vault secrets:") + .with_display_mode(PasswordDisplayMode::Masked) + .with_validator(required!()) + .with_validator(min_length!(10)) + .prompt(); + + match password { + Ok(pw) => { + std::fs::write(&password_file, pw.as_bytes())?; + local_provider.password_file = Some(password_file); + println!( + "✓ Password file '{}' created.", + vault_password_file.display() + ); + } + Err(_) => { + return Err(anyhow!( + "Failed to read password from input. Password file not created." + )); + } + } + } + + Ok(()) +}