259 lines
8.2 KiB
Rust
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(", ")
|
|
)
|
|
})
|
|
}
|
|
}
|
|
}
|