From 6fe647c10dfc468da4fd42a59416d4fbad2bb4f8 Mon Sep 17 00:00:00 2001 From: yhangf Date: Tue, 28 Apr 2026 14:28:21 +0800 Subject: [PATCH] chore(cli): rebrand CLI from Claude to CoStrict with restructured help output --- src/main.tsx | 370 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 229 insertions(+), 141 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index 27563e2dd..48befb197 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1243,9 +1243,11 @@ async function getInputPrompt( async function run(): Promise { profileCheckpoint("run_function_start"); - // Create help config that sorts options by long option name. - // Commander supports compareOptions at runtime but @commander-js/extra-typings - // doesn't include it in the type definitions, so we use Object.assign to add it. + // Create help config that sorts options by long option name and puts + // Commands section before Options section in the root command's help output. + // Commander supports compareOptions/formatHelp at runtime but + // @commander-js/extra-typings doesn't include them in the type definitions, + // so we use Object.assign to add them. function createSortedHelpConfig(): { sortSubcommands: true; sortOptions: true; @@ -1257,10 +1259,72 @@ async function run(): Promise { { compareOptions: (a: Option, b: Option) => getOptionSortKey(a).localeCompare(getOptionSortKey(b)), + // Reorder help sections: Usage → Description → Arguments → + // Commands → Options (matching cs/opencode style). + // Uses duck-typed access because @commander-js/extra-typings + // doesn't expose the full Help class API. + formatHelp: (cmd: CommanderCommand, helper: unknown) => { + const h = helper as { + padWidth: (cmd: CommanderCommand, helper: unknown) => number; + helpWidth: number; + commandUsage: (cmd: CommanderCommand) => string; + commandDescription: (cmd: CommanderCommand) => string; + visibleArguments: (cmd: CommanderCommand) => unknown[]; + visibleCommands: (cmd: CommanderCommand) => unknown[]; + visibleOptions: (cmd: CommanderCommand) => unknown[]; + argumentTerm: (arg: unknown) => string; + argumentDescription: (arg: unknown) => string; + subcommandTerm: (cmd: unknown) => string; + subcommandDescription: (cmd: unknown) => string; + optionTerm: (opt: unknown) => string; + optionDescription: (opt: unknown) => string; + formatItem: (term: string, termWidth: number, description: string, helper: unknown) => string; + styleTitle: (s: string) => string; + styleUsage: (s: string) => string; + styleCommandDescription: (s: string) => string; + styleArgumentTerm: (s: string) => string; + styleArgumentDescription: (s: string) => string; + styleSubcommandTerm: (s: string) => string; + styleSubcommandDescription: (s: string) => string; + styleOptionTerm: (s: string) => string; + styleOptionDescription: (s: string) => string; + boxWrap: (s: string, width: number) => string; + }; + const termWidth = h.padWidth(cmd, helper); + const helpWidth = h.helpWidth ?? 80; + const fmt = (term: string, desc: string) => + h.formatItem(term, termWidth, desc, helper); + + let output = [ + `${h.styleTitle('Usage:')} ${h.styleUsage(h.commandUsage(cmd))}`, + '', + ]; + + const desc = h.commandDescription(cmd); + if (desc.length > 0) { + output = output.concat([h.boxWrap(h.styleCommandDescription(desc), helpWidth), '']); + } + + // Order: Commands → Options → Arguments (matches usage line: [options] [command] [prompt]) + const cmdList = h.visibleCommands(cmd).map(c => + fmt(h.styleSubcommandTerm(h.subcommandTerm(c)), h.styleSubcommandDescription(h.subcommandDescription(c)))); + if (cmdList.length > 0) output = output.concat([h.styleTitle('Commands:'), ...cmdList, '']); + + const optList = h.visibleOptions(cmd).map(o => + fmt(h.styleOptionTerm(h.optionTerm(o)), h.styleOptionDescription(h.optionDescription(o)))); + if (optList.length > 0) output = output.concat([h.styleTitle('Options:'), ...optList, '']); + + const argList = h.visibleArguments(cmd).map(a => + fmt(h.styleArgumentTerm(h.argumentTerm(a)), h.styleArgumentDescription(h.argumentDescription(a)))); + if (argList.length > 0) output = output.concat([h.styleTitle('Arguments:'), ...argList, '']); + + return output.join('\n'); + }, }, ); } const program = new CommanderCommand() + .addHelpCommand(false) .configureHelp(createSortedHelpConfig()) .enablePositionalOptions(); profileCheckpoint("run_commander_initialized"); @@ -1340,10 +1404,9 @@ async function run(): Promise { }); program - .name("claude") - .description( - `CoStrict - starts an interactive session by default, use -p/--print for non-interactive output`, - ) + .name("csc") + .description(`CoStrict AI coding assistant`) + .usage("[command] [options] [prompt]") .argument("[prompt]", "Your prompt", String) // Subcommands inherit helpOption via commander's copyInheritedSettings — // setting it once here covers mcp, plugin, auth, and all other subcommands. @@ -1363,25 +1426,28 @@ async function run(): Promise { .argParser(Boolean) .hideHelp(), ) - .option( - "--debug-file ", - "Write debug logs to a specific file path (implicitly enables debug mode)", - () => true, + .addOption( + new Option("--debug-file ", "Write debug logs to a specific file path (implicitly enables debug mode)") + .argParser(() => true) + .hideHelp(), ) - .option( - "--verbose", - "Override verbose mode setting from config", - () => true, + .addOption( + new Option("--verbose", "Override verbose mode setting from config") + .argParser(() => true) + .hideHelp(), ) .option( "-p, --print", - "Print response and exit (useful for pipes). Note: The workspace trust dialog is skipped when Claude is run with the -p mode. Only use this flag in directories you trust.", + "Print response and exit (useful for pipes). Note: The workspace trust dialog is skipped when CSC is run with the -p mode. Only use this flag in directories you trust.", () => true, ) - .option( - "--bare", - "Minimal mode: skip hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery. Sets CLAUDE_CODE_SIMPLE=1. Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings (OAuth and keychain are never read). 3P providers (Bedrock/Vertex/Foundry) use their own credentials. Skills still resolve via /skill-name. Explicitly provide context via: --system-prompt[-file], --append-system-prompt[-file], --add-dir (CLAUDE.md dirs), --mcp-config, --settings, --agents, --plugin-dir.", - () => true, + .addOption( + new Option( + "--bare", + "Minimal mode: skip hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery. Sets CLAUDE_CODE_SIMPLE=1. Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings (OAuth and keychain are never read). 3P providers (Bedrock/Vertex/Foundry) use their own credentials. Skills still resolve via /skill-name. Explicitly provide context via: --system-prompt[-file], --append-system-prompt[-file], --add-dir (CLAUDE.md dirs), --mcp-config, --settings, --agents, --plugin-dir.", + ) + .argParser(() => true) + .hideHelp(), ) .addOption( new Option( @@ -1412,33 +1478,39 @@ async function run(): Promise { "--json-schema ", "JSON Schema for structured output validation. " + 'Example: {"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}', - ).argParser(String), + ) + .argParser(String) + .hideHelp(), ) - .option( - "--include-hook-events", - "Include all hook lifecycle events in the output stream (only works with --output-format=stream-json)", - () => true, + .addOption( + new Option("--include-hook-events", "Include all hook lifecycle events in the output stream (only works with --output-format=stream-json)") + .argParser(() => true) + .hideHelp(), ) - .option( - "--include-partial-messages", - "Include partial message chunks as they arrive (only works with --print and --output-format=stream-json)", - () => true, + .addOption( + new Option("--include-partial-messages", "Include partial message chunks as they arrive (only works with --print and --output-format=stream-json)") + .argParser(() => true) + .hideHelp(), ) .addOption( new Option( "--input-format ", 'Input format (only works with --print): "text" (default), or "stream-json" (realtime streaming input)', - ).choices(["text", "stream-json"]), + ) + .choices(["text", "stream-json"]) + .hideHelp(), ) - .option( - "--mcp-debug", - "[DEPRECATED. Use --debug instead] Enable MCP debug mode (shows MCP server errors)", - () => true, + .addOption( + new Option( + "--mcp-debug", + "[DEPRECATED. Use --debug instead] Enable MCP debug mode (shows MCP server errors)", + ) + .argParser(() => true) + .hideHelp(), ) - .option( - "--dangerously-skip-permissions", - "Bypass all permission checks. Recommended only for sandboxes with no internet access.", - () => true, + .addOption( + new Option("--dangerously-skip-permissions", "Bypass all permission checks. Recommended only for sandboxes with no internet access.") + .argParser(() => true), ) // .option( // "--allow-dangerously-skip-permissions", @@ -1473,15 +1545,17 @@ async function run(): Promise { new Option( "--max-budget-usd ", "Maximum dollar amount to spend on API calls (only works with --print)", - ).argParser((value) => { - const amount = Number(value); - if (isNaN(amount) || amount <= 0) { - throw new Error( - "--max-budget-usd must be a positive number greater than 0", - ); - } - return amount; - }), + ) + .argParser((value) => { + const amount = Number(value); + if (isNaN(amount) || amount <= 0) { + throw new Error( + "--max-budget-usd must be a positive number greater than 0", + ); + } + return amount; + }) + .hideHelp(), ) .addOption( new Option( @@ -1503,10 +1577,10 @@ async function run(): Promise { }) .hideHelp(), ) - .option( - "--replay-user-messages", - "Re-emit user messages from stdin back on stdout for acknowledgment (only works with --input-format=stream-json and --output-format=stream-json)", - () => true, + .addOption( + new Option("--replay-user-messages", "Re-emit user messages from stdin back on stdout for acknowledgment (only works with --input-format=stream-json and --output-format=stream-json)") + .argParser(() => true) + .hideHelp(), ) .addOption( new Option( @@ -1516,21 +1590,35 @@ async function run(): Promise { .default(false) .hideHelp(), ) - .option( - "--allowedTools, --allowed-tools ", - 'Comma or space-separated list of tool names to allow (e.g. "Bash(git:*) Edit")', + .addOption( + new Option( + "--allowed-tools ", + 'Comma or space-separated list of tool names to allow (e.g. "Bash(git:*) Edit")', + ).hideHelp(), ) - .option( - "--tools ", - 'Specify the list of available tools from the built-in set. Use "" to disable all tools, "default" to use all tools, or specify tool names (e.g. "Bash,Edit,Read").', + .addOption( + new Option( + "--allowedTools ", + 'Comma or space-separated list of tool names to allow (e.g. "Bash(git:*) Edit")', + ).hideHelp(), ) - .option( - "--disallowedTools, --disallowed-tools ", - 'Comma or space-separated list of tool names to deny (e.g. "Bash(git:*) Edit")', + .addOption( + new Option("--tools ", 'Specify the list of available tools from the built-in set. Use "" to disable all tools, "default" to use all tools, or specify tool names (e.g. "Bash,Edit,Read").').hideHelp(), ) - .option( - "--mcp-config ", - "Load MCP servers from JSON files or strings (space-separated)", + .addOption( + new Option( + "--disallowed-tools ", + 'Comma or space-separated list of tool names to deny (e.g. "Bash(git:*) Edit")', + ).hideHelp(), + ) + .addOption( + new Option( + "--disallowedTools ", + 'Comma or space-separated list of tool names to deny (e.g. "Bash(git:*) Edit")', + ).hideHelp(), + ) + .addOption( + new Option("--mcp-config ", "Load MCP servers from JSON files or strings (space-separated)").hideHelp(), ) .addOption( new Option( @@ -1558,7 +1646,9 @@ async function run(): Promise { new Option( "--append-system-prompt ", "Append a system prompt to the default system prompt", - ).argParser(String), + ) + .argParser(String) + .hideHelp(), ) .addOption( new Option( @@ -1586,10 +1676,10 @@ async function run(): Promise { "Resume a conversation by session ID, or open interactive picker with optional search term", (value) => value || true, ) - .option( - "--fork-session", - "When resuming, create a new session ID instead of reusing the original (use with --resume or --continue)", - () => true, + .addOption( + new Option("--fork-session", "When resuming, create a new session ID instead of reusing the original (use with --resume or --continue)") + .argParser(() => true) + .hideHelp(), ) .addOption( new Option( @@ -1620,14 +1710,14 @@ async function run(): Promise { }) .hideHelp(), ) - .option( - "--from-pr [value]", - "Resume a session linked to a PR by PR number/URL, or open interactive picker with optional search term", - (value) => value || true, + .addOption( + new Option("--from-pr [value]", "Resume a session linked to a PR by PR number/URL, or open interactive picker with optional search term") + .argParser((value) => value || true) + .hideHelp(), ) - .option( - "--no-session-persistence", - "Disable session persistence - sessions will not be saved to disk and cannot be resumed (only works with --print)", + .addOption( + new Option("--no-session-persistence", "Disable session persistence - sessions will not be saved to disk and cannot be resumed (only works with --print)") + .hideHelp(), ) .addOption( new Option( @@ -1646,34 +1736,34 @@ async function run(): Promise { // @[MODEL LAUNCH]: Update the example model ID in the --model help text. .option( "--model ", - `Model for the current session. Provide an alias for the latest model (e.g. 'sonnet' or 'opus') or a model's full name (e.g. 'claude-sonnet-4-6').`, + `Model for the current session. Provide a model's full name (e.g. 'GLM-5.1').`, ) .addOption( new Option( "--effort ", `Effort level for the current session (low, medium, high, max)`, - ).argParser((rawValue: string) => { - const value = rawValue.toLowerCase(); - const allowed = ["low", "medium", "high", "max"]; - if (!allowed.includes(value)) { - throw new InvalidArgumentError( - `It must be one of: ${allowed.join(", ")}`, - ); - } - return value; - }), + ) + .argParser((rawValue: string) => { + const value = rawValue.toLowerCase(); + const allowed = ["low", "medium", "high", "max"]; + if (!allowed.includes(value)) { + throw new InvalidArgumentError( + `It must be one of: ${allowed.join(", ")}`, + ); + } + return value; + }) + .hideHelp(), ) .option( "--agent ", `Agent for the current session. Overrides the 'agent' setting.`, ) - .option( - "--betas ", - "Beta headers to include in API requests (API key users only)", + .addOption( + new Option("--betas ", "Beta headers to include in API requests (API key users only)").hideHelp(), ) - .option( - "--fallback-model ", - "Enable automatic fallback to specified model when default model is overloaded (only works with --print)", + .addOption( + new Option("--fallback-model ", "Enable automatic fallback to specified model when default model is overloaded (only works with --print)").hideHelp(), ) .addOption( new Option( @@ -1689,49 +1779,46 @@ async function run(): Promise { "--add-dir ", "Additional directories to allow tool access to", ) - .option( - "--ide", - "Automatically connect to IDE on startup if exactly one valid IDE is available", - () => true, + .addOption( + new Option("--ide", "Automatically connect to IDE on startup if exactly one valid IDE is available") + .argParser(() => true) + .hideHelp(), ) - .option( - "--strict-mcp-config", - "Only use MCP servers from --mcp-config, ignoring all other MCP configurations", - () => true, + .addOption( + new Option("--strict-mcp-config", "Only use MCP servers from --mcp-config, ignoring all other MCP configurations") + .argParser(() => true) + .hideHelp(), ) - .option( - "--session-id ", - "Use a specific session ID for the conversation (must be a valid UUID)", + .addOption( + new Option("--session-id ", "Use a specific session ID for the conversation (must be a valid UUID)").hideHelp(), ) - .option( - "-n, --name ", - "Set a display name for this session (shown in /resume and terminal title)", + .addOption( + new Option("-n, --name ", "Set a display name for this session (shown in /resume and terminal title)").hideHelp(), ) - .option( - "--agents ", - 'JSON object defining custom agents (e.g. \'{"reviewer": {"description": "Reviews code", "prompt": "You are a code reviewer"}}\')', + .addOption( + new Option("--agents ", 'JSON object defining custom agents (e.g. \'{"reviewer": {"description": "Reviews code", "prompt": "You are a code reviewer"}}\')').hideHelp(), ) - .option( - "--setting-sources ", - "Comma-separated list of setting sources to load (user, project, local).", + .addOption( + new Option("--setting-sources ", "Comma-separated list of setting sources to load (user, project, local).").hideHelp(), ) // gh-33508: (variadic) consumed everything until the next // --flag. `claude --plugin-dir /path mcp add --transport http` swallowed // `mcp` and `add` as paths, then choked on --transport as an unknown // top-level option. Single-value + collect accumulator means each // --plugin-dir takes exactly one arg; repeat the flag for multiple dirs. - .option( - "--plugin-dir ", - "Load plugins from a directory for this session only (repeatable: --plugin-dir A --plugin-dir B)", - (val: string, prev: string[]) => [...prev, val], - [] as string[], + .addOption( + new Option("--plugin-dir ", "Load plugins from a directory for this session only (repeatable: --plugin-dir A --plugin-dir B)") + .argParser((val: string, prev: string[]) => [...prev, val]) + .default([] as string[]) + .hideHelp(), ) - .option("--disable-slash-commands", "Disable all skills", () => true) - .option("--chrome", "Enable Claude in Chrome integration") - .option("--no-chrome", "Disable Claude in Chrome integration") - .option( - "--file ", - "File resources to download at startup. Format: file_id:relative_path (e.g., --file file_abc:doc.txt file_def:img.png)", + .addOption( + new Option("--disable-slash-commands", "Disable all skills").argParser(() => true).hideHelp(), + ) + .addOption(new Option("--chrome", "Enable Claude in Chrome integration").hideHelp()) + .addOption(new Option("--no-chrome", "Disable Claude in Chrome integration").hideHelp()) + .addOption( + new Option("--file ", "File resources to download at startup. Format: file_id:relative_path (e.g., --file file_abc:doc.txt file_def:img.png)").hideHelp(), ) .action(async (prompt, options) => { profileCheckpoint("action_handler_start"); @@ -5532,13 +5619,11 @@ async function run(): Promise { ); // Worktree flags - program.option( - "-w, --worktree [name]", - "Create a new git worktree for this session (optionally specify a name)", + program.addOption( + new Option("-w, --worktree [name]", "Create a new git worktree for this session (optionally specify a name)").hideHelp(), ); - program.option( - "--tmux", - "Create a tmux session for the worktree (requires --worktree). Uses iTerm2 native panes when available; use --tmux=classic for traditional tmux.", + program.addOption( + new Option("--tmux", "Create a tmux session for the worktree (requires --worktree). Uses iTerm2 native panes when available; use --tmux=classic for traditional tmux.").hideHelp(), ); if (canUserConfigureAdvisor()) { @@ -5598,7 +5683,7 @@ async function run(): Promise { if (feature("PROACTIVE") || feature("KAIROS")) { program.addOption( - new Option("--proactive", "Start in proactive autonomous mode"), + new Option("--proactive", "Start in proactive autonomous mode").hideHelp(), ); } @@ -5607,7 +5692,7 @@ async function run(): Promise { new Option( "--messaging-socket-path ", "Unix domain socket path for the UDS messaging server (defaults to a tmp path)", - ), + ).hideHelp(), ); } @@ -5616,7 +5701,7 @@ async function run(): Promise { new Option( "--brief", "Enable SendUserMessage tool for agent-to-user communication", - ), + ).hideHelp(), ); } if (feature("KAIROS")) { @@ -5759,6 +5844,7 @@ async function run(): Promise { const mcp = program .command("mcp") .description("Configure and manage MCP servers") + .addHelpCommand(false) .configureHelp(createSortedHelpConfig()) .enablePositionalOptions(); @@ -5792,6 +5878,7 @@ async function run(): Promise { } mcp.command("remove ") + .alias("rm") .description("Remove an MCP server") .option( "-s, --scope ", @@ -5803,18 +5890,15 @@ async function run(): Promise { }); mcp.command("list") - .description( - "List configured MCP servers. Note: The workspace trust dialog is skipped and stdio servers from .mcp.json are spawned for health checks. Only use this command in directories you trust.", - ) + .alias("ls") + .description("List configured MCP servers") .action(async () => { const { mcpListHandler } = await import("./cli/handlers/mcp.js"); await mcpListHandler(); }); mcp.command("get ") - .description( - "Get details about an MCP server. Note: The workspace trust dialog is skipped and stdio servers from .mcp.json are spawned for health checks. Only use this command in directories you trust.", - ) + .description("Get details about an MCP server") .action(async (name: string) => { const { mcpGetHandler } = await import("./cli/handlers/mcp.js"); await mcpGetHandler(name); @@ -6088,6 +6172,7 @@ async function run(): Promise { const auth = program .command("auth") .description("Manage authentication") + .addHelpCommand(false) .configureHelp(createSortedHelpConfig()); auth.command("login") @@ -6149,7 +6234,9 @@ async function run(): Promise { const pluginCmd = program .command("plugin") .alias("plugins") + .alias("plug") .description("Manage CoStrict plugins") + .addHelpCommand(false) .configureHelp(createSortedHelpConfig()); pluginCmd @@ -6165,6 +6252,7 @@ async function run(): Promise { // Plugin list command pluginCmd .command("list") + .alias("ls") .description("List installed plugins") .option("--json", "Output as JSON") .option( @@ -6406,7 +6494,7 @@ async function run(): Promise { // Reads from disk cache — GrowthBook isn't initialized at registration time. if (getAutoModeEnabledStateIfCached() !== "disabled") { const autoModeCmd = program - .command("auto-mode") + .command("auto-mode", { hidden: true }) .description("Inspect auto mode classifier configuration"); autoModeCmd @@ -6471,7 +6559,7 @@ async function run(): Promise { if (feature("KAIROS")) { program - .command("assistant [sessionId]") + .command("assistant [sessionId]", { hidden: true }) .description( "Attach the REPL as a client to a running bridge session. Discovers sessions via API if no sessionId given.", ) @@ -6503,7 +6591,7 @@ async function run(): Promise { program .command('doctor') .description( - 'Check the health of your CoStrict auto-updater. Note: The workspace trust dialog is skipped and stdio servers from .mcp.json are spawned for health checks. Only use this command in directories you trust.', + 'Check the health of your CoStrict auto-updater', ) .action(async () => { const [{ doctorHandler }, { createRoot }] = await Promise.all([