use clap::{Args as ClapArgs, Parser, Subcommand};
use std::io::IsTerminal;
use crate::commands;
use crate::envelope;
use crate::error::AkmError;
use crate::exit;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Parser)]
#[command(
name = "akm",
version = VERSION,
about = "Use macOS Keychain secrets in commands without copying their values.",
long_about = "Store API keys in the macOS Login Keychain. Run commands with selected keys as environment variables, or supply a value through stdin. Keychain access is noninteractive; unavailable access returns an error.",
after_long_help = "Examples:\n akm run --only OPENAI_API_KEY -- python script.py\n akm run --only API_KEY=PROJECT_API_KEY -- node server.js\n akm stdin OPENAI_API_KEY -- gh secret set OPENAI_API_KEY\n akm list --names-only\n akm agent-info --command run\n\nStore values through your subprocess API's stdin: subprocess.run([\"akm\",\"add\",\"NAME\"], input=value).\nInstall the optional agent instructions with `akm skill install`."
)]
pub struct Cli {
#[command(flatten)]
pub global: Global,
#[command(subcommand)]
pub command: Cmd,
}
#[derive(Debug, Clone, Default, ClapArgs)]
pub struct Global {
#[arg(long, global = true)]
pub json: bool,
#[arg(long, global = true)]
pub quiet: bool,
}
#[derive(Debug, Subcommand)]
pub enum Cmd {
Add(commands::add::Args),
Get(commands::get::Args),
Run(commands::run::Args),
Stdin(commands::stdin_cmd::Args),
#[command(visible_alias = "ls")]
List(commands::list::Args),
Export(commands::export::Args),
Import(commands::import_cmd::Args),
Migrate(commands::migrate::Args),
Rm(commands::rm::Args),
Audit(commands::audit_cmd::Args),
Guard(commands::guard::Args),
#[command(visible_alias = "info")]
AgentInfo(commands::agent_info::Args),
#[command(name = "skill")]
Skill(commands::skill_install::Args),
}
pub fn run() -> u8 {
let json_mode = !std::io::stdout().is_terminal()
|| std::env::args_os()
.skip(1)
.take_while(|arg| arg != "--")
.any(|arg| arg == "--json");
let Cli { global, command } = match Cli::try_parse() {
Ok(cli) => cli,
Err(err) => {
if matches!(
err.kind(),
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
) {
print!("{err}");
return exit::SUCCESS;
}
let message = match err.kind() {
clap::error::ErrorKind::UnknownArgument => "unknown argument",
clap::error::ErrorKind::InvalidSubcommand => "unknown command",
clap::error::ErrorKind::MissingRequiredArgument => "missing required argument",
clap::error::ErrorKind::ArgumentConflict => "conflicting arguments",
_ => "invalid command arguments",
};
return report_error(&AkmError::BadInput(message.into()), json_mode);
}
};
let _keychain_ui =
match security_framework::os::macos::keychain::SecKeychain::disable_user_interaction() {
Ok(guard) => guard,
Err(error) => {
return report_error(
&AkmError::KeychainUnavailable(format!(
"cannot disable Keychain dialogs: {error}"
)),
json_mode,
)
}
};
let result: Result<u8, AkmError> = match command {
Cmd::Add(args) => commands::add::run(args, &global),
Cmd::Get(args) => commands::get::run(args, &global),
Cmd::Run(args) => commands::run::run(args, &global),
Cmd::Stdin(args) => commands::stdin_cmd::run(args, &global),
Cmd::List(args) => commands::list::run(args, &global),
Cmd::Export(args) => commands::export::run(args, &global),
Cmd::Import(args) => commands::import_cmd::run(args, &global),
Cmd::Migrate(args) => commands::migrate::run(args, &global),
Cmd::Rm(args) => commands::rm::run(args, &global),
Cmd::Audit(args) => commands::audit_cmd::run(args, &global),
Cmd::Guard(args) => commands::guard::run(args, &global),
Cmd::AgentInfo(args) => commands::agent_info::run(args, &global),
Cmd::Skill(args) => commands::skill_install::run(args, &global),
};
match result {
Ok(code) => code,
Err(err) => report_error(&err, global.json || !std::io::stdout().is_terminal()),
}
}
fn report_error(err: &AkmError, json_mode: bool) -> u8 {
if json_mode {
eprintln!(
"{}",
envelope::err(err.code_str(), err.to_string(), Some(err.suggestion()))
);
} else {
eprintln!("akm: {}: {}\n{}", err.code_str(), err, err.suggestion());
}
err.exit_code()
}