a3s 0.8.0

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
use std::process::Stdio;

use super::catalog;

pub(crate) const USAGE: &str = "usage:\n\
  a3s account list\n\
  a3s account status\n\
  a3s account login <claude-code|codex|a3s-os> [token]\n\
  a3s account logout <claude-code|codex|a3s-os>\n";

pub(crate) async fn run(args: &[String]) -> anyhow::Result<()> {
    match args.first().map(String::as_str) {
        None | Some("list" | "status") => {
            if args.len() > 1 {
                anyhow::bail!("account list/status does not accept arguments\n\n{USAGE}");
            }
            print_accounts();
            Ok(())
        }
        Some("login") => login(&args[1..]).await,
        Some("logout") => logout(&args[1..]).await,
        Some("-h" | "--help" | "help") if args.len() == 1 => {
            print!("{USAGE}");
            Ok(())
        }
        Some(other) => anyhow::bail!("unknown account command `{other}`\n\n{USAGE}"),
    }
}

fn print_accounts() {
    println!("SOURCE\tSTATUS\tACCOUNT");
    for account in catalog::discover() {
        let status = if account.signed_in {
            "signed-in"
        } else {
            "signed-out"
        };
        let account_label = account
            .label
            .or(account.detail)
            .unwrap_or_else(|| "-".to_string());
        println!("{}\t{status}\t{account_label}", account.source.id());
    }
}

async fn login(args: &[String]) -> anyhow::Result<()> {
    let Some(source) = args.first().map(String::as_str) else {
        anyhow::bail!("account login requires an account source\n\n{USAGE}");
    };
    match source {
        "a3s-os" => crate::os_cmd::login(&args[1..]).await,
        "claude-code" => {
            reject_extra_account_args(source, &args[1..])?;
            run_product_command("claude", &["auth", "login"]).await
        }
        "codex" => {
            reject_extra_account_args(source, &args[1..])?;
            run_product_command("codex", &["login"]).await
        }
        _ => anyhow::bail!("unknown account source `{source}`\n\n{USAGE}"),
    }
}

async fn logout(args: &[String]) -> anyhow::Result<()> {
    let [source] = args else {
        anyhow::bail!("account logout requires exactly one account source\n\n{USAGE}");
    };
    match source.as_str() {
        "a3s-os" => crate::os_cmd::logout(&[]),
        "claude-code" => run_product_command("claude", &["auth", "logout"]).await,
        "codex" => run_product_command("codex", &["logout"]).await,
        _ => anyhow::bail!("unknown account source `{source}`\n\n{USAGE}"),
    }
}

fn reject_extra_account_args(source: &str, args: &[String]) -> anyhow::Result<()> {
    if args.is_empty() {
        Ok(())
    } else {
        anyhow::bail!("{source} login does not accept a token; its product CLI owns authentication")
    }
}

async fn run_product_command(program: &str, args: &[&str]) -> anyhow::Result<()> {
    let status = tokio::process::Command::new(program)
        .args(args)
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()
        .await
        .map_err(|error| anyhow::anyhow!("could not run `{program}`: {error}"))?;
    if status.success() {
        Ok(())
    } else {
        anyhow::bail!("`{program} {}` exited with {status}", args.join(" "))
    }
}