//! 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: 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} ", 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} ", 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} ", 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).", 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" }, ], }, 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).", search_terms: &["autocomplete", "shell"], flags: &[], subcommands: &[], parameters: &[ HelpParam { name: "shell", typ: "oneof", 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} ", 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} ", 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); 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}"); } }