use anyhow::{anyhow, Context, Result};
use clap::{Args as ClapArgs, ValueEnum};
use serde_json::json;
use std::io::{IsTerminal, Write};
use std::process::{Command, Stdio};
use crate::audit;
use crate::cli::Global;
use crate::envelope;
use crate::exit;
use crate::keychain;
#[derive(Debug, Clone, ValueEnum)]
pub enum Target {
Vercel,
Gh,
Fly,
}
#[derive(Debug, ClapArgs)]
pub struct Args {
pub target: Target,
pub name: String,
#[arg(long, default_value = "production")]
pub env: String,
#[arg(long)]
pub project: Option<String>,
#[arg(long)]
pub repo: Option<String>,
#[arg(long)]
pub app: Option<String>,
}
pub fn run(args: Args, global: &Global) -> Result<u8> {
let value = keychain::get(&args.name)?;
let (cmd_label, exit_status) = match args.target {
Target::Vercel => push_vercel(&args, &value)?,
Target::Gh => push_gh(&args, &value)?,
Target::Fly => push_fly(&args, &value)?,
};
let ok = exit_status.success();
let target_str = target_label(&args.target);
let _ = audit::append(&audit::Entry {
ts: audit::now(),
akm_version: audit::AKM_VERSION,
command: "push",
keys: vec![args.name.clone()],
input_mode: None,
child_command: Some(cmd_label.clone()),
injected_keys: None,
push_target: Some(target_str),
cwd: audit::cwd_string(),
ppid: audit::ppid(),
parent_exe: audit::parent_exe(audit::ppid()),
status: if ok { "ok" } else { "upstream_failed" },
});
let code = if ok {
exit::SUCCESS
} else {
exit::TRANSIENT
};
let json_mode = global.json || !std::io::stdout().is_terminal();
if json_mode {
println!(
"{}",
envelope::ok(json!({
"target": target_str,
"name": args.name,
"ok": ok,
"command": cmd_label,
}))
);
} else if !global.quiet {
eprintln!(
"akm: pushed {} -> {} ({})",
args.name,
target_str,
if ok { "ok" } else { "failed" }
);
}
Ok(code)
}
fn push_vercel(args: &Args, value: &str) -> Result<(String, std::process::ExitStatus)> {
which::which("vercel").context("`vercel` CLI not found in PATH")?;
let mut cmd = Command::new("vercel");
cmd.arg("env").arg("add").arg(&args.name).arg(&args.env);
if let Some(p) = &args.project {
cmd.arg("--cwd").arg(p);
}
cmd.stdin(Stdio::piped());
let label = format!("vercel env add {} {}", args.name, args.env);
let mut child = cmd.spawn()?;
{
let stdin = child
.stdin
.as_mut()
.ok_or_else(|| anyhow!("failed to open vercel stdin"))?;
stdin.write_all(value.as_bytes())?;
stdin.write_all(b"\n")?;
}
let status = child.wait()?;
Ok((label, status))
}
fn push_gh(args: &Args, value: &str) -> Result<(String, std::process::ExitStatus)> {
which::which("gh").context("`gh` CLI not found in PATH")?;
let repo = args
.repo
.as_ref()
.ok_or_else(|| anyhow!("--repo OWNER/NAME is required for `push gh`"))?;
let mut cmd = Command::new("gh");
cmd.arg("secret")
.arg("set")
.arg(&args.name)
.arg("--repo")
.arg(repo)
.arg("--body")
.arg("-");
cmd.stdin(Stdio::piped());
let label = format!("gh secret set {} --repo {}", args.name, repo);
let mut child = cmd.spawn()?;
{
let stdin = child
.stdin
.as_mut()
.ok_or_else(|| anyhow!("failed to open gh stdin"))?;
stdin.write_all(value.as_bytes())?;
}
let status = child.wait()?;
Ok((label, status))
}
fn push_fly(args: &Args, value: &str) -> Result<(String, std::process::ExitStatus)> {
let bin = which::which("fly")
.or_else(|_| which::which("flyctl"))
.context("`fly` / `flyctl` CLI not found in PATH")?;
let app = args
.app
.as_ref()
.ok_or_else(|| anyhow!("--app APPNAME is required for `push fly`"))?;
let mut cmd = Command::new(bin);
cmd.arg("secrets").arg("import").arg("--app").arg(app);
cmd.stdin(Stdio::piped());
let label = format!("fly secrets import --app {}", app);
let mut child = cmd.spawn()?;
{
let stdin = child
.stdin
.as_mut()
.ok_or_else(|| anyhow!("failed to open fly stdin"))?;
writeln!(stdin, "{}={}", args.name, value)?;
}
let status = child.wait()?;
Ok((label, status))
}
fn target_label(t: &Target) -> &'static str {
match t {
Target::Vercel => "vercel",
Target::Gh => "gh",
Target::Fly => "fly",
}
}