//! Command implementations and dispatch. pub mod completion_cmd; pub mod config_cmd; pub mod doctor_cmd; pub mod export_import; pub mod help_cmd; pub mod history_cmd; pub mod info_cmd; pub mod install_cmd; pub mod list_cmd; pub mod log_cmd; pub mod run_cmd; pub mod search_cmd; pub mod self_uninstall; pub mod sessions_cmd; pub mod stats_cmd; pub mod self_update; pub mod status_cmd; pub mod uninstall_cmd; pub mod update_cmd; pub mod version_cmd; use crate::app::App; use crate::cli::Command; use crate::config::{AgentDef, MethodSpec}; use crate::state::InstalledEntry; use anyhow::{anyhow, bail, Result}; use std::collections::BTreeMap; use std::path::Path; pub fn execute(app: &App) -> Result { match &app.cli.command { None => crate::repl::run(app), Some(cmd) => execute_command(app, cmd), } } /// Execute one parsed command (also used by the interactive shell). pub fn execute_command(app: &App, cmd: &Command) -> Result { match cmd { Command::List { all, category } => list_cmd::run(app, *all, category.as_deref()), Command::Start(a) => run_cmd::start(app, a), Command::Stop { agent, force, timeout, } => run_cmd::stop(app, agent, *force, *timeout), Command::Restart { start, force, timeout, } => run_cmd::restart(app, start, *force, *timeout), Command::Status { agent } => status_cmd::run(app, agent.as_deref()), Command::Stats { agent, period } => stats_cmd::run(app, agent.as_deref(), period.as_deref()), Command::Log { agent, kind, since, limit, } => log_cmd::run(app, agent.as_deref(), kind.as_deref(), since.as_deref(), *limit), Command::Sessions { agent, project, status, show, } => sessions_cmd::run( app, agent.as_deref(), project.as_deref(), status.as_deref(), show.as_deref(), ), Command::Install { agent, method, force, } => install_cmd::run(app, agent, method.as_deref(), *force), Command::Uninstall { agent, purge } => uninstall_cmd::run(app, agent, *purge), Command::Update { agent, all } => update_cmd::run(app, agent.as_deref(), *all), Command::Search { keyword, category } => search_cmd::run(app, keyword, category.as_deref()), Command::History { kind, cwd, search, failed, session, limit, } => history_cmd::run( app, kind.as_deref(), cwd.as_deref(), search.as_deref(), *failed, session.as_deref(), *limit, ), Command::Info { agent } => info_cmd::run(app, agent), Command::Help { command, find } => help_cmd::run(app, command.as_deref(), find.as_deref()), Command::Version => version_cmd::run(app), Command::Config(sub) => config_cmd::run(app, sub), Command::Doctor { fix } => doctor_cmd::run(app, *fix), Command::Run { agent, args } => run_cmd::run(app, agent, args), Command::Completion { shell } => completion_cmd::run(*shell), Command::SelfUpdate { check, to } => self_update::run(app, *check, to.as_deref()), Command::SelfUninstall => self_uninstall::run(app), Command::Export { output } => export_import::export_cmd(app, output), Command::Import { file } => export_import::import_cmd(app, file), } } // --------------------------------------------------------------------------- // Shared helpers // --------------------------------------------------------------------------- pub struct ResolvedExec { pub program: String, pub args: Vec, pub env: BTreeMap, } /// Resolve the program, arguments and environment for running an agent. /// Preference order: recorded binary > run command first token (resolved on /// PATH when possible). pub fn resolve_exec( app: &App, agent: &AgentDef, extra_args: &[String], extra_env: &BTreeMap, ) -> Result { let entry: Option = app.state.get(&agent.name).ok().flatten(); let mut tokens = agent.run_tokens(); if tokens.is_empty() { bail!("agent '{}' has no run command", agent.name); } let program = match &entry { Some(e) if !e.bins.is_empty() => { let p = std::path::PathBuf::from(&e.bins[0]); if p.exists() || app.dry_run() { p.display().to_string() } else { tokens.remove(0) } } _ => { let first = tokens.remove(0); if first.contains(std::path::MAIN_SEPARATOR) || Path::new(&first).exists() { first } else if let Ok(p) = which::which(&first) { p.display().to_string() } else { first } } }; let mut args: Vec = tokens; args.extend(agent.args.iter().cloned()); args.extend(extra_args.iter().cloned()); let mut env = agent.env.clone(); env.extend(extra_env.clone()); Ok(ResolvedExec { program, args, env }) } /// Parse --env KEY=VALUE entries. pub fn parse_env_list(items: &[String]) -> Result> { let mut map = BTreeMap::new(); for item in items { let Some((k, v)) = item.split_once('=') else { bail!("invalid --env '{item}': expected KEY=VALUE"); }; if k.trim().is_empty() { bail!("invalid --env '{item}': empty key"); } map.insert(k.to_string(), v.to_string()); } Ok(map) } /// Parse --args entries; quoted strings are split on whitespace. pub fn parse_extra_args(items: &[String]) -> Vec { let mut out = Vec::new(); for item in items { if item.contains(' ') || item.contains('\t') { if let Ok(parts) = shell_words::split(item) { out.extend(parts); continue; } } out.push(item.clone()); } out } /// Resolve an agent by name or alias, with a helpful error message. pub fn require_agent<'a>(app: &'a App, name_or_alias: &str) -> Result<&'a AgentDef> { app.catalog.resolve(name_or_alias).ok_or_else(|| { anyhow!( "unknown agent '{name_or_alias}' — run 'am search ' or 'am list' to see available agents" ) }) } /// One-line description of an install method. pub fn describe_method(m: &MethodSpec) -> String { let mut parts = vec![format!("type={}", m.kind)]; if let Some(p) = &m.package { parts.push(format!("package={p}")); } if let Some(u) = &m.url { parts.push(format!("url={u}")); } if let Some(s) = &m.script { parts.push(format!("script={s}")); } if let Some(r) = &m.repo { parts.push(format!("repo={r}")); } if let Some(b) = &m.binary_path { parts.push(format!("binary_path={b}")); } parts.join(" ") } /// Select an install method by index (1-based) or type name. pub fn select_method<'a>( methods: &[&'a MethodSpec], sel: Option<&str>, ) -> Result<&'a MethodSpec> { if methods.is_empty() { bail!("no install methods defined"); } match sel { None => Ok(methods[0]), Some(s) => { if let Ok(idx) = s.parse::() { if (1..=methods.len()).contains(&idx) { return Ok(methods[idx - 1]); } bail!("method index {idx} out of range (1..={})", methods.len()); } let lower = s.trim_start_matches("type:").to_lowercase(); methods .iter() .find(|m| m.kind.as_str() == lower) .copied() .ok_or_else(|| { anyhow!( "unknown method '{s}'; available: {}", methods .iter() .map(|m| m.kind.as_str()) .collect::>() .join(", ") ) }) } } }