Files
agent-manager/src/commands/mod.rs
T

259 lines
8.2 KiB
Rust

//! 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<i32> {
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<i32> {
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<String>,
pub env: BTreeMap<String, String>,
}
/// 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<String, String>,
) -> Result<ResolvedExec> {
let entry: Option<InstalledEntry> = 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<String> = 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<BTreeMap<String, String>> {
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<String> {
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 <keyword>' 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::<usize>() {
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::<Vec<_>>()
.join(", ")
)
})
}
}
}