//! Command implementations and dispatch. pub mod agents_cmd; pub mod alias_cmd; pub mod annotations_cmd; pub mod audit_cmd; pub mod catalog_cmd; pub mod completion_cmd; pub mod config_cmd; pub mod dashboard_cmd; pub mod doctor_cmd; pub mod export_import; pub mod help_cmd; pub mod history_cmd; pub mod info_cmd; pub mod init_cmd; pub mod install_cmd; pub mod list_cmd; pub mod log_cmd; pub mod logs_cmd; pub mod man_cmd; pub mod models_cmd; pub mod open_cmd; pub mod profile_cmd; pub mod projects_cmd; pub mod run_cmd; pub mod schedule_cmd; pub mod search_cmd; pub mod service_cmd; pub mod secret_cmd; pub mod self_uninstall; pub mod self_update; pub mod sessions_cmd; pub mod stats_cmd; pub mod status_cmd; pub mod suggest_cmd; pub mod theme_cmd; pub mod timeline_cmd; pub mod tip_cmd; pub mod uninstall_cmd; pub mod update_cmd; pub mod version_cmd; pub mod watch_cmd; use crate::app::App; use crate::cli::Command; use crate::config::{AgentDef, MethodSpec, UpdatePolicy}; 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, running, sort, } => list_cmd::run(app, *all, category.as_deref(), *running, sort.as_deref()), Command::Logs { agent, lines, follow, } => logs_cmd::run(app, agent, *lines, *follow), Command::Open { agent } => open_cmd::run(app, agent), Command::Watch { agent, restart, notify, interval, } => watch_cmd::run(app, agent, *restart, *notify, *interval), Command::Dashboard => dashboard_cmd::run(app), Command::Favorite { agent } => annotations_cmd::run_favorite(app, agent), Command::Unfavorite { agent } => annotations_cmd::run_unfavorite(app, agent), Command::Note { agent, text } => annotations_cmd::run_note(app, agent, text), Command::Tag { agent, words } => annotations_cmd::run_tag(app, agent, words), Command::Untag { agent, tag } => annotations_cmd::run_untag(app, agent, tag), Command::Tags { agent } => annotations_cmd::run_tags(app, agent.as_deref()), Command::Profile(sub) => profile_cmd::run(app, sub), Command::Man { command, output } => man_cmd::run(app, command.as_deref(), output.as_deref()), Command::Agents { installed } => agents_cmd::run(app, *installed), Command::Alias(sub) => alias_cmd::run(app, sub), Command::Secret(sub) => secret_cmd::run(app, sub), Command::Models(args) => models_cmd::run(app, args), Command::Catalog(sub) => catalog_cmd::run(app, sub), Command::Suggest { words } => suggest_cmd::run(app, &words.join(" ")), 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::Top { period } => stats_cmd::run_top(app, period.as_deref()), Command::Report { last_week, last_month, output, } => stats_cmd::run_report(app, *last_week, *last_month, output.as_deref()), Command::Log { agent, kind, since, limit, follow, } => log_cmd::run(app, agent.as_deref(), kind.as_deref(), since.as_deref(), *limit, *follow), Command::Projects { name } => projects_cmd::run(app, name.as_deref()), Command::Timeline { agent, project, since, limit, } => timeline_cmd::run(app, agent.as_deref(), project.as_deref(), since.as_deref(), *limit), Command::Sessions { agent, project, status, show, resume, export, output, retention, } => sessions_cmd::run( app, agent.as_deref(), project.as_deref(), status.as_deref(), show.as_deref(), resume.as_deref(), export.as_deref(), output.as_deref(), *retention, ), 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, rollback } => { update_cmd::run(app, agent.as_deref(), *all, rollback.as_deref()) } Command::Audit => audit_cmd::run(app), Command::Search { keyword, category, tag } => { search_cmd::run(app, keyword.as_deref(), category.as_deref(), tag.as_deref()) } Command::History { kind, cwd, search, failed, session, limit, rerun, } => history_cmd::run( app, kind.as_deref(), cwd.as_deref(), search.as_deref(), *failed, session.as_deref(), *limit, *rerun, ), Command::Info { agent } => info_cmd::run(app, agent), Command::Init { force, template } => { init_cmd::run(app, *force, template.as_deref()) } Command::Help { command, find } => help_cmd::run(app, command.as_deref(), find.as_deref()), Command::Version => version_cmd::run(app), Command::Tip { random } => tip_cmd::run(app, *random), Command::Config(sub) => config_cmd::run(app, sub), Command::Theme { name } => theme_cmd::run(app, name.as_deref()), Command::Doctor { fix, watch } => { doctor_cmd::run(app, *fix, *watch) } Command::Run { agent, model, container, args } => { run_cmd::run(app, agent, model.as_deref(), *container, args) } Command::Service(svc) => match svc { crate::cli::ServiceCmd::Install { agent, autostart } => { service_cmd::run(app, "install", Some(agent), *autostart) } crate::cli::ServiceCmd::Uninstall { agent } => { service_cmd::run(app, "uninstall", Some(agent), false) } crate::cli::ServiceCmd::Status { agent } => { service_cmd::run(app, "status", Some(agent), false) } }, Command::Schedule(sched) => match sched { crate::cli::ScheduleCmd::Add { command, at } => { // '--at HH:MM' may land inside the trailing command args // (allow_hyphen_values): strip it and recover the value. let mut cmd = command.clone(); let mut at_value = at.clone(); if let Some(idx) = cmd.iter().position(|a| a == "--at") { if idx + 1 < cmd.len() { at_value = at_value.or_else(|| Some(cmd[idx + 1].clone())); cmd.remove(idx + 1); } cmd.remove(idx); } schedule_cmd::run(app, "add", cmd, at_value.as_deref()) } crate::cli::ScheduleCmd::List => schedule_cmd::run(app, "list", vec![], None), crate::cli::ScheduleCmd::Remove { id } => { schedule_cmd::run(app, "remove", vec![id.clone()], None) } crate::cli::ScheduleCmd::Run { id } => schedule_cmd::run(app, "run", vec![id.clone()], None), }, Command::Completion { shell, installed } => completion_cmd::run(*shell, *installed), 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()); let warnings = crate::secrets::resolve_env_secrets(&crate::secrets::store(), &agent.name, &mut env); for w in warnings { app.log.warn(&w); } let (program, args) = route_cmd_script(program, args); Ok(ResolvedExec { program, args, env }) } /// On Windows, .cmd/.bat shims (git installs without a build step) must be /// executed through cmd.exe — spawning them directly fails. pub fn route_cmd_script(program: String, args: Vec) -> (String, Vec) { let lower = program.to_lowercase(); if cfg!(windows) && (lower.ends_with(".cmd") || lower.ends_with(".bat")) { let shell = std::env::var_os("COMSPEC") .map(|s| s.to_string_lossy().to_string()) .unwrap_or_else(|| "cmd.exe".to_string()); let mut full = vec!["/C".to_string(), program]; full.extend(args); (shell, full) } else { (program, args) } } /// 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(", ") ) }) } } } #[cfg(test)] mod tests { use super::*; #[test] fn routes_cmd_scripts_through_comspec() { let (program, args) = route_cmd_script( "C:/x/agent.cmd".to_string(), vec!["--flag".to_string()], ); let lower = program.to_lowercase(); if cfg!(windows) { assert!(lower.ends_with("cmd.exe"), "{program}"); assert_eq!(args[0], "/C"); assert_eq!(args[1], "C:/x/agent.cmd"); assert_eq!(args[2], "--flag"); } else { assert_eq!(program, "C:/x/agent.cmd"); assert_eq!(args, vec!["--flag".to_string()]); } } #[test] fn leaves_regular_programs_untouched() { let (program, args) = route_cmd_script("node".to_string(), vec!["-v".to_string()]); assert_eq!(program, "node"); assert_eq!(args, vec!["-v".to_string()]); } }