api-key-manager 0.1.3

Agent-driven macOS Keychain CLI for API keys. Zero human friction.
use clap::{Args as ClapArgs, Parser, Subcommand};

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 = "Agent-driven macOS Keychain CLI for API keys. Zero human friction.",
    long_about = "akm stores API keys in the macOS Login Keychain and injects them into child processes \
                  without writing plaintext to disk, shell history, or process argv. Designed so that AI \
                  coding agents (Claude Code, Cursor, Codex) can drive the entire workflow with no \
                  human-in-the-loop prompts.",
    after_long_help = "Tips:\n  - From an agent, pass key values via the subprocess stdin API (e.g. Python\n    `subprocess.run([\"akm\",\"add\",\"NAME\"], input=value)`). Avoid wrapping the value in a shell command — it lands in shell history.\n  - Use `akm run --only OPENAI_API_KEY -- <cmd>` to scope injection. `--all` injects every stored key (large blast radius).\n  - Use `akm stdin <name> -- vercel env add <name> production --force` to feed the value to any upstream CLI.\n  - Run `akm agent-info --json` for a machine-readable capability manifest.\n  - Install the agent skill: `akm skill install`.\n\nExamples:\n  printf %s \"$VALUE\" | akm add OPENAI_API_KEY\n  akm run --only OPENAI_API_KEY -- python script.py\n  akm stdin OPENAI_API_KEY -- gh secret set OPENAI_API_KEY --repo me/myproj\n  akm list --json | jq '.data.keys[]'"
)]
pub struct Cli {
    #[command(flatten)]
    pub global: Global,

    #[command(subcommand)]
    pub command: Cmd,
}

#[derive(Debug, Clone, Default, ClapArgs)]
pub struct Global {
    /// Emit JSON envelope output on stdout (forced on when stdout is piped).
    #[arg(long, global = true)]
    pub json: bool,

    /// Suppress non-essential stderr output.
    #[arg(long, global = true)]
    pub quiet: bool,
}

#[derive(Debug, Subcommand)]
pub enum Cmd {
    /// Store a key. Reads value from argv or stdin.
    Add(commands::add::Args),
    /// Retrieve a key. Masked by default; use --raw for the unmasked value.
    Get(commands::get::Args),
    /// Run a command with keys injected as environment variables.
    Run(commands::run::Args),
    /// Write a key value to a child process's stdin (replaces push wrappers).
    Stdin(commands::stdin_cmd::Args),
    /// List stored key names.
    List(commands::list::Args),
    /// Remove a key.
    Rm(commands::rm::Args),
    /// Print the audit log.
    Audit(commands::audit_cmd::Args),
    /// Manage the optional pre-commit hook that scans staged files for key prefixes.
    Guard(commands::guard::Args),
    /// Print the machine-readable capability manifest.
    AgentInfo(commands::agent_info::Args),
    /// Install or update the bundled agent skill (Claude Code, Codex, Gemini).
    #[command(name = "skill")]
    Skill(commands::skill_install::Args),
}

pub fn run() -> u8 {
    let Cli { global, command } = Cli::parse();
    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::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) => {
            // For machine consumers we always emit a JSON error envelope on
            // stdout. For humans (TTY stdout, not --json) we use stderr.
            let json_mode = global.json || !atty::is(atty::Stream::Stdout);
            if json_mode {
                println!(
                    "{}",
                    envelope::err(err.code_str(), err.to_string(), None)
                );
            } else {
                eprintln!("akm: {}: {}", err.code_str(), err);
            }
            // BAD_INPUT cap defensively to avoid leaking surprising codes.
            let code = err.exit_code();
            if code == 0 {
                exit::TRANSIENT
            } else {
                code
            }
        }
    }
}