1393 lines
48 KiB
Rust
1393 lines
48 KiB
Rust
//! Nushell-style help: every command documented as a spec, rendered like
|
|
//! 'nu help <command>' (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: default, ocean, sunset, forest, dracula, mono",
|
|
},
|
|
];
|
|
|
|
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: "search",
|
|
category: "Commands",
|
|
usage: "search {flags} <keyword>",
|
|
about: "Search the catalog by keyword (name, description, category, tags).",
|
|
search_terms: &["find", "catalog", "keyword"],
|
|
flags: &[
|
|
HelpFlag { short: "", long: "--category", value: "CATEGORY", desc: "Restrict to this category" },
|
|
],
|
|
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" },
|
|
],
|
|
},
|
|
HelpSpec {
|
|
name: "info",
|
|
category: "Commands",
|
|
usage: "info {flags} <agent>",
|
|
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: "install",
|
|
category: "Commands",
|
|
usage: "install {flags} <agent>",
|
|
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} <agent>",
|
|
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} <agent>",
|
|
about: "Start an agent (foreground by default, or detached with --background).",
|
|
search_terms: &["launch", "begin"],
|
|
flags: START_FLAGS,
|
|
subcommands: &[],
|
|
parameters: &[
|
|
HelpParam { name: "agent", typ: "string", desc: "Agent name, alias, or group:<name>" },
|
|
],
|
|
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" },
|
|
],
|
|
},
|
|
HelpSpec {
|
|
name: "stop",
|
|
category: "Commands",
|
|
usage: "stop {flags} <agent>",
|
|
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:<name>" },
|
|
],
|
|
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} <agent>",
|
|
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:<name>" },
|
|
],
|
|
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} <agent> ...(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} <subcommand>",
|
|
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 <file>", "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} <shell>",
|
|
about: "Generate a shell completion script (bash, zsh, fish, powershell, elvish).",
|
|
search_terms: &["autocomplete", "shell"],
|
|
flags: &[],
|
|
subcommands: &[],
|
|
parameters: &[
|
|
HelpParam { name: "shell", typ: "oneof<bash, zsh, fish, powershell, elvish>", desc: "Shell to generate completions for" },
|
|
],
|
|
io: None,
|
|
examples: &[
|
|
HelpExample { desc: "Generate PowerShell completions.", code: "completion powershell" },
|
|
],
|
|
},
|
|
HelpSpec {
|
|
name: "self-update",
|
|
category: "Commands",
|
|
usage: "self-update {flags}",
|
|
about: "Update agent-manager itself from GitHub Releases.",
|
|
search_terms: &["upgrade", "release"],
|
|
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} <file>",
|
|
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: "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 (default, ocean, sunset, forest, dracula, mono); omit to list them" },
|
|
],
|
|
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} <column> <operator> <value>",
|
|
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} <column>",
|
|
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<usize> = 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 <name> for commands, aliases and
|
|
/// modules" behavior).
|
|
pub fn print_command(app: &App, name: &str) -> Result<i32, anyhow::Error> {
|
|
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 <text>'"
|
|
)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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 <name> - display help about a particular command or agent\n"));
|
|
out.push_str(&format!(" {bullet} help --find <text to search> - search through all help commands table\n"));
|
|
out.push_str(&format!(
|
|
" {bullet} am --theme <name> - 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<i32, anyhow::Error> {
|
|
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 <text>': 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<String> {
|
|
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<String> {
|
|
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<chrono::Utc> = 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 <command> -h|--help' (also 'am ls -h' for shell commands) → the
|
|
/// command help; flags after '--' are never intercepted, so
|
|
/// 'am run <agent> -- --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<i32> {
|
|
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<String> {
|
|
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("<agent>"));
|
|
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<usize> = 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);
|
|
assert_eq!(info.minor, 2);
|
|
}
|
|
|
|
#[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}");
|
|
}
|
|
}
|