feat: added REPL support for interacting with the Loki vault

This commit is contained in:
2025-10-15 15:15:04 -06:00
parent 591f204b67
commit df8b326d89
9 changed files with 419 additions and 324 deletions
+1 -1
View File
@@ -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). editor: null # Specifies the command used to edit input buffer or session. (e.g. vim, emacs, nano).
wrap: no # Controls text wrapping (no, auto, <max-width>) wrap: no # Controls text wrapping (no, auto, <max-width>)
wrap_code: false # Enables or disables wrapping of code blocks 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 ----
function_calling: true # Enables or disables function calling (Globally). function_calling: true # Enables or disables function calling (Globally).
+80
View File
@@ -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<CompletionCandidate> {
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<CompletionCandidate> {
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<CompletionCandidate> {
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<CompletionCandidate> {
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<CompletionCandidate> {
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<CompletionCandidate> {
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<CompletionCandidate> {
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![],
}
}
+5 -67
View File
@@ -1,15 +1,14 @@
mod secrets; mod completer;
use crate::cli::secrets::secrets_completer; use crate::cli::completer::{
use crate::client::{list_models, ModelType}; agent_completer, macro_completer, model_completer, rag_completer, role_completer,
use crate::config::{list_agents, Config}; secrets_completer, session_completer,
};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::ValueHint; use clap::ValueHint;
use clap::{crate_authors, crate_description, crate_name, crate_version, Parser}; use clap::{crate_authors, crate_description, crate_name, crate_version, Parser};
use clap_complete::ArgValueCompleter; use clap_complete::ArgValueCompleter;
use clap_complete::CompletionCandidate;
use is_terminal::IsTerminal; use is_terminal::IsTerminal;
use std::ffi::OsStr;
use std::io::{stdin, Read}; use std::io::{stdin, Read};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -174,64 +173,3 @@ impl Cli {
} }
} }
} }
fn model_completer(current: &OsStr) -> Vec<CompletionCandidate> {
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<CompletionCandidate> {
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<CompletionCandidate> {
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<CompletionCandidate> {
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<CompletionCandidate> {
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<CompletionCandidate> {
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![],
}
}
-226
View File
@@ -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<String> {
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<CompletionCandidate> {
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![],
}
}
+29 -26
View File
@@ -24,8 +24,8 @@ use crate::utils::*;
use crate::mcp::{ use crate::mcp::{
McpRegistry, MCP_INVOKE_META_FUNCTION_NAME_PREFIX, MCP_LIST_META_FUNCTION_NAME_PREFIX, McpRegistry, MCP_INVOKE_META_FUNCTION_NAME_PREFIX, MCP_LIST_META_FUNCTION_NAME_PREFIX,
}; };
use crate::vault::Vault;
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use gman::providers::local::LocalProvider;
use indexmap::IndexMap; use indexmap::IndexMap;
use inquire::{list_option::ListOption, validator::Validation, Confirm, MultiSelect, Select, Text}; use inquire::{list_option::ListOption, validator::Validation, Confirm, MultiSelect, Select, Text};
use log::LevelFilter; use log::LevelFilter;
@@ -121,7 +121,7 @@ pub struct Config {
pub editor: Option<String>, pub editor: Option<String>,
pub wrap: Option<String>, pub wrap: Option<String>,
pub wrap_code: bool, pub wrap_code: bool,
pub(crate) password_file: Option<PathBuf>, vault_password_file: Option<PathBuf>,
pub function_calling: bool, pub function_calling: bool,
pub mapping_tools: IndexMap<String, String>, pub mapping_tools: IndexMap<String, String>,
@@ -162,8 +162,8 @@ pub struct Config {
pub clients: Vec<ClientConfig>, pub clients: Vec<ClientConfig>,
#[serde(skip)] #[serde(skip)]
pub secrets_provider: Option<LocalProvider>, pub vault: Vault,
#[serde(skip)] #[serde(skip)]
pub macro_flag: bool, pub macro_flag: bool,
@@ -207,7 +207,7 @@ impl Default for Config {
editor: None, editor: None,
wrap: None, wrap: None,
wrap_code: false, wrap_code: false,
password_file: None, vault_password_file: None,
function_calling: true, function_calling: true,
mapping_tools: Default::default(), mapping_tools: Default::default(),
@@ -247,7 +247,7 @@ impl Default for Config {
clients: vec![], clients: vec![],
secrets_provider: None, vault: Default::default(),
macro_flag: false, macro_flag: false,
info_flag: false, info_flag: false,
@@ -312,7 +312,6 @@ impl Config {
config.working_mode = working_mode; config.working_mode = working_mode;
config.info_flag = info_flag; config.info_flag = info_flag;
config.load_secrets_provider();
let setup = async |config: &mut Self| -> Result<()> { let setup = async |config: &mut Self| -> Result<()> {
config.load_envs(); config.load_envs();
@@ -329,6 +328,7 @@ impl Config {
config.setup_model()?; config.setup_model()?;
config.setup_document_loaders(); config.setup_document_loaders();
config.setup_user_agent(); config.setup_user_agent();
config.vault = Vault::init(config);
Ok(()) Ok(())
}; };
let ret = setup(&mut config).await; let ret = setup(&mut config).await;
@@ -370,8 +370,8 @@ impl Config {
} }
} }
pub fn secrets_password_file(&self) -> PathBuf { pub fn vault_password_file(&self) -> PathBuf {
match &self.password_file { match &self.vault_password_file {
Some(path) => match path.exists() { Some(path) => match path.exists() {
true => path.clone(), true => path.clone(),
false => gman::config::Config::local_provider_password_file(), false => gman::config::Config::local_provider_password_file(),
@@ -707,6 +707,10 @@ impl Config {
("macros_dir", display_path(&Self::macros_dir())), ("macros_dir", display_path(&Self::macros_dir())),
("functions_dir", display_path(&Self::functions_dir())), ("functions_dir", display_path(&Self::functions_dir())),
("messages_file", display_path(&self.messages_file())), ("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()) { if let Ok((_, Some(log_path))) = Self::log_config(self.working_mode.is_serve()) {
items.push(("log_path", display_path(&log_path))); items.push(("log_path", display_path(&log_path)));
@@ -2050,6 +2054,14 @@ impl Config {
".delete" => { ".delete" => {
map_completion_values(vec!["role", "session", "rag", "macro", "agent-data"]) 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![], _ => vec![],
}; };
} else if cmd == ".set" && args.len() == 2 { } else if cmd == ".set" && args.len() == 2 {
@@ -2139,6 +2151,14 @@ impl Config {
_ => vec![], _ => vec![],
}; };
values = candidates.into_iter().map(|v| (v, None)).collect(); 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" { } else if cmd == ".agent" {
if args.len() == 2 { if args.len() == 2 {
let dir = Self::agent_data_dir(args[0]).join(SESSIONS_DIR_NAME); let dir = Self::agent_data_dir(args[0]).join(SESSIONS_DIR_NAME);
@@ -2523,23 +2543,6 @@ impl Config {
Ok(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) { fn load_envs(&mut self) {
if let Ok(v) = env::var(get_env_name("model")) { if let Ok(v) = env::var(get_env_name("model")) {
self.model_id = v; self.model_id = v;
+5 -3
View File
@@ -10,6 +10,7 @@ mod serve;
mod utils; mod utils;
mod mcp; mod mcp;
mod parsers; mod parsers;
mod vault;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
@@ -26,6 +27,7 @@ use crate::repl::Repl;
use crate::utils::*; use crate::utils::*;
use crate::cli::Cli; use crate::cli::Cli;
use crate::vault::Vault;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use clap::{CommandFactory, Parser}; use clap::{CommandFactory, Parser};
use clap_complete::CompleteEnv; use clap_complete::CompleteEnv;
@@ -67,7 +69,7 @@ async fn main() -> Result<()> {
|| cli.list_rags || cli.list_rags
|| cli.list_macros || cli.list_macros
|| cli.list_sessions; || 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.get_secret.is_some()
|| cli.update_secret.is_some() || cli.update_secret.is_some()
|| cli.delete_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())?; let log_path = setup_logger(working_mode.is_serve())?;
if secrets_flags { if vault_flags {
return cli.handle_secret_flag(Config::init_bare()?).await; return Vault::handle_vault_flags(cli, Config::init_bare()?);
} }
let abort_signal = create_abort_signal(); let abort_signal = create_abort_signal();
+42 -1
View File
@@ -32,7 +32,7 @@ use std::{env, mem, process};
const MENU_NAME: &str = "completion_menu"; 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(".help", "Show this help guide", AssertState::pass()),
ReplCommand::new(".info", "Show system info", AssertState::pass()), ReplCommand::new(".info", "Show system info", AssertState::pass()),
@@ -185,6 +185,11 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 36]> = LazyLock::new(|| {
AssertState::pass(), AssertState::pass(),
), ),
ReplCommand::new(".exit", "Exit REPL", AssertState::pass()), ReplCommand::new(".exit", "Exit REPL", AssertState::pass()),
ReplCommand::new(
".vault",
"View or modify the Loki vault",
AssertState::pass(),
),
] ]
}); });
static COMMAND_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*(\.\S*)\s*").unwrap()); static COMMAND_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*(\.\S*)\s*").unwrap());
@@ -767,6 +772,42 @@ pub async fn run_repl_command(
} }
_ => unknown_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 <name>");
}
}
Some(("get", name)) => {
if let Some(name) = name {
config.read().vault.get_secret(name)?;
} else {
println!("Usage: .vault get <name>");
}
}
Some(("update", name)) => {
if let Some(name) = name {
config.read().vault.update_secret(name)?;
} else {
println!("Usage: .vault update <name>");
}
}
Some(("delete", name)) => {
if let Some(name) = name {
config.read().vault.delete_secret(name)?;
} else {
println!("Usage: .vault delete <name>");
}
}
Some(("list", _)) => {
config.read().vault.list_secrets(true)?;
}
None | Some(_) => {
println!("Usage: .vault <add|get|update|delete|list> [name]")
}
},
_ => unknown_command()?, _ => unknown_command()?,
}, },
None => { None => {
+125
View File
@@ -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<Vec<String>> {
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(())
}
}
+132
View File
@@ -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(())
}