Files
agent-manager/src/cli.rs
T
bruno 4c7866a25a feat: phase 2 — am lab (benchmark N agents) + plugins d'événements (contrat JSON)
- #62: am lab --agents a,b --task <f> — séquentiel ou --parallel, capture
  durée/exit/sortie/coût estimé, rapport comparatif tableau + --json stable,
  tâches versionnables dans <state>/lab/ (--list), rejouables à l'identique
- #75: plugins/ sous state_dir, déclenchés sur on_install/on_start/on_stop/
  on_update (App::emit), contrat JSON stdin/stdout, timeout configurable,
  échec non bloquant, garde anti-récursion AM_PLUGINS_RUNNING, am plugins
  + am plugins --test <name> (CI), exemples notify + ci-webhook
- intégration REPL (parse, complétion, bannière, is_am_command), help, tips,
  man pages (am-lab.1, am-plugins.1), README, ROADMAP (26/27), v0.5.4
- 334 tests verts (309 + 25)

closes #62
closes #75
2026-08-18 14:17:30 -04:00

748 lines
26 KiB
Rust

//! Command line interface definition (clap v4, derive API).
use clap::{ArgAction, Args, Parser, Subcommand};
use clap_complete::Shell;
use std::path::PathBuf;
/// Manage local AI coding agents: install, start, stop, update.
#[derive(Parser, Debug, Clone)]
#[command(
name = "am",
bin_name = "am",
version,
// The help subcommand is provided by the custom Nushell-style help
// (cli::Command::Help) instead of clap's generated one.
disable_help_subcommand = true,
about = "agent-manager (am) — manage local AI coding agents",
long_about = "agent-manager (am) — a CLI to manage local AI coding agents.
Install, uninstall, start, stop, restart and update AI agents from a YAML
catalog. Handles dependencies (Node, Python, Go, Rust, Bun), binaries from
GitHub Releases, install scripts, and git repositories. All state lives under
your user directories; nothing pollutes the system."
)]
pub struct Cli {
/// Use an alternate configuration file
#[arg(short = 'c', long, global = true, value_name = "FILE")]
pub config: Option<PathBuf>,
/// Verbose output: show every command executed and its details
#[arg(short = 'v', long, global = true, action = ArgAction::SetTrue)]
pub verbose: bool,
/// Quiet mode: only errors are printed
#[arg(short = 'q', long, global = true, conflicts_with = "verbose", action = ArgAction::SetTrue)]
pub quiet: bool,
/// Answer yes to every confirmation prompt
#[arg(short = 'y', long, global = true, action = ArgAction::SetTrue)]
pub yes: bool,
/// Simulate the action without changing anything
#[arg(long, global = true, action = ArgAction::SetTrue)]
pub dry_run: bool,
/// Emit machine-readable JSON on stdout (for scripts)
#[arg(long, global = true, action = ArgAction::SetTrue)]
pub json: bool,
/// Disable ANSI colors
#[arg(long, global = true, action = ArgAction::SetTrue)]
pub no_color: bool,
/// Color theme for the output (see 'am help theme' for the list)
#[arg(long, global = true, value_name = "THEME")]
pub theme: Option<String>,
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Subcommand, Debug, Clone)]
pub enum Command {
/// List installed agents (default); use --all for the whole catalog
List {
/// Show every agent known to the catalog, not only the installed ones
#[arg(long)]
all: bool,
/// Only show agents of this category
#[arg(long, value_name = "CATEGORY")]
category: Option<String>,
/// Only show running agents
#[arg(long)]
running: bool,
/// Sort by name, version or status (default: name)
#[arg(long, value_name = "KEY")]
sort: Option<String>,
},
/// Tail the log file of an agent (--follow for a live view)
Logs {
/// Agent name or alias
agent: String,
/// Number of lines to show (default: 20)
#[arg(long, value_name = "N", default_value = "20")]
lines: usize,
/// Follow the log as it grows
#[arg(long)]
follow: bool,
},
/// Open an agent's installation directory in the file manager
Open {
/// Agent name or alias
agent: String,
},
/// Supervise an agent and optionally restart it on crash
Watch {
/// Agent name or alias
agent: String,
/// Restart the agent automatically when it dies (with backoff)
#[arg(long)]
restart: bool,
/// Desktop notification on every restart
#[arg(long)]
notify: bool,
/// Check interval in seconds (default: 5)
#[arg(long, value_name = "SECS", default_value = "5")]
interval: u64,
},
/// Live TUI dashboard: overview, activity, stats, sessions, projects
Dashboard,
/// Mark an agent as a favorite (star in list and status)
Favorite {
/// Agent name or alias
agent: String,
},
/// Remove the favorite star from an agent
Unfavorite {
/// Agent name or alias
agent: String,
},
/// Attach a free-form note to an agent (shown in 'am info');
/// 'am note <agent>' without text prints the current note
Note {
/// Agent name or alias
agent: String,
/// Note text (several words are joined); empty prints the note
#[arg(value_name = "TEXT...", num_args = 0..)]
text: Vec<String>,
},
/// Add a personal tag to an agent
Tag {
/// Agent name or alias
agent: String,
/// One or more tags to add
#[arg(value_name = "TAG", num_args = 1.., required = true)]
words: Vec<String>,
},
/// Remove a personal tag from an agent
Untag {
/// Agent name or alias
agent: String,
/// Tag to remove
tag: String,
},
/// List personal tags (with counts), optionally for one agent
Tags {
/// Only show the tags of this agent
agent: Option<String>,
},
/// Manage environment profiles (dev, prod, ...)
#[command(subcommand)]
Profile(ProfileCmd),
/// Generate a man page for am (or for one command)
Man {
/// Command name (omit for the whole CLI)
command: Option<String>,
/// Write the page(s) to this directory instead of stdout
#[arg(long, value_name = "DIR")]
output: Option<PathBuf>,
},
/// Set or show the active color theme (persisted for the next run)
Theme {
/// Theme name (omit to list available themes)
name: Option<String>,
},
/// Internal: list agent names for shell completions (hidden)
#[command(name = "_agents", hide = true)]
Agents {
/// Only installed agents (managed + external), aliases and groups
#[arg(long)]
installed: bool,
},
/// Manage command aliases (cc -> claude-code)
#[command(subcommand)]
Alias(AliasCmd),
/// Manage secrets in the OS keyring (never in plaintext config)
#[command(subcommand)]
Secret(SecretCmd),
/// Audit the configuration: who changed what, when (issue #63)
Audit,
/// Register an agent as a system service (issue #55)
#[command(subcommand)]
Service(ServiceCmd),
/// Schedule am commands (issue #56)
#[command(subcommand)]
Schedule(ScheduleCmd),
/// List local models (ollama, llama.cpp, LM Studio) and prune unused ones
Models(ModelsArgs),
/// Manage remote catalogs: update the official one, add external ones
#[command(subcommand)]
Catalog(CatalogCmd),
/// Suggest agents matching a query, boosted by real usage (issue #61)
Suggest {
/// Free-form request ("un agent pour du Python")
#[arg(value_name = "QUERY", num_args = 1.., required = true)]
words: Vec<String>,
},
/// Start an agent (foreground by default, or detached with --background)
Start(StartArgs),
/// Stop a background agent (SIGTERM, then SIGKILL after the timeout)
Stop {
/// Agent name, alias, or group:<name>
agent: String,
/// Kill immediately instead of terminating gracefully
#[arg(long)]
force: bool,
/// Grace period in seconds before killing (default: from config, 5s)
#[arg(long, value_name = "SECS")]
timeout: Option<u64>,
},
/// Restart an agent: stop, then start with the same options
Restart {
#[command(flatten)]
start: StartArgs,
/// Kill immediately instead of terminating gracefully
#[arg(long)]
force: bool,
/// Grace period in seconds before killing (default: from config, 5s)
#[arg(long, value_name = "SECS")]
timeout: Option<u64>,
},
/// Show the state of one agent, or of every installed agent
Status {
/// Agent name or alias (omit to show all installed agents)
agent: Option<String>,
},
/// List the sessions of every agent (and of the interactive shell)
Sessions {
/// Only sessions of this agent
agent: Option<String>,
/// Only sessions of this project
#[arg(long, value_name = "PROJECT")]
project: Option<String>,
/// Only sessions with this status (running, stopped, failed, interrupted)
#[arg(long, value_name = "STATUS")]
status: Option<String>,
/// Show one session in detail (summary + log excerpt)
#[arg(long, value_name = "ID")]
show: Option<String>,
/// Resume a finished session: relaunch the agent with its recorded arguments
#[arg(long, value_name = "ID")]
resume: Option<String>,
/// Export one session (metadata, commands, log excerpt) to a JSON file
#[arg(long, value_name = "ID")]
export: Option<String>,
/// Destination of the export (default: ./am-session-<ID>.json)
#[arg(long, value_name = "FILE")]
output: Option<String>,
/// Purge sessions finished more than N days ago (settings default 90)
#[arg(long, value_name = "DAYS")]
retention: Option<u64>,
},
/// Show usage statistics computed from the event journal
Stats {
/// Agent name or alias (omit for the global view)
agent: Option<String>,
/// Only events of the last period (7d, 30d, 90d, all)
#[arg(long, value_name = "PERIOD")]
period: Option<String>,
/// Show token usage and estimated spend per agent (issue #49)
#[arg(long, action = ArgAction::SetTrue)]
costs: bool,
},
/// Real-time monitor of the managed processes (issue #50)
Monitor {
/// Refresh interval in seconds (default 2)
#[arg(long, value_name = "SECONDS")]
interval: Option<u64>,
/// Print one JSON document per tick instead of the TUI
#[arg(long, action = ArgAction::SetTrue)]
json: bool,
},
/// Push the state into the configured git repository (issue #66)
Sync {
/// Commit message (default: "am sync — state update")
#[arg(long, value_name = "MESSAGE")]
message: Option<String>,
},
/// Transfer the installation and state to another machine (issue #68)
Migrate {
/// Export the bundle (default) or import one
#[arg(long, action = ArgAction::SetTrue)]
export: bool,
/// Bundle path for export (default agent-manager-migrate.amx)
#[arg(long, value_name = "FILE")]
output: Option<PathBuf>,
/// Bundle path to import
#[arg(value_name = "BUNDLE")]
bundle: Option<PathBuf>,
},
/// Show the most used agents (top 10)
Top {
/// Only events of the last period (7d, 30d, 90d, all)
#[arg(long, value_name = "PERIOD")]
period: Option<String>,
},
/// Write a markdown activity report
Report {
/// Cover the last 7 days
#[arg(long, conflicts_with = "last_month")]
last_week: bool,
/// Cover the last 30 days
#[arg(long)]
last_month: bool,
/// Output file (default: report-YYYYMMDD.md in the state directory)
#[arg(long, value_name = "FILE")]
output: Option<PathBuf>,
},
/// Show which agent works on which project
Projects {
/// Project name (omit to list every project)
name: Option<String>,
},
/// One chronological view of every activity (journal + REPL history)
Timeline {
/// Only activity of this agent
agent: Option<String>,
/// Only activity of this project
#[arg(long, value_name = "PROJECT")]
project: Option<String>,
/// Only activity at or after this date (YYYY-MM-DD or RFC 3339)
#[arg(long, value_name = "DATE")]
since: Option<String>,
/// Maximum number of entries (default: 50)
#[arg(long, value_name = "N", default_value = "50")]
limit: usize,
},
/// Read the event journal: everything am did
Log {
/// Only events of this agent
agent: Option<String>,
/// Only these event kinds (comma separated: start,stop,run,install,...)
#[arg(long, value_name = "KINDS")]
kind: Option<String>,
/// Only events at or after this date (YYYY-MM-DD or RFC 3339)
#[arg(long, value_name = "DATE")]
since: Option<String>,
/// Maximum number of events (default: 50)
#[arg(long, value_name = "N", default_value = "50")]
limit: usize,
/// Follow the journal as new events arrive
#[arg(long)]
follow: bool,
},
/// Install an agent and its dependencies
Install {
/// Agent name or alias
agent: String,
/// Select the install method (index, or type: npm, pip, uv, cargo, go, bun, curl, binary, git)
#[arg(long, value_name = "METHOD")]
method: Option<String>,
/// Reinstall even if already installed
#[arg(long)]
force: bool,
},
/// Uninstall an agent (managed or external) and clean its files
Uninstall {
/// Agent name or alias
agent: String,
/// Also remove logs and the agent entry from the user config file
#[arg(long)]
purge: bool,
},
/// Update an installed agent to the latest available version
Update {
/// Agent name or alias (required unless --all)
agent: Option<String>,
/// Update every installed managed agent
#[arg(long)]
all: bool,
/// Roll back to a pre-update backup instead of updating
/// ("latest" or a backup id; "--rollback list" shows them)
#[arg(long, num_args = 0..=1, default_missing_value = "latest")]
rollback: Option<String>,
},
/// Search the catalog by keyword (name, description, category, tags)
Search {
/// Keyword to search for (case-insensitive substring)
#[arg(value_name = "KEYWORD", required_unless_present = "tag")]
keyword: Option<String>,
/// Restrict to this category
#[arg(long, value_name = "CATEGORY")]
category: Option<String>,
/// Only agents carrying this personal tag
#[arg(long, value_name = "TAG")]
tag: Option<String>,
},
/// Search the command history (am and shell commands)
History {
/// Only commands of this kind (am or shell)
#[arg(long, value_name = "KIND")]
kind: Option<String>,
/// Only commands run in this directory
#[arg(long, value_name = "DIR")]
cwd: Option<String>,
/// Full-text search in the command line
#[arg(long, value_name = "TEXT")]
search: Option<String>,
/// Only failed commands
#[arg(long)]
failed: bool,
/// Only commands of this session
#[arg(long, value_name = "ID")]
session: Option<String>,
/// Maximum number of entries (default: 100)
#[arg(long, value_name = "N", default_value = "100")]
limit: usize,
/// Re-run the Nth most recent entry (with confirmation)
#[arg(long, value_name = "N")]
rerun: Option<usize>,
/// Range to export as a playbook: "12..25" (1-based, newest first)
#[arg(value_name = "RANGE")]
range: Option<String>,
/// Export the range as a playbook YAML file (issue #51)
#[arg(long, value_name = "FILE")]
save: Option<PathBuf>,
},
/// Replay a saved playbook step by step with confirmation (issue #51)
Playbook {
/// Playbook file (state_dir/playbooks/ when a bare name is given)
#[arg(value_name = "FILE")]
path: PathBuf,
/// Variable values, repeatable: --var name=value
#[arg(long, value_name = "NAME=VALUE")]
var: Vec<String>,
},
/// Benchmark: run the same task on several agents and compare (issue #62)
Lab(LabArgs),
/// List the event plugins and test one (issue #75)
Plugins(PluginsArgs),
/// Show detailed information about one agent
Info {
/// Agent name or alias
agent: String,
},
/// Generate a local agent-manager.yaml for the current directory
Init {
/// Overwrite an existing agent-manager.yaml
#[arg(long)]
force: bool,
/// Use a template (web, python, rust, cli) instead of stack detection;
/// "list" shows the available templates
#[arg(long, value_name = "TEMPLATE")]
template: Option<String>,
},
/// Display help: the overview, a command, the command list, or a search
Help {
/// Command name (also accepts an agent name or alias); "commands" lists every command
command: Option<String>,
/// Search through all help commands table
#[arg(long, value_name = "TEXT")]
find: Option<String>,
},
/// Display version and build information
Version,
/// Cheat sheet: the most useful commands, their key options, your
/// most-used commands and a rotating tip of the day
Tip {
/// Show a single random tip instead of the whole page
#[arg(long)]
random: bool,
},
/// Manage the configuration file
#[command(subcommand)]
Config(ConfigCmd),
/// Check the environment: tools, config validity, paths, permissions
Doctor {
/// Attempt to repair problems (create directories, fix the state file)
#[arg(long)]
fix: bool,
/// Re-run the checks every N seconds, alerting on failure (issue #59)
#[arg(long, value_name = "SECONDS")]
watch: Option<u64>,
},
/// Run the agent command directly with the given arguments (no process management)
Run {
/// Agent name or alias
agent: String,
/// Local model to use for this run (validated against the local runtimes)
#[arg(long, value_name = "MODEL")]
model: Option<String>,
/// Run the agent inside a container (issue #58)
#[arg(long)]
container: bool,
/// Arguments passed through to the agent command
#[arg(trailing_var_arg = true, allow_hyphen_values = true, value_name = "ARGS...")]
args: Vec<std::ffi::OsString>,
},
/// Generate a shell completion script
Completion {
/// Shell to generate completions for
#[arg(value_enum, value_name = "SHELL")]
shell: Shell,
/// Add dynamic completion of the installed agents, aliases and
/// groups (queried from 'am _agents' at Tab time)
#[arg(long)]
installed: bool,
},
/// Update agent-manager itself from the latest release
SelfUpdate {
/// Only check whether a newer version exists
#[arg(long)]
check: bool,
/// Download the new binary to this path instead of replacing the current one
#[arg(long, value_name = "FILE")]
to: Option<PathBuf>,
},
/// Remove agent-manager and everything it created from this machine
///
/// Stops every managed background agent, then deletes the data, state and
/// configuration directories, and finally the executable itself. Combine
/// with --yes for a fully automated one-liner.
SelfUninstall,
/// Export the configuration and installation state (backup)
Export {
/// Output file (default: am-export.json)
#[arg(long, value_name = "FILE", default_value = "am-export.json")]
output: PathBuf,
},
/// Import a previously exported configuration and state
Import {
/// File exported by the export command
file: PathBuf,
},
}
/// Arguments shared by start and restart.
#[derive(Args, Debug, Clone, Default)]
pub struct StartArgs {
/// Agent name, alias, or group:<name> (defaults to the project's default agent)
pub agent: Option<String>,
/// Run detached in the background; output goes to the agent log file
#[arg(short = 'b', long, conflicts_with = "foreground", action = ArgAction::SetTrue)]
pub background: bool,
/// Run in the foreground, attached to this terminal (the default)
#[arg(short = 'f', long, action = ArgAction::SetTrue)]
pub foreground: bool,
/// Extra arguments passed to the agent (repeatable; quoted strings are split on spaces)
#[arg(long, value_name = "ARGS", num_args = 0.., action = ArgAction::Append)]
pub args: Vec<String>,
/// Set an environment variable for the agent (repeatable): --env KEY=VALUE
#[arg(long, value_name = "KEY=VALUE", action = ArgAction::Append)]
pub env: Vec<String>,
/// Send a desktop notification once the agent has started
#[arg(long, action = ArgAction::SetTrue)]
pub notify: bool,
/// Apply an environment profile (env + args, defined in the config)
#[arg(long, value_name = "NAME")]
pub profile: Option<String>,
/// Local model to use for this run (validated against the local runtimes)
#[arg(long, value_name = "MODEL")]
pub model: Option<String>,
/// Start every member of a group simultaneously (issue #57)
#[arg(long, action = ArgAction::SetTrue)]
pub parallel: bool,
/// Run the agent inside a container (issue #58)
#[arg(long, action = ArgAction::SetTrue)]
pub container: bool,
}
/// Arguments of the models command: inventory by default, prune with --prune.
#[derive(Args, Debug, Clone, Default)]
pub struct ModelsArgs {
/// Purge unused models instead of listing them
#[arg(long)]
pub prune: bool,
/// Only list what would be pruned, delete nothing
#[arg(long, conflicts_with = "yes")]
pub dry_run: bool,
/// Answer yes to the deletion prompt
#[arg(long)]
pub yes: bool,
/// Consider a model unused after N days (default: 30)
#[arg(long, value_name = "DAYS")]
pub days: Option<u64>,
}
/// Arguments of the lab command (issue #62).
#[derive(Args, Debug, Clone, Default)]
pub struct LabArgs {
/// Comma-separated agent names (or aliases) to benchmark
#[arg(long, value_name = "AGENTS", required_unless_present = "list")]
pub agents: Option<String>,
/// Task file (a bare name resolves under state_dir/lab/, .yaml appended)
#[arg(long, value_name = "FILE", required_unless_present = "list")]
pub task: Option<PathBuf>,
/// Run every agent concurrently instead of one after another
#[arg(long, action = ArgAction::SetTrue)]
pub parallel: bool,
/// Override the task timeout in seconds
#[arg(long, value_name = "SECS")]
pub timeout: Option<u64>,
/// List the task files of the lab directory
#[arg(long, action = ArgAction::SetTrue)]
pub list: bool,
}
/// Arguments of the plugins command (issue #75).
#[derive(Args, Debug, Clone, Default)]
pub struct PluginsArgs {
/// Run one plugin against a synthetic event and print its response
#[arg(long, value_name = "NAME")]
pub test: Option<String>,
}
/// Remote catalog subcommands (issues #60 #67).
#[derive(Subcommand, Debug, Clone)]
pub enum CatalogCmd {
/// Fetch the official catalog, show the diff and register it as an include
Update,
/// Register an external catalog (same YAML format) by URL
Add {
/// Catalog URL (http, https or file)
#[arg(value_name = "URL")]
url: String,
},
/// List the registered remote catalogs
List,
}
/// Environment profile subcommands (issue #40).
#[derive(Subcommand, Debug, Clone)]
pub enum ProfileCmd {
/// List every defined profile
List,
/// Show one profile in detail (agent, env, args)
Show {
/// Profile name
name: String,
},
}
/// 'am service' subcommands (issue #55).
#[derive(Subcommand, Debug, Clone)]
pub enum ServiceCmd {
/// Install the service unit (systemd / launchd / Task Scheduler)
Install {
/// Agent name
agent: String,
/// Enable autostart (boot/logon)
#[arg(long)]
autostart: bool,
},
/// Remove the service unit
Uninstall {
/// Agent name
agent: String,
},
/// Show whether the service is installed
Status {
/// Agent name
agent: String,
},
}
/// 'am schedule' subcommands (issue #56).
#[derive(Subcommand, Debug, Clone)]
pub enum ScheduleCmd {
/// Add a daily schedule (ex: 'am schedule add update --all')
Add {
/// Command to run, e.g. ["update", "--all"]
#[arg(trailing_var_arg = true, allow_hyphen_values = true, value_name = "CMD...")]
command: Vec<String>,
/// Time of day HH:MM (default 08:00)
#[arg(long, value_name = "HH:MM")]
at: Option<String>,
},
/// List the schedules
List,
/// Remove a schedule by id
Remove {
/// Schedule id ('am schedule list' shows them)
id: String,
},
/// Run a schedule now (invoked by the OS task, issue #56)
Run {
/// Schedule id
id: String,
},
}
#[derive(Subcommand, Debug, Clone)]
pub enum SecretCmd {
/// Store a secret for an agent
Set {
/// Secret name (the environment variable name)
name: String,
/// Agent it belongs to
#[arg(long, value_name = "AGENT")]
agent: String,
/// Secret value (prefer --value over shell history; never logged)
#[arg(long, value_name = "VALUE")]
value: String,
},
/// Remove a secret
Unset {
/// Secret name
name: String,
/// Agent it belongs to
#[arg(long, value_name = "AGENT")]
agent: String,
},
/// List stored secret names (values are never shown)
List,
}
/// Alias management subcommands.
#[derive(Subcommand, Debug, Clone)]
pub enum AliasCmd {
/// Add an alias (am alias add cc claude-code)
Add {
/// Short name of the alias
name: String,
/// Agent name (or another alias) it points to
target: String,
},
/// Remove an alias
Remove {
/// Alias to remove
name: String,
},
/// List every alias
List,
}
/// Configuration management subcommands.
#[derive(Subcommand, Debug, Clone)]
pub enum ConfigCmd {
/// Print the effective (merged) configuration as YAML
Show,
/// Print the location of the configuration file(s)
Path,
/// Open the user configuration file in your editor
Edit,
/// Validate the configuration file(s)
Validate,
/// Add another agent definition file to the user configuration
Add {
/// YAML (or JSON) file defining agents to include
file: PathBuf,
},
/// Set one configuration key without an editor (config set settings.default_shell pwsh)
Set {
/// Dotted key path, e.g. settings.default_shell
key: String,
/// New value (booleans and integers keep their type)
value: String,
},
}