//! Nushell-style help: every command documented as a spec, rendered like //! 'nu help ' (description, search terms, usage, flags, command //! type, parameters, input/output types, examples) with colors. //! //! Also provides the version table ('am version', Nushell-style) and the //! pre-parsing interception of -h/--help flags so that 'am install -h', //! 'am ls --help' and friends all show this help instead of the clap //! generated one. use crate::app::App; use crate::theme::Theme; use anyhow::bail; use serde::Serialize; use std::io::IsTerminal; // --------------------------------------------------------------------------- // Command specifications // --------------------------------------------------------------------------- pub struct HelpFlag { pub short: &'static str, pub long: &'static str, pub value: &'static str, pub desc: &'static str, } pub struct HelpParam { pub name: &'static str, pub typ: &'static str, pub desc: &'static str, } pub struct HelpExample { pub desc: &'static str, pub code: &'static str, } pub struct HelpSpec { pub name: &'static str, pub category: &'static str, pub usage: &'static str, pub about: &'static str, pub search_terms: &'static [&'static str], pub flags: &'static [HelpFlag], pub subcommands: &'static [(&'static str, &'static str)], pub parameters: &'static [HelpParam], pub io: Option<(&'static str, &'static str)>, pub examples: &'static [HelpExample], } const FLAG_HELP: HelpFlag = HelpFlag { short: "-h", long: "--help", value: "", desc: "Display the help message for this command", }; /// Flags shared by every am command. const GLOBAL_FLAGS: &[HelpFlag] = &[ HelpFlag { short: "-c", long: "--config", value: "FILE", desc: "Use an alternate configuration file", }, HelpFlag { short: "-v", long: "--verbose", value: "", desc: "Verbose output: show every command executed and its details", }, HelpFlag { short: "-q", long: "--quiet", value: "", desc: "Quiet mode: only errors are printed", }, HelpFlag { short: "-y", long: "--yes", value: "", desc: "Answer yes to every confirmation prompt", }, HelpFlag { short: "", long: "--dry-run", value: "", desc: "Simulate the action without changing anything", }, HelpFlag { short: "", long: "--json", value: "", desc: "Emit machine-readable JSON on stdout (for scripts)", }, HelpFlag { short: "", long: "--no-color", value: "", desc: "Disable ANSI colors", }, HelpFlag { short: "", long: "--theme", value: "THEME", desc: "Color theme for the output (see 'am help theme' for the list)", }, ]; const START_FLAGS: &[HelpFlag] = &[ HelpFlag { short: "-b", long: "--background", value: "", desc: "Run detached in the background; output goes to the agent log file", }, HelpFlag { short: "-f", long: "--foreground", value: "", desc: "Run in the foreground, attached to this terminal (the default)", }, HelpFlag { short: "", long: "--args", value: "ARGS", desc: "Extra arguments passed to the agent (repeatable; quoted strings are split on spaces)", }, HelpFlag { short: "", long: "--env", value: "KEY=VALUE", desc: "Set an environment variable for the agent (repeatable)", }, HelpFlag { short: "", long: "--notify", value: "", desc: "Send a desktop notification once the agent has started", }, ]; pub static HELP_SPECS: &[HelpSpec] = &[ HelpSpec { name: "list", category: "Commands", usage: "list {flags}", about: "List installed agents, or every catalog entry with --all.", search_terms: &["catalog", "agents", "installed"], flags: &[ HelpFlag { short: "", long: "--all", value: "", desc: "Show every agent known to the catalog, not only the installed ones" }, HelpFlag { short: "", long: "--category", value: "CATEGORY", desc: "Only show agents of this category" }, ], subcommands: &[], parameters: &[], io: None, examples: &[ HelpExample { desc: "List the installed agents.", code: "list" }, HelpExample { desc: "Show every agent in the catalog.", code: "list --all" }, HelpExample { desc: "Show only one category.", code: "list --category coding-agent" }, ], }, HelpSpec { name: "status", category: "Commands", usage: "status {flags} [agent]", about: "Show the state of one agent, or of every installed agent.", search_terms: &["state", "running", "pid"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias (omit to show all installed agents)" }, ], io: None, examples: &[ HelpExample { desc: "Show the state of every installed agent.", code: "status" }, HelpExample { desc: "Show one agent.", code: "status claude-code" }, ], }, HelpSpec { name: "sessions", category: "Commands", usage: "sessions {flags} [agent]", about: "List the sessions of every agent and of the interactive shell.", search_terms: &["session", "history", "log"], flags: &[ HelpFlag { short: "", long: "--project", value: "PROJECT", desc: "Only sessions of this project" }, HelpFlag { short: "", long: "--status", value: "STATUS", desc: "Only sessions with this status (running, stopped, failed, interrupted)" }, HelpFlag { short: "", long: "--show", value: "ID", desc: "Show one session in detail (summary + log excerpt)" }, ], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Only sessions of this agent" }, ], io: None, examples: &[ HelpExample { desc: "List recent sessions.", code: "sessions" }, HelpExample { desc: "Show one session with its log excerpt.", code: "sessions --show 20260815_143926_a1b2c3" }, ], }, HelpSpec { name: "stats", category: "Commands", usage: "stats {flags} [agent]", about: "Show usage statistics computed from the event journal.", search_terms: &["usage", "metrics", "duration"], flags: &[ HelpFlag { short: "", long: "--period", value: "PERIOD", desc: "Only events of the last period (7d, 30d, 90d, all)" }, ], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias (omit for the global view)" }, ], io: None, examples: &[ HelpExample { desc: "Global usage statistics.", code: "stats" }, HelpExample { desc: "One agent over the last month.", code: "stats claude-code --period 30d" }, ], }, HelpSpec { name: "top", category: "Commands", usage: "top {flags}", about: "Show the most used agents (top 10).", search_terms: &["ranking", "most", "usage"], flags: &[ HelpFlag { short: "", long: "--period", value: "PERIOD", desc: "Only events of the last period (7d, 30d, 90d, all)" }, ], subcommands: &[], parameters: &[], io: None, examples: &[ HelpExample { desc: "The most used agents.", code: "top" }, ], }, HelpSpec { name: "report", category: "Commands", usage: "report {flags}", about: "Write a markdown activity report.", search_terms: &["digest", "weekly", "monthly"], flags: &[ HelpFlag { short: "", long: "--last-week", value: "", desc: "Cover the last 7 days" }, HelpFlag { short: "", long: "--last-month", value: "", desc: "Cover the last 30 days" }, HelpFlag { short: "", long: "--output", value: "FILE", desc: "Output file (default: report-YYYYMMDD.md in the state directory)" }, ], subcommands: &[], parameters: &[], io: None, examples: &[ HelpExample { desc: "Weekly digest.", code: "report --last-week" }, HelpExample { desc: "Monthly digest to a file.", code: "report --last-month --output ~/rapport-am.md" }, ], }, HelpSpec { name: "projects", category: "Commands", usage: "projects {flags} [name]", about: "Show which agent works on which project (derived from the journal).", search_terms: &["workspace", "repository", "activity"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "name", typ: "string", desc: "Project name (omit to list every project)" }, ], io: None, examples: &[ HelpExample { desc: "Every project with activity.", code: "projects" }, HelpExample { desc: "One project in detail.", code: "projects api" }, ], }, HelpSpec { name: "timeline", category: "Commands", usage: "timeline {flags}", about: "One chronological view of every activity (journal + REPL history).", search_terms: &["chronology", "activity", "merged"], flags: &[ HelpFlag { short: "", long: "--project", value: "PROJECT", desc: "Only activity of this project" }, HelpFlag { short: "", long: "--since", value: "DATE", desc: "Only activity at or after this date" }, HelpFlag { short: "", long: "--limit", value: "N", desc: "Maximum number of entries (default: 50)" }, ], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Only activity of this agent" }, ], io: None, examples: &[ HelpExample { desc: "The last 50 activities.", code: "timeline" }, HelpExample { desc: "One project since a date.", code: "timeline --project api --since 2026-08-01" }, ], }, HelpSpec { name: "log", category: "Commands", usage: "log {flags} [agent]", about: "Read the event journal: everything am did.", search_terms: &["journal", "events", "history"], flags: &[ HelpFlag { short: "", long: "--kind", value: "KINDS", desc: "Only these event kinds (comma separated)" }, HelpFlag { short: "", long: "--since", value: "DATE", desc: "Only events at or after this date (YYYY-MM-DD or RFC 3339)" }, HelpFlag { short: "", long: "--limit", value: "N", desc: "Maximum number of events (default: 50)" }, ], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Only events of this agent" }, ], io: None, examples: &[ HelpExample { desc: "The last 50 events.", code: "log" }, HelpExample { desc: "Starts of one agent since a date.", code: "log claude-code --kind start --since 2026-08-01" }, ], }, HelpSpec { name: "logs", category: "Commands", usage: "logs {flags} ", about: "Tail the log file of an agent.", search_terms: &["tail", "follow", "output"], flags: &[ HelpFlag { short: "", long: "--lines", value: "N", desc: "Number of lines to show (default: 20)" }, HelpFlag { short: "", long: "--follow", value: "", desc: "Follow the log as it grows" }, ], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" }, ], io: None, examples: &[ HelpExample { desc: "The last 20 lines.", code: "logs claude-code" }, HelpExample { desc: "A live view.", code: "logs claude-code --follow" }, ], }, HelpSpec { name: "open", category: "Commands", usage: "open {flags} ", about: "Open an agent's installation directory in the file manager.", search_terms: &["explorer", "folder", "directory"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" }, ], io: None, examples: &[ HelpExample { desc: "Open the install directory.", code: "open claude-code" }, ], }, HelpSpec { name: "watch", category: "Commands", usage: "watch {flags} ", about: "Supervise an agent and optionally restart it on crash.", search_terms: &["monitor", "supervise", "restart"], flags: &[ HelpFlag { short: "", long: "--restart", value: "", desc: "Restart the agent automatically when it dies (with backoff)" }, HelpFlag { short: "", long: "--notify", value: "", desc: "Desktop notification on every restart" }, HelpFlag { short: "", long: "--interval", value: "SECS", desc: "Check interval in seconds (default: 5)" }, ], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" }, ], io: None, examples: &[ HelpExample { desc: "Supervise with auto-restart.", code: "watch claude-code --restart --notify" }, ], }, HelpSpec { name: "alias", category: "Commands", usage: "alias {flags} [name] [target]", about: "Manage command aliases (cc -> claude-code) without editing the YAML.", search_terms: &["shortcut", "rename"], flags: &[], subcommands: &[ ("add", "Add an alias pointing to an agent"), ("remove", "Remove an alias"), ], parameters: &[], io: None, examples: &[ HelpExample { desc: "Shortcut for claude-code.", code: "alias add cc claude-code" }, HelpExample { desc: "List every alias.", code: "alias list" }, ], }, HelpSpec { name: "secret", category: "Commands", usage: "secret {flags} ", about: "Manage secrets in the OS keyring (values never shown or logged).", search_terms: &["keyring", "token", "password"], flags: &[], subcommands: &[ ("set", "Store a secret for an agent"), ("unset", "Remove a secret"), ], parameters: &[], io: None, examples: &[ HelpExample { desc: "Store a token.", code: "secret set OPENAI_KEY --agent claude-code --value sk-..." }, HelpExample { desc: "Use it at launch.", code: "start claude-code --env OPENAI_KEY=@secret" }, ], }, HelpSpec { name: "history", category: "Commands", usage: "history {flags}", about: "Search the command history (am and shell commands).", search_terms: &["past", "commands", "rerun"], flags: &[ HelpFlag { short: "", long: "--kind", value: "KIND", desc: "Only commands of this kind (am or shell)" }, HelpFlag { short: "", long: "--cwd", value: "DIR", desc: "Only commands run in this directory" }, HelpFlag { short: "", long: "--search", value: "TEXT", desc: "Full-text search in the command line" }, HelpFlag { short: "", long: "--failed", value: "", desc: "Only failed commands" }, HelpFlag { short: "", long: "--session", value: "ID", desc: "Only commands of this session" }, ], subcommands: &[], parameters: &[], io: None, examples: &[ HelpExample { desc: "The last 100 commands.", code: "history" }, HelpExample { desc: "Find past installs.", code: "history --search install" }, HelpExample { desc: "Failed commands only.", code: "history --failed" }, ], }, HelpSpec { name: "search", category: "Commands", usage: "search {flags} ", about: "Search the catalog by keyword (name, description, category, tags); --tag filters on personal tags.", search_terms: &["find", "catalog", "keyword"], flags: &[ HelpFlag { short: "", long: "--category", value: "CATEGORY", desc: "Restrict to this category" }, HelpFlag { short: "", long: "--tag", value: "TAG", desc: "Only agents carrying this personal tag" }, ], subcommands: &[], parameters: &[ HelpParam { name: "keyword", typ: "string", desc: "Keyword to search for (case-insensitive substring)" }, ], io: None, examples: &[ HelpExample { desc: "Find agents related to rust.", code: "search rust" }, HelpExample { desc: "Search within one category.", code: "search coding --category coding-agent" }, HelpExample { desc: "Find agents tagged python.", code: "search --tag python" }, ], }, HelpSpec { name: "info", category: "Commands", usage: "info {flags} ", about: "Show detailed information about one agent.", search_terms: &["details", "show", "agent"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" }, ], io: None, examples: &[ HelpExample { desc: "Show details about an agent.", code: "info claude-code" }, HelpExample { desc: "Use an alias.", code: "info cc" }, ], }, HelpSpec { name: "init", category: "Commands", usage: "init {flags}", about: "Generate a local agent-manager.yaml for the current directory, based on the detected stack.", search_terms: &["project", "setup", "scaffold"], flags: &[ HelpFlag { short: "", long: "--force", value: "", desc: "Overwrite an existing agent-manager.yaml" }, ], subcommands: &[], parameters: &[], io: None, examples: &[ HelpExample { desc: "Generate a project config.", code: "init" }, HelpExample { desc: "Regenerate over an existing one.", code: "init --force" }, ], }, HelpSpec { name: "install", category: "Commands", usage: "install {flags} ", about: "Install an agent and its dependencies.", search_terms: &["add", "setup"], flags: &[ HelpFlag { short: "", long: "--method", value: "METHOD", desc: "Select the install method (index, or type: npm, pip, uv, cargo, go, bun, curl, binary, git)" }, HelpFlag { short: "", long: "--force", value: "", desc: "Reinstall even if already installed" }, ], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" }, ], io: None, examples: &[ HelpExample { desc: "Install an agent with its dependencies.", code: "install claude-code" }, HelpExample { desc: "Pick a specific install method.", code: "install smelt --method uv" }, HelpExample { desc: "Reinstall over an existing install.", code: "install claude-code --force" }, ], }, HelpSpec { name: "uninstall", category: "Commands", usage: "uninstall {flags} ", about: "Uninstall an agent (managed or external) and clean its files.", search_terms: &["remove", "delete"], flags: &[ HelpFlag { short: "", long: "--purge", value: "", desc: "Also remove logs and the agent entry from the user config file" }, ], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" }, ], io: None, examples: &[ HelpExample { desc: "Remove an agent.", code: "uninstall claude-code" }, HelpExample { desc: "Remove everything, including logs and the config entry.", code: "uninstall claude-code --purge" }, ], }, HelpSpec { name: "update", category: "Commands", usage: "update {flags} [agent]", about: "Update an installed agent to the latest available version.", search_terms: &["upgrade"], flags: &[ HelpFlag { short: "", long: "--all", value: "", desc: "Update every installed managed agent" }, ], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias (required unless --all)" }, ], io: None, examples: &[ HelpExample { desc: "Update one agent.", code: "update claude-code" }, HelpExample { desc: "Update every managed agent.", code: "update --all" }, ], }, HelpSpec { name: "start", category: "Commands", usage: "start {flags} ", about: "Start an agent (foreground by default, or detached with --background). --profile applies an environment profile (env + args).", search_terms: &["launch", "begin"], flags: START_FLAGS, subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name, alias, or group:" }, ], io: None, examples: &[ HelpExample { desc: "Start an agent in the background.", code: "start claude-code --background" }, HelpExample { desc: "Start with extra arguments.", code: "start claude-code --args \"--model sonnet\"" }, HelpExample { desc: "Start every agent of a group.", code: "start group:dev --background" }, HelpExample { desc: "Start with an environment profile.", code: "start claude-code --profile prod" }, ], }, HelpSpec { name: "stop", category: "Commands", usage: "stop {flags} ", about: "Stop a background agent (SIGTERM, then SIGKILL after the timeout).", search_terms: &["kill", "terminate"], flags: &[ HelpFlag { short: "", long: "--force", value: "", desc: "Kill immediately instead of terminating gracefully" }, HelpFlag { short: "", long: "--timeout", value: "SECS", desc: "Grace period in seconds before killing (default: from config, 5s)" }, ], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name, alias, or group:" }, ], io: None, examples: &[ HelpExample { desc: "Stop an agent gracefully.", code: "stop claude-code" }, HelpExample { desc: "Kill immediately.", code: "stop claude-code --force" }, ], }, HelpSpec { name: "restart", category: "Commands", usage: "restart {flags} ", about: "Restart an agent: stop, then start with the same options.", search_terms: &["reboot", "reload"], flags: &[ HelpFlag { short: "-b", long: "--background", value: "", desc: "Restart detached in the background; output goes to the agent log file" }, HelpFlag { short: "-f", long: "--foreground", value: "", desc: "Restart in the foreground, attached to this terminal (the default)" }, HelpFlag { short: "", long: "--args", value: "ARGS", desc: "Extra arguments passed to the agent (repeatable; quoted strings are split on spaces)" }, HelpFlag { short: "", long: "--env", value: "KEY=VALUE", desc: "Set an environment variable for the agent (repeatable)" }, HelpFlag { short: "", long: "--notify", value: "", desc: "Send a desktop notification once the agent has started" }, HelpFlag { short: "", long: "--force", value: "", desc: "Kill immediately instead of terminating gracefully" }, HelpFlag { short: "", long: "--timeout", value: "SECS", desc: "Grace period in seconds before killing (default: from config, 5s)" }, ], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name, alias, or group:" }, ], io: None, examples: &[ HelpExample { desc: "Restart an agent in the background.", code: "restart claude-code --background" }, HelpExample { desc: "Restart with extra arguments.", code: "restart claude-code --args \"--model sonnet\"" }, ], }, HelpSpec { name: "run", category: "Commands", usage: "run {flags} ...(args)", about: "Run the agent command directly with the given arguments (no process management).", search_terms: &["exec", "pass-through"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" }, HelpParam { name: "args", typ: "any", desc: "Arguments passed through to the agent command" }, ], io: None, examples: &[ HelpExample { desc: "Run an agent command directly.", code: "run claude-code" }, HelpExample { desc: "Pass --help through to the agent (after --).", code: "run claude-code -- --help" }, ], }, HelpSpec { name: "doctor", category: "Commands", usage: "doctor {flags}", about: "Check the environment: tools, config validity, paths, permissions.", search_terms: &["check", "environment", "repair"], flags: &[ HelpFlag { short: "", long: "--fix", value: "", desc: "Attempt to repair problems (create directories, fix the state file)" }, ], subcommands: &[], parameters: &[], io: None, examples: &[ HelpExample { desc: "Check the environment.", code: "doctor" }, HelpExample { desc: "Check and repair.", code: "doctor --fix" }, ], }, HelpSpec { name: "config", category: "Commands", usage: "config {flags} ", about: "Manage the configuration file.", search_terms: &["settings", "yaml", "configure"], flags: &[], subcommands: &[ ("show", "Print the effective (merged) configuration as YAML"), ("path", "Print the location of the configuration file(s)"), ("edit", "Open the user configuration file in your editor"), ("validate", "Validate the configuration file(s)"), ("add ", "Add another agent definition file to the user configuration"), ], parameters: &[], io: None, examples: &[ HelpExample { desc: "Show the effective configuration.", code: "config show" }, HelpExample { desc: "Locate the config file.", code: "config path" }, HelpExample { desc: "Validate it.", code: "config validate" }, ], }, HelpSpec { name: "completion", category: "Commands", usage: "completion {flags} ", about: "Generate a shell completion script (bash, zsh, fish, powershell, elvish). With --installed, Tab also completes the installed agents, aliases and groups.", search_terms: &["autocomplete", "shell"], flags: &[ HelpFlag { short: "", long: "--installed", value: "", desc: "Add dynamic completion of installed agents, aliases and groups" }, ], subcommands: &[], parameters: &[ HelpParam { name: "shell", typ: "oneof", desc: "Shell to generate completions for" }, ], io: None, examples: &[ HelpExample { desc: "Generate PowerShell completions.", code: "completion powershell" }, HelpExample { desc: "Generate bash completions with the installed agents.", code: "completion bash --installed" }, ], }, HelpSpec { name: "self-update", category: "Commands", usage: "self-update {flags}", about: "Update agent-manager itself from the latest release (GitHub ou Gitea, selon settings.self_update_*).", search_terms: &["upgrade", "release", "gitea"], flags: &[ HelpFlag { short: "", long: "--check", value: "", desc: "Only check whether a newer version exists" }, HelpFlag { short: "", long: "--to", value: "FILE", desc: "Download the new binary to this path instead of replacing the current one" }, ], subcommands: &[], parameters: &[], io: None, examples: &[ HelpExample { desc: "Check for a newer version.", code: "self-update --check" }, HelpExample { desc: "Download without replacing the current binary.", code: "self-update --to ./am-new.exe" }, ], }, HelpSpec { name: "self-uninstall", category: "Commands", usage: "self-uninstall {flags}", about: "Remove agent-manager and everything it created from this machine.", search_terms: &["remove", "cleanup"], flags: &[], subcommands: &[], parameters: &[], io: None, examples: &[ HelpExample { desc: "Remove agent-manager completely.", code: "self-uninstall --yes" }, ], }, HelpSpec { name: "export", category: "Commands", usage: "export {flags}", about: "Export the configuration and installation state (backup).", search_terms: &["backup", "save"], flags: &[ HelpFlag { short: "", long: "--output", value: "FILE", desc: "Output file (default: am-export.json)" }, ], subcommands: &[], parameters: &[], io: None, examples: &[ HelpExample { desc: "Back up to the default file.", code: "export" }, HelpExample { desc: "Choose the output file.", code: "export --output backup.json" }, ], }, HelpSpec { name: "import", category: "Commands", usage: "import {flags} ", about: "Import a previously exported configuration and state.", search_terms: &["restore", "backup"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "file", typ: "path", desc: "File exported by the export command" }, ], io: None, examples: &[ HelpExample { desc: "Restore a backup.", code: "import am-export.json" }, ], }, HelpSpec { name: "dashboard", category: "Commands", usage: "dashboard {flags}", about: "Live TUI dashboard: the whole fleet in one screen — overview, activity, stats, sessions and projects, refreshed every second.", search_terms: &["tui", "monitor", "overview", "cockpit"], flags: &[], subcommands: &[], parameters: &[], io: None, examples: &[ HelpExample { desc: "Open the dashboard (Tab or arrows switch tabs, j/k scroll, q quits).", code: "dashboard" }, ], }, HelpSpec { name: "favorite", category: "Commands", usage: "favorite {flags} ", about: "Mark an agent as a favorite: a star appears in 'am list', 'am status' and 'am info'.", search_terms: &["star", "bookmark", "prefer"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" }, ], io: None, examples: &[ HelpExample { desc: "Mark claude-code as a favorite.", code: "favorite claude-code" }, ], }, HelpSpec { name: "unfavorite", category: "Commands", usage: "unfavorite {flags} ", about: "Remove the favorite star from an agent.", search_terms: &["star", "bookmark"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" }, ], io: None, examples: &[ HelpExample { desc: "Remove the star from claude-code.", code: "unfavorite claude-code" }, ], }, HelpSpec { name: "note", category: "Commands", usage: "note {flags} [text...]", about: "Attach a free-form note to an agent, shown in 'am info'. Without text, prints the current note.", search_terms: &["comment", "remark", "memo"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" }, HelpParam { name: "text", typ: "string", desc: "Note text (several words are joined); empty prints the note" }, ], io: None, examples: &[ HelpExample { desc: "Set a note.", code: "note claude-code use the sonnet model for this repo" }, HelpExample { desc: "Show the note.", code: "note claude-code" }, ], }, HelpSpec { name: "tag", category: "Commands", usage: "tag {flags} ...", about: "Add personal tags to an agent; they are searchable with 'am search --tag' and listed in 'am info'.", search_terms: &["label", "keyword", "filter"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" }, HelpParam { name: "tag", typ: "string", desc: "One or more tags to add" }, ], io: None, examples: &[ HelpExample { desc: "Tag claude-code for Python work.", code: "tag claude-code python frontend" }, HelpExample { desc: "Find everything tagged python.", code: "search --tag python" }, ], }, HelpSpec { name: "untag", category: "Commands", usage: "untag {flags} ", about: "Remove a personal tag from an agent.", search_terms: &["label", "remove"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias" }, HelpParam { name: "tag", typ: "string", desc: "Tag to remove" }, ], io: None, examples: &[ HelpExample { desc: "Remove the python tag from claude-code.", code: "untag claude-code python" }, ], }, HelpSpec { name: "tags", category: "Commands", usage: "tags {flags} [agent]", about: "List personal tags with the agents carrying them; with an agent, list only its tags.", search_terms: &["labels", "tag list"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "agent", typ: "string", desc: "Agent name or alias (optional)" }, ], io: None, examples: &[ HelpExample { desc: "List every personal tag.", code: "tags" }, HelpExample { desc: "List the tags of one agent.", code: "tags claude-code" }, ], }, HelpSpec { name: "profile", category: "Commands", usage: "profile {flags} [name]", about: "Environment profiles: a named set of agent, env and args defined in the config, applied with 'am start --profile dev'.", search_terms: &["environment", "context", "dev", "prod"], flags: &[], subcommands: &[ ("list", "List every defined profile"), ("show", "Show one profile in detail"), ], parameters: &[ HelpParam { name: "name", typ: "string", desc: "Profile to show" }, ], io: None, examples: &[ HelpExample { desc: "List the profiles.", code: "profile list" }, HelpExample { desc: "Show the dev profile.", code: "profile show dev" }, HelpExample { desc: "Start an agent with the prod profile.", code: "start claude-code --profile prod" }, ], }, HelpSpec { name: "man", category: "Commands", usage: "man {flags} [command]", about: "Generate roff man pages from the command definitions: one page for the whole CLI, or one page per command with --output.", search_terms: &["documentation", "roff", "unix"], flags: &[ HelpFlag { short: "", long: "--output", value: "DIR", desc: "Write am.1 plus one am-.1 per command into this directory" }, ], subcommands: &[], parameters: &[ HelpParam { name: "command", typ: "string", desc: "Command to document; omit for the whole CLI" }, ], io: None, examples: &[ HelpExample { desc: "Print the man page of the whole CLI.", code: "man" }, HelpExample { desc: "Print one command page.", code: "man install" }, HelpExample { desc: "Write every page into man/ (for a package).", code: "man --output man/" }, ], }, HelpSpec { name: "help", category: "Commands", usage: "help {flags} [name]", about: "Display help: the overview, a particular command or agent, the command list, or a search through it.", search_terms: &["man"], flags: &[ HelpFlag { short: "", long: "--find", value: "TEXT", desc: "Search through all help commands table" }, ], subcommands: &[], parameters: &[ HelpParam { name: "name", typ: "string", desc: "Command name, agent name, or alias; 'commands' lists every command" }, ], io: None, examples: &[ HelpExample { desc: "Show the overview.", code: "help" }, HelpExample { desc: "Show one command.", code: "help install" }, HelpExample { desc: "List every command.", code: "help commands" }, HelpExample { desc: "Search the help.", code: "help --find install" }, ], }, HelpSpec { name: "version", category: "Commands", usage: "version {flags}", about: "Display version and build information.", search_terms: &["about"], flags: &[], subcommands: &[], parameters: &[], io: None, examples: &[ HelpExample { desc: "Show version and build information.", code: "version" }, ], }, HelpSpec { name: "theme", category: "Shell", usage: "theme {flags} [name]", about: "Show the color themes, or switch the active one inside the shell.", search_terms: &["color", "colors", "palette"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "name", typ: "string", desc: "Theme to switch to; omit to list every available theme" }, ], io: None, examples: &[ HelpExample { desc: "List the available themes.", code: "theme" }, HelpExample { desc: "Switch to the ocean theme.", code: "theme ocean" }, ], }, HelpSpec { name: "ls", category: "Shell", usage: "ls {flags} [path]", about: "List the filenames, sizes, and modification times of items in a directory.", search_terms: &["dir", "files", "directory"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "path", typ: "path", desc: "The directory or glob to list (default: current directory)" }, ], io: Some(("nothing", "table")), examples: &[ HelpExample { desc: "List the current directory.", code: "ls" }, HelpExample { desc: "List a subdirectory.", code: "ls src" }, HelpExample { desc: "List Rust files.", code: "ls *.rs" }, HelpExample { desc: "List files larger than 1 MB.", code: "ls | where size > 1mb" }, HelpExample { desc: "Select one column.", code: "ls | get name" }, ], }, HelpSpec { name: "dir", category: "Shell", usage: "dir {flags} [path]", about: "List the filenames, sizes, and modification times of items in a directory (Windows alias of ls).", search_terms: &["ls", "files", "directory"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "path", typ: "path", desc: "The directory or glob to list (default: current directory)" }, ], io: Some(("nothing", "table")), examples: &[ HelpExample { desc: "List the current directory.", code: "dir" }, HelpExample { desc: "List a subdirectory.", code: "dir src" }, ], }, HelpSpec { name: "ps", category: "Shell", usage: "ps {flags}", about: "List the processes on the system.", search_terms: &["processes", "task"], flags: &[], subcommands: &[], parameters: &[], io: Some(("nothing", "table")), examples: &[ HelpExample { desc: "List all processes.", code: "ps" }, HelpExample { desc: "Filter by name.", code: "ps | where name =~ am" }, ], }, HelpSpec { name: "where", category: "Shell", usage: "where {flags} ", about: "Filter the rows of the last table (ls or ps) with a predicate.", search_terms: &["filter", "query"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "column", typ: "string", desc: "Column to filter on" }, HelpParam { name: "operator", typ: "string", desc: "One of > < >= <= == != =~" }, HelpParam { name: "value", typ: "string", desc: "Value to compare (supports 1kb/1mb/1gb sizes)" }, ], io: Some(("table", "table")), examples: &[ HelpExample { desc: "Keep files larger than 1 MB.", code: "ls | where size > 1mb" }, HelpExample { desc: "Keep processes whose name contains 'am'.", code: "ps | where name =~ am" }, ], }, HelpSpec { name: "get", category: "Shell", usage: "get {flags} ", about: "Select one column of the last table.", search_terms: &["select", "column"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "column", typ: "string", desc: "Column to select" }, ], io: Some(("table", "table")), examples: &[ HelpExample { desc: "Select the name column.", code: "ls | get name" }, ], }, HelpSpec { name: "cd", category: "Shell", usage: "cd {flags} [dir]", about: "Change the session directory (Tab completes folders).", search_terms: &["chdir", "directory"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "dir", typ: "path", desc: "Target directory (default: home)" }, ], io: None, examples: &[ HelpExample { desc: "Go to a directory.", code: "cd src" }, HelpExample { desc: "Go home.", code: "cd" }, HelpExample { desc: "Go home explicitly.", code: "cd ~" }, ], }, HelpSpec { name: "shell", category: "Shell", usage: "shell {flags} [name]", about: "Show or switch the system shell used by the gateway.", search_terms: &["pwsh", "bash", "gateway"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "name", typ: "string", desc: "Shell to switch to (pwsh, powershell, cmd, bash, zsh, fish, sh, nu, elvish); omit to show the status" }, ], io: None, examples: &[ HelpExample { desc: "Show the active shell.", code: "shell" }, HelpExample { desc: "Switch to bash.", code: "shell bash" }, ], }, HelpSpec { name: "exit", category: "Shell", usage: "exit {flags}", about: "Quit the interactive shell.", search_terms: &["quit", "q"], flags: &[], subcommands: &[], parameters: &[], io: None, examples: &[ HelpExample { desc: "Leave the shell.", code: "exit" }, ], }, ]; /// Find a help spec by command name (case-insensitive). pub fn find_spec(name: &str) -> Option<&'static HelpSpec> { let lower = name.to_lowercase(); HELP_SPECS.iter().find(|s| s.name.to_lowercase() == lower) } /// All specs matching a --find keyword (name, about, usage, search terms). fn matching_specs(text: &str) -> Vec<&'static HelpSpec> { let needle = text.to_lowercase(); HELP_SPECS .iter() .filter(|s| { s.name.to_lowercase().contains(&needle) || s.about.to_lowercase().contains(&needle) || s.usage.to_lowercase().contains(&needle) || s.search_terms .iter() .any(|t| t.to_lowercase().contains(&needle)) }) .collect() } // --------------------------------------------------------------------------- // Rendering helpers // --------------------------------------------------------------------------- fn header(s: &str, theme: &Theme, color: bool) -> String { if color { theme.hdr(s) } else { s.to_string() } } fn prompt(s: &str, theme: &Theme, color: bool) -> String { if color { theme.acc(s) } else { s.to_string() } } fn name_style(s: &str, theme: &Theme, color: bool) -> String { if color { theme.acc(s) } else { s.to_string() } } fn type_style(s: &str, theme: &Theme, color: bool) -> String { if color { theme.inf(s) } else { s.to_string() } } fn dim(s: &str, theme: &Theme, color: bool) -> String { if color { theme.dimmed(s) } else { s.to_string() } } fn flag_line(f: &HelpFlag, theme: &Theme, color: bool) -> String { let name = match (f.short.is_empty(), f.long.is_empty()) { (true, false) => f.long.to_string(), (false, true) => f.short.to_string(), _ => format!("{}, {}", f.short, f.long), }; let name = name_style(&name, theme, color); let value = if f.value.is_empty() { String::new() } else { format!(" {}", type_style(&format!("<{}>", f.value), theme, color)) }; format!(" {name}{value}: {}\n", f.desc) } /// The small '# | input | output' table shown in command help. fn io_table(input: &str, output: &str, theme: &Theme, color: bool) -> String { let headers = ["#", "input", "output"]; let cells = ["0", input, output]; let widths: Vec = headers .iter() .zip(cells.iter()) .map(|(h, c)| h.chars().count().max(c.chars().count())) .collect(); let border = |left: char, mid: char, right: char| -> String { let mut line = String::new(); line.push(left); for (i, w) in widths.iter().enumerate() { if i > 0 { line.push(mid); } line.push_str(&"─".repeat(w + 2)); } line.push(right); if color { theme.frame(&line) } else { line } }; let bar = |s: &str| -> String { if color { theme.frame(s) } else { s.to_string() } }; let row = |cells: &[&str], bold: bool| -> String { let mut line = bar("│"); for (i, c) in cells.iter().enumerate() { line.push(' '); let text = if bold { if color { theme.hdr(c) } else { (*c).to_string() } } else { (*c).to_string() }; line.push_str(&text); line.push_str(&" ".repeat(widths[i] - c.chars().count())); line.push(' '); line.push_str(&bar("│")); } line }; format!( "{}\n{}\n{}\n{}\n{}", border('╭', '┬', '╮'), row(&headers, true), border('├', '┼', '┤'), row(&cells, false), border('╰', '┴', '╯') ) } fn truncate(s: &str, max: usize) -> String { if s.chars().count() <= max { s.to_string() } else { let mut cut: String = s.chars().take(max.saturating_sub(1)).collect(); cut.push('…'); cut } } // --------------------------------------------------------------------------- // Command help // --------------------------------------------------------------------------- /// Render one command's help, Nushell-style. pub fn render_command(spec: &HelpSpec, theme: &Theme, color: bool) -> String { let mut out = String::new(); // Description. for line in spec.about.lines() { out.push_str(line); out.push('\n'); } out.push('\n'); // Search terms. if !spec.search_terms.is_empty() { out.push_str(&dim( &format!("Search terms: {}", spec.search_terms.join(", ")), theme, color, )); out.push('\n'); out.push('\n'); } // Usage. out.push_str(&format!("{}:\n", header("Usage", theme, color))); out.push_str(&format!(" {} {}\n", prompt(">", theme, color), spec.usage)); out.push('\n'); // Flags. out.push_str(&format!("{}:\n", header("Flags", theme, color))); out.push_str(&flag_line(&FLAG_HELP, theme, color)); for f in spec.flags { out.push_str(&flag_line(f, theme, color)); } out.push_str(&format!("{}:\n", header("Global Flags", theme, color))); for f in GLOBAL_FLAGS { out.push_str(&flag_line(f, theme, color)); } out.push('\n'); // Command type. out.push_str(&format!("{}:\n", header("Command Type", theme, color))); out.push_str(&format!( " {}\n", if color { theme.acc("built-in") } else { "built-in".to_string() } )); out.push('\n'); // Subcommands. if !spec.subcommands.is_empty() { out.push_str(&format!("{}:\n", header("Subcommands", theme, color))); for (sub, desc) in spec.subcommands { out.push_str(&format!(" {}: {desc}\n", name_style(sub, theme, color))); } out.push('\n'); } // Parameters. if !spec.parameters.is_empty() { out.push_str(&format!("{}:\n", header("Parameters", theme, color))); for p in spec.parameters { let name = name_style(p.name, theme, color); let typ = type_style(&format!("<{}>", p.typ), theme, color); out.push_str(&format!(" {name} {typ}: {}\n", p.desc)); } out.push('\n'); } // Input/output types. if let Some((input, output)) = spec.io { out.push_str(&format!( "{}:\n", header("Input/output types", theme, color) )); for line in io_table(input, output, theme, color).lines() { out.push_str(&format!(" {line}\n")); } out.push('\n'); } // Examples. if !spec.examples.is_empty() { out.push_str(&format!("{}:\n", header("Examples", theme, color))); for (i, ex) in spec.examples.iter().enumerate() { if i > 0 { out.push('\n'); } out.push_str(&format!(" {}\n", ex.desc)); out.push_str(&format!(" {} {}\n", prompt(">", theme, color), ex.code)); } } out } /// Print the help of one command; agent names and aliases fall back to /// the 'info' command (Nushell's "help for commands, aliases and /// modules" behavior). pub fn print_command(app: &App, name: &str) -> Result { if let Some(spec) = find_spec(name) { print!("{}", render_command(spec, app.theme(), app.color())); return Ok(0); } if app.catalog.resolve(name).is_some() { return crate::commands::info_cmd::run(app, name); } bail!( "unknown help topic '{name}' — try 'am help commands' or 'am help --find '" ) } // --------------------------------------------------------------------------- // General help (the 'help' overview, like 'nu help') // --------------------------------------------------------------------------- /// Render the general help overview. pub fn render_general(theme: &Theme, color: bool) -> String { let bullet = if color { theme.warn("*") } else { "*".to_string() }; let mut out = String::new(); out.push_str(&format!( "{}\n\n", if color { theme.hdr("Welcome to Agent Manager.") } else { "Welcome to Agent Manager.".to_string() } )); out.push_str("Here are some tips to help you get started.\n"); out.push_str(&format!(" {bullet} help -h or help help - show available 'help' subcommands and examples\n")); out.push_str(&format!(" {bullet} help commands - list all available commands\n")); out.push_str(&format!(" {bullet} help - display help about a particular command or agent\n")); out.push_str(&format!(" {bullet} help --find - search through all help commands table\n")); out.push_str(&format!( " {bullet} am --theme - choose a color theme ({})\n", crate::theme::names().join(", ") )); out.push('\n'); out.push_str( "am works on the idea of a \"catalog\". Catalog entries describe AI coding\n\ agents: how to install them, which dependencies they need, and how to start\n\ them. am installs, starts, stops and updates those agents, and its\n\ interactive prompt doubles as a gateway to your system shell.\n", ); out.push('\n'); out.push_str(&format!("{}\n\n", header("[Examples]", theme, color))); let example = |desc: &str, code: &str, out: &mut String| { out.push_str(&format!(" {desc}\n")); out.push_str(&format!(" {} {code}\n\n", prompt(">", theme, color))); }; example("List the installed agents:", "am list", &mut out); example( "Install an agent and its dependencies:", "am install claude-code", &mut out, ); example( "Start an agent in the background:", "am start claude-code --background", &mut out, ); example("Search the catalog:", "am search coding", &mut out); out.push_str( "Type 'am help commands' for the complete command list, or run 'am' alone for\n\ the interactive shell.\n", ); out } /// Print the general help overview. pub fn print_general(app: &App) { print!("{}", render_general(app.theme(), app.color())); } /// Print a help topic: None → overview, "commands" → the command list, /// anything else → the command/agent help. pub fn print_topic(app: &App, topic: Option<&str>) -> Result { match topic.map(str::trim).filter(|s| !s.is_empty()) { None => { print_general(app); Ok(0) } Some("commands") => { print_commands_list(app); Ok(0) } Some(name) => print_command(app, name), } } /// The 'help commands' table (also used by 'help --find'). pub fn print_commands_list(app: &App) { let specs: Vec<&HelpSpec> = HELP_SPECS.iter().collect(); print_specs_table(app, &specs); } /// 'help --find ': search through all help commands. pub fn print_find(app: &App, text: &str) { let hits = matching_specs(text); if hits.is_empty() { app.log .info(format!("no help matches '{text}' — try 'am help commands'")); return; } print_specs_table(app, &hits); } fn print_specs_table(app: &App, specs: &[&HelpSpec]) { let color = app.color(); let theme = app.theme(); let width = crate::output::terminal_width().unwrap_or(120); let table = crate::tables::DataTable { columns: vec![ "name".to_string(), "category".to_string(), "usage".to_string(), "description".to_string(), ], rows: specs .iter() .map(|s| { vec![ crate::tables::Cell::str(name_style(s.name, theme, color)), crate::tables::Cell::str(s.category.to_string()), crate::tables::Cell::str(s.usage.to_string()), crate::tables::Cell::str(truncate( s.about.lines().next().unwrap_or(""), 48, )), ] }) .collect(), }; print!("{}", table.render_themed(theme, color, width)); } // --------------------------------------------------------------------------- // Version (Nushell-style table) // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize)] pub struct VersionInfo { pub version: String, pub major: u64, pub minor: u64, pub patch: u64, pub branch: String, pub commit_hash: String, pub build_os: String, pub build_target: String, pub rust_version: String, pub rust_channel: String, pub cargo_version: String, pub build_time: String, pub build_rust_channel: String, pub allocator: String, } /// Collect the version and build information shown by 'am version'. pub fn version_info() -> VersionInfo { let version = env!("CARGO_PKG_VERSION").to_string(); let mut parts = version.split('.'); let major = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0); let minor = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0); let patch = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0); let (rust_version, rust_channel) = rust_info(); VersionInfo { version, major, minor, patch, branch: option_env!("AM_GIT_BRANCH").unwrap_or("").to_string(), commit_hash: option_env!("AM_GIT_COMMIT").unwrap_or("").to_string(), build_os: build_os(), build_target: build_target(), rust_version, rust_channel, cargo_version: tool_version("cargo", &["--version"]).unwrap_or_default(), build_time: build_time(), build_rust_channel: if cfg!(debug_assertions) { "debug".to_string() } else { "release".to_string() }, allocator: "standard".to_string(), } } fn tool_version(program: &str, args: &[&str]) -> Option { std::process::Command::new(program) .args(args) .output() .ok() .filter(|o| o.status.success()) .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) .filter(|s| !s.is_empty()) } fn line_of(text: &str, key: &str) -> Option { text.lines() .find_map(|l| l.trim().strip_prefix(key).map(|v| v.trim().to_string())) } fn rust_info() -> (String, String) { let version = tool_version("rustc", &["--version"]).unwrap_or_default(); let channel = tool_version("rustc", &["-vV"]) .and_then(|v| { let release = line_of(&v, "release:")?; match line_of(&v, "host:") { Some(host) => Some(format!("{release}-{host}")), None => Some(release), } }) .unwrap_or_default(); (version, channel) } fn build_os() -> String { let os = if cfg!(windows) { "windows" } else if cfg!(target_os = "macos") { "macos" } else { std::env::consts::OS }; format!("{os}-{}", std::env::consts::ARCH) } fn build_target() -> String { let vendor = if cfg!(target_vendor = "apple") { "apple" } else if cfg!(target_vendor = "pc") { "pc" } else { "unknown" }; let env = if cfg!(target_env = "msvc") { "msvc" } else if cfg!(target_env = "gnu") { "gnu" } else if cfg!(target_env = "musl") { "musl" } else { "" }; let suffix = if env.is_empty() { String::new() } else { format!("-{env}") }; format!( "{}-{}-{}{}", std::env::consts::ARCH, vendor, std::env::consts::OS, suffix ) } /// Build time: the modification time of the running executable (the moment /// cargo wrote it), formatted like the Nushell build_time row. fn build_time() -> String { let modified = std::env::current_exe() .ok() .and_then(|p| std::fs::metadata(p).ok()) .and_then(|m| m.modified().ok()); modified .map(|t| { let dt: chrono::DateTime = t.into(); dt.format("%Y-%m-%d %H:%M:%S +00:00").to_string() }) .unwrap_or_default() } /// Render the version table (two columns, boxed, no header row), colored /// with the active theme: keys in the key style, values in the value style. pub fn version_table(info: &VersionInfo, theme: &Theme, color: bool) -> String { let rows: Vec<(String, String)> = vec![ ("version".to_string(), info.version.clone()), ("major".to_string(), info.major.to_string()), ("minor".to_string(), info.minor.to_string()), ("patch".to_string(), info.patch.to_string()), ("branch".to_string(), info.branch.clone()), ("commit_hash".to_string(), info.commit_hash.clone()), ("build_os".to_string(), info.build_os.clone()), ("build_target".to_string(), info.build_target.clone()), ("rust_version".to_string(), info.rust_version.clone()), ("rust_channel".to_string(), info.rust_channel.clone()), ("cargo_version".to_string(), info.cargo_version.clone()), ("build_time".to_string(), info.build_time.clone()), ("build_rust_channel".to_string(), info.build_rust_channel.clone()), ("allocator".to_string(), info.allocator.clone()), ("theme".to_string(), theme.name.to_string()), ]; crate::output::kv_table(&rows, theme, color) } /// Print the version table. pub fn print_version(app: &App) { print!("{}", version_table(&version_info(), app.theme(), app.color())); } // --------------------------------------------------------------------------- // -h/--help interception (runs before clap parses the command line) // --------------------------------------------------------------------------- /// Intercept Nushell-style help requests on the raw command line: /// /// * 'am -h' / 'am --help' → the general help /// * 'am -h|--help' (also 'am ls -h' for shell commands) → the /// command help; flags after '--' are never intercepted, so /// 'am run -- --help' still passes --help through to the agent. /// /// Returns Some(exit code) when a help request was handled, None when clap /// should parse the command line normally. pub fn intercept(args: &[String]) -> Option { if args.len() < 2 { return None; } let color = !args.iter().any(|a| a == "--no-color") && std::io::stdout().is_terminal(); let theme = crate::theme::default_theme(); let mut topic: Option<&'static HelpSpec> = None; for token in args.iter().skip(1) { let t = token.as_str(); if t == "--" { break; } if t == "-h" || t == "--help" { match topic { None => { print!("{}", render_general(theme, color)); return Some(0); } Some(spec) => { print!("{}", render_command(spec, theme, color)); return Some(0); } } } if topic.is_none() { topic = find_spec(t); } } None } #[cfg(test)] mod tests { use super::*; fn t() -> &'static Theme { crate::theme::default_theme() } fn cli_args(v: &[&str]) -> Vec { std::iter::once("am".to_string()) .chain(v.iter().map(|s| s.to_string())) .collect() } #[test] fn renders_ls_help_sections() { let text = render_command(find_spec("ls").unwrap(), t(), false); for part in [ "List the filenames, sizes, and modification times", "Search terms:", "Usage:", "> ls {flags} [path]", "Flags:", "-h, --help", "Command Type:", "built-in", "Parameters:", "Input/output types:", "Examples:", "> ls", ] { assert!(text.contains(part), "missing {part:?} in:\n{text}"); } } #[test] fn renders_install_help_flags() { let text = render_command(find_spec("install").unwrap(), t(), false); assert!(text.contains("--force")); assert!(text.contains("--method")); assert!(text.contains("")); assert!(text.contains("Global Flags:")); } #[test] fn renders_config_subcommands() { let text = render_command(find_spec("config").unwrap(), t(), false); assert!(text.contains("Subcommands:")); assert!(text.contains("validate")); } #[test] fn intercepts_help_flags() { assert!(intercept(&cli_args(&["ls", "--help"])).is_some()); assert!(intercept(&cli_args(&["-h"])).is_some()); assert!(intercept(&cli_args(&["--help"])).is_some()); assert!(intercept(&cli_args(&["install", "-h"])).is_some()); assert!(intercept(&cli_args(&["list", "-v", "--help"])).is_some()); assert!(intercept(&cli_args(&["run", "--help"])).is_some()); assert!(intercept(&cli_args(&["run", "claude-code", "--help"])).is_some()); // No help flag at all: clap parses normally. assert!(intercept(&cli_args(&["list"])).is_none()); assert!(intercept(&cli_args(&["install", "claude-code"])).is_none()); // -- stops flag parsing. assert!(intercept(&cli_args(&["list", "--", "-h"])).is_none()); assert!(intercept(&cli_args(&[])).is_none()); } #[test] fn general_help_has_tips() { let text = render_general(t(), false); assert!(text.contains("Welcome to Agent Manager.")); assert!(text.contains("help commands")); assert!(text.contains("help --find")); assert!(text.contains("[Examples]")); assert!(text.contains("> am install claude-code")); } #[test] fn find_matches() { let hits = matching_specs("install"); assert!(hits.iter().any(|s| s.name == "install")); assert!(hits.iter().any(|s| s.name == "uninstall")); let hits = matching_specs("processes"); assert!(hits.iter().any(|s| s.name == "ps")); } #[test] fn every_spec_has_content() { for s in HELP_SPECS { assert!(!s.about.is_empty(), "{}: empty about", s.name); assert!(!s.usage.is_empty(), "{}: empty usage", s.name); assert!(!s.examples.is_empty(), "{}: no examples", s.name); } } #[test] fn version_table_is_aligned() { let info = version_info(); let text = version_table(&info, t(), false); let widths: Vec = text.lines().map(|l| l.chars().count()).collect(); assert!( widths.iter().all(|w| *w == widths[0]), "unaligned:\n{text}" ); assert!(text.contains("version")); assert!(text.contains("build_target")); assert!(text.contains("allocator")); assert_eq!(info.version, env!("CARGO_PKG_VERSION")); assert_eq!(info.major, 0); // The minor version follows Cargo.toml: parse it instead of pinning it. let minor: u64 = env!("CARGO_PKG_VERSION") .split('.') .nth(1) .unwrap() .parse() .unwrap(); assert_eq!(info.minor, minor); } #[test] fn build_target_looks_like_a_triple() { let target = build_target(); assert_eq!(target.split('-').count(), 4, "{target}"); assert!(target.contains(std::env::consts::ARCH), "{target}"); } }