diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 75f9954d..09fd854a 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -696,6 +696,8 @@ struct CMUXCLI { if dispatchSubcommandHelp(command: command, commandArgs: commandArgs) { return } + print("Unknown command '\(command)'. Run 'cmux help' to see available commands.") + return } let client = SocketClient(path: socketPath) @@ -3394,10 +3396,59 @@ struct CMUXCLI { throw CLIError(message: "Unable to resolve surface ID") } - /// Return the help/usage text for a subcommand, or nil if the command has no - /// dedicated help (e.g. simple no-arg commands like `ping`). + /// Return the help/usage text for a subcommand, or nil if the command is unknown. private func subcommandUsage(_ command: String) -> String? { switch command { + case "ping": + return """ + Usage: cmux ping + + Check connectivity to the cmux socket server. + """ + case "capabilities": + return """ + Usage: cmux capabilities + + Print server capabilities as JSON. + """ + case "help": + return """ + Usage: cmux help + + Show top-level CLI usage and command list. + """ + case "identify": + return """ + Usage: cmux identify [--workspace ] [--surface ] [--no-caller] + + Print server identity and caller context details. + + Flags: + --workspace Caller workspace context (default: $CMUX_WORKSPACE_ID) + --surface Caller surface context (default: $CMUX_SURFACE_ID) + --no-caller Omit caller context from the request + """ + case "list-windows": + return """ + Usage: cmux list-windows + + List open windows. + """ + case "current-window": + return """ + Usage: cmux current-window + + Print the currently selected window ID. + """ + case "new-window": + return """ + Usage: cmux new-window + + Create a new window. + + Example: + cmux new-window + """ case "focus-window": return """ Usage: cmux focus-window --window @@ -3426,47 +3477,56 @@ struct CMUXCLI { """ case "move-workspace-to-window": return """ - Usage: cmux move-workspace-to-window --workspace --window + Usage: cmux move-workspace-to-window --workspace --window Move a workspace to a different window. Flags: - --workspace Workspace to move (required) - --window Target window (required) + --workspace Workspace to move (required) + --window Target window (required) Example: cmux move-workspace-to-window --workspace workspace:2 --window window:1 """ case "move-surface": return """ - Usage: cmux move-surface --surface [flags] + Usage: cmux move-surface [--surface | ] [flags] Move a surface to a different pane, workspace, or window. Flags: - --surface Surface to move (required) + --surface Surface to move (required unless passed positionally) --pane Target pane --workspace Target workspace --window Target window --before Place before this surface + --before-surface + Alias for --before --after Place after this surface + --after-surface + Alias for --after --index Place at this index --focus Focus the surface after moving Example: cmux move-surface --surface surface:1 --workspace workspace:2 - cmux move-surface --surface 0 --pane pane:2 --index 0 + cmux move-surface surface:1 --pane pane:2 --index 0 """ case "reorder-surface": return """ - Usage: cmux reorder-surface --surface [flags] + Usage: cmux reorder-surface [--surface | ] [flags] Reorder a surface within its pane. Flags: - --surface Surface to reorder (required) + --surface Surface to reorder (required unless passed positionally) + --workspace Workspace context --before Place before this surface + --before-surface + Alias for --before --after Place after this surface + --after-surface + Alias for --after --index Place at this index Example: @@ -3475,15 +3535,19 @@ struct CMUXCLI { """ case "reorder-workspace": return """ - Usage: cmux reorder-workspace --workspace [flags] + Usage: cmux reorder-workspace [--workspace | ] [flags] Reorder a workspace within its window. Flags: - --workspace Workspace to reorder (required) + --workspace Workspace to reorder (required unless passed positionally) --index Place at this index --before Place before this workspace + --before-workspace + Alias for --before --after Place after this workspace + --after-workspace + Alias for --after --window Window context Example: @@ -3506,7 +3570,7 @@ struct CMUXCLI { Flags: --action Action name (required if not positional) --workspace Target workspace (default: current/$CMUX_WORKSPACE_ID) - --title Title for rename + --title Title for rename (or pass trailing title text) Example: cmux workspace-action --workspace workspace:2 --action pin @@ -3529,10 +3593,10 @@ struct CMUXCLI { Flags: --action Action name (required if not positional) - --tab Target tab (accepts tab: or surface:; alias: --surface) + --tab Target tab (accepts tab: or surface:; default: $CMUX_TAB_ID, then $CMUX_SURFACE_ID, then focused tab) --surface Alias for --tab (backward compatibility) --workspace Workspace context (default: current/$CMUX_WORKSPACE_ID) - --title Title for rename + --title Title for rename (or pass trailing title text) --url Optional URL for new-browser-right Example: @@ -3562,12 +3626,25 @@ struct CMUXCLI { """ case "new-workspace": return """ - Usage: cmux new-workspace + Usage: cmux new-workspace [--command ] Create a new workspace in the current window. + Flags: + --command Send text+Enter to the new workspace after creation + Example: cmux new-workspace + cmux new-workspace --command "npm test" + """ + case "list-workspaces": + return """ + Usage: cmux list-workspaces + + List workspaces in the current window. + + Example: + cmux list-workspaces """ case "new-split": return """ @@ -3584,6 +3661,33 @@ struct CMUXCLI { cmux new-split right cmux new-split down --workspace workspace:1 """ + case "list-panes": + return """ + Usage: cmux list-panes [--workspace ] + + List panes in a workspace. + + Flags: + --workspace Workspace context (default: $CMUX_WORKSPACE_ID) + + Example: + cmux list-panes + cmux list-panes --workspace workspace:2 + """ + case "list-pane-surfaces": + return """ + Usage: cmux list-pane-surfaces [--workspace ] [--pane ] + + List surfaces in a pane. + + Flags: + --workspace Workspace context (default: $CMUX_WORKSPACE_ID) + --pane Restrict to a specific pane (default: focused pane) + + Example: + cmux list-pane-surfaces + cmux list-pane-surfaces --workspace workspace:2 --pane pane:1 + """ case "tree": return """ Usage: cmux tree [flags] @@ -3612,16 +3716,17 @@ struct CMUXCLI { """ case "focus-pane": return """ - Usage: cmux focus-pane --pane [flags] + Usage: cmux focus-pane [--pane | ] [flags] Focus the specified pane. Flags: - --pane Pane to focus (required) + --pane Pane to focus (required unless passed positionally) --workspace Workspace context (default: $CMUX_WORKSPACE_ID) Example: cmux focus-pane --pane pane:2 + cmux focus-pane pane:1 cmux focus-pane --pane pane:1 --workspace workspace:2 """ case "new-pane": @@ -3685,26 +3790,87 @@ struct CMUXCLI { cmux drag-surface-to-split --surface surface:1 right cmux drag-surface-to-split --panel surface:2 down """ + case "refresh-surfaces": + return """ + Usage: cmux refresh-surfaces + + Refresh surface snapshots for the focused workspace. + """ + case "surface-health": + return """ + Usage: cmux surface-health [--workspace ] + + List health details for surfaces in a workspace. + + Flags: + --workspace Workspace context (default: $CMUX_WORKSPACE_ID) + + Example: + cmux surface-health + cmux surface-health --workspace workspace:2 + """ + case "trigger-flash": + return """ + Usage: cmux trigger-flash [--workspace ] [--surface ] [--panel ] + + Trigger the unread flash indicator for a surface. + + Flags: + --workspace Workspace context (default: $CMUX_WORKSPACE_ID) + --surface Target surface (default: $CMUX_SURFACE_ID) + --panel Alias for --surface + + Example: + cmux trigger-flash + cmux trigger-flash --workspace workspace:2 --surface surface:3 + """ + case "list-panels": + return """ + Usage: cmux list-panels [--workspace ] + + List surfaces (panels) in a workspace. + + Flags: + --workspace Workspace context (default: $CMUX_WORKSPACE_ID) + + Example: + cmux list-panels + cmux list-panels --workspace workspace:2 + """ + case "focus-panel": + return """ + Usage: cmux focus-panel --panel [--workspace ] + + Focus a specific panel (surface). + + Flags: + --panel Panel/surface to focus (required) + --workspace Workspace context (default: $CMUX_WORKSPACE_ID) + + Example: + cmux focus-panel --panel surface:2 + cmux focus-panel --panel surface:5 --workspace workspace:2 + """ case "close-workspace": return """ - Usage: cmux close-workspace --workspace + Usage: cmux close-workspace --workspace Close the specified workspace. Flags: - --workspace Workspace to close (required) + --workspace Workspace to close (required) Example: cmux close-workspace --workspace workspace:2 """ case "select-workspace": return """ - Usage: cmux select-workspace --workspace + Usage: cmux select-workspace --workspace Select (switch to) the specified workspace. Flags: - --workspace Workspace to select (required) + --workspace Workspace to select (required) Example: cmux select-workspace --workspace workspace:2 @@ -3712,51 +3878,210 @@ struct CMUXCLI { """ case "rename-workspace", "rename-window": return """ - Usage: cmux rename-workspace [--workspace ] [--] + Usage: cmux rename-workspace [--workspace <id|ref|index>] [--] <title> Rename a workspace. Defaults to the current workspace. tmux-compatible alias: rename-window Flags: - --workspace <id|ref> Workspace to rename (default: current workspace) + --workspace <id|ref|index> Workspace to rename (default: current/$CMUX_WORKSPACE_ID) Example: cmux rename-workspace "backend logs" cmux rename-window --workspace workspace:2 "agent run" """ + case "current-workspace": + return """ + Usage: cmux current-workspace + + Print the currently selected workspace ID. + """ case "capture-pane": return """ Usage: cmux capture-pane [--workspace <id|ref>] [--surface <id|ref>] [--scrollback] [--lines <n>] tmux-compatible alias for reading terminal text from a pane. + Flags: + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --surface <id|ref> Surface context (default: $CMUX_SURFACE_ID) + --scrollback Include scrollback + --lines <n> Return only the last N lines (implies --scrollback) + Example: cmux capture-pane --workspace workspace:2 --surface surface:1 --scrollback --lines 200 """ case "resize-pane": return """ - Usage: cmux resize-pane --pane <id|ref> [--workspace <id|ref>] (-L|-R|-U|-D) [--amount <n>] + Usage: cmux resize-pane [--pane <id|ref>] [--workspace <id|ref>] [-L|-R|-U|-D] [--amount <n>] tmux-compatible pane resize command. - Note: currently returns not_supported until programmable divider resize is implemented. + + Flags: + --pane <id|ref> Pane to resize (default: focused pane) + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + -L|-R|-U|-D Direction (default: -R) + --amount <n> Resize amount (default: 1) """ case "pipe-pane": return """ - Usage: cmux pipe-pane --command <shell-command> [--workspace <id|ref>] [--surface <id|ref>] + Usage: cmux pipe-pane [--workspace <id|ref>] [--surface <id|ref>] [--command <shell-command> | <shell-command>] Capture pane text and pipe it to a shell command via stdin. + + Flags: + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --surface <id|ref> Surface context (default: focused surface) + --command <command> Shell command to run (or pass as trailing text) """ case "wait-for": return """ Usage: cmux wait-for [-S|--signal] <name> [--timeout <seconds>] Wait for or signal a named synchronization token. - """ - case "swap-pane", "break-pane", "join-pane", "next-window", "previous-window", "last-window", "last-pane", "find-window", "clear-history", "set-hook", "popup", "bind-key", "unbind-key", "copy-mode", "set-buffer", "paste-buffer", "list-buffers", "respawn-pane", "display-message": - return """ - Usage: cmux \(command) --help - tmux compatibility command. See `cmux --help` for exact syntax. + Flags: + -S, --signal Signal the token instead of waiting + --timeout <seconds> Wait timeout (default: 30) + """ + case "swap-pane": + return """ + Usage: cmux swap-pane --pane <id|ref> --target-pane <id|ref> [--workspace <id|ref>] + + Swap two panes. + + Flags: + --pane <id|ref> Source pane (required) + --target-pane <id|ref> Target pane (required) + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + """ + case "break-pane": + return """ + Usage: cmux break-pane [--workspace <id|ref>] [--pane <id|ref>] [--surface <id|ref>] [--no-focus] + + Move a pane/surface out into its own pane context. + + Flags: + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --pane <id|ref> Source pane + --surface <id|ref> Source surface + --no-focus Do not focus the result + """ + case "join-pane": + return """ + Usage: cmux join-pane --target-pane <id|ref> [--workspace <id|ref>] [--pane <id|ref>] [--surface <id|ref>] [--no-focus] + + Join a pane/surface into another pane. + + Flags: + --target-pane <id|ref> Target pane (required) + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --pane <id|ref> Source pane + --surface <id|ref> Source surface + --no-focus Do not focus the result + """ + case "next-window", "previous-window", "last-window": + return """ + Usage: cmux \(command) + + Switch workspace selection (next/previous/last) in the current window. + """ + case "last-pane": + return """ + Usage: cmux last-pane [--workspace <id|ref>] + + Focus the previously focused pane in a workspace. + + Flags: + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + """ + case "find-window": + return """ + Usage: cmux find-window [--content] [--select] [query] + + Find workspaces by title (and optionally terminal content). + + Flags: + --content Search terminal content in addition to workspace titles + --select Select the first match + """ + case "clear-history": + return """ + Usage: cmux clear-history [--workspace <id|ref>] [--surface <id|ref>] + + Clear terminal scrollback history. + + Flags: + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --surface <id|ref> Surface context (default: focused surface) + """ + case "set-hook": + return """ + Usage: cmux set-hook [--list] [--unset <event>] | <event> <command> + + Manage tmux-compat hook definitions. + + Flags: + --list List configured hooks + --unset <event> Remove a hook by event name + """ + case "popup": + return """ + Usage: cmux popup + + tmux compatibility placeholder. This command is currently not supported. + """ + case "bind-key", "unbind-key", "copy-mode": + return """ + Usage: cmux \(command) + + tmux compatibility placeholder. This command is currently not supported. + """ + case "set-buffer": + return """ + Usage: cmux set-buffer [--name <name>] [--] <text> + + Save text into a named tmux-compat buffer. + + Flags: + --name <name> Buffer name (default: default) + """ + case "paste-buffer": + return """ + Usage: cmux paste-buffer [--name <name>] [--workspace <id|ref>] [--surface <id|ref>] + + Paste a named tmux-compat buffer into a surface. + + Flags: + --name <name> Buffer name (default: default) + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --surface <id|ref> Surface context (default: focused surface) + """ + case "list-buffers": + return """ + Usage: cmux list-buffers + + List tmux-compat buffers. + """ + case "respawn-pane": + return """ + Usage: cmux respawn-pane [--workspace <id|ref>] [--surface <id|ref>] [--command <cmd> | <cmd>] + + Send a command (or default shell restart command) to a surface. + + Flags: + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --surface <id|ref> Surface context (default: focused surface) + --command <cmd> Command text (or pass trailing command text) + """ + case "display-message": + return """ + Usage: cmux display-message [-p|--print] <text> + + Print text (or show it via notification bridge in parity mode). + + Flags: + -p, --print Print to stdout only """ case "read-screen": return """ @@ -3846,6 +4171,18 @@ struct CMUXCLI { cmux notify --title "Build done" --body "All tests passed" cmux notify --title "Error" --subtitle "test.swift" --body "Line 42: syntax error" """ + case "list-notifications": + return """ + Usage: cmux list-notifications + + List queued notifications. + """ + case "clear-notifications": + return """ + Usage: cmux clear-notifications + + Clear all queued notifications. + """ case "set-status": return """ Usage: cmux set-status <key> <value> [flags] @@ -3970,16 +4307,35 @@ struct CMUXCLI { cmux sidebar-state cmux sidebar-state --workspace workspace:2 """ + case "set-app-focus": + return """ + Usage: cmux set-app-focus <active|inactive|clear> + + Override app focus state for notification routing tests. + + Example: + cmux set-app-focus inactive + cmux set-app-focus clear + """ + case "simulate-app-active": + return """ + Usage: cmux simulate-app-active + + Trigger the app-active handler used by notification focus tests. + """ case "claude-hook": return """ - Usage: cmux claude-hook <session-start|stop|notification> [flags] + Usage: cmux claude-hook <session-start|active|stop|idle|notification|notify> [flags] Hook for Claude Code integration. Reads JSON from stdin. Subcommands: session-start Signal that a Claude session has started + active Alias for session-start stop Signal that a Claude session has stopped + idle Alias for stop notification Forward a Claude notification + notify Alias for notification Flags: --workspace <id|ref> Target workspace (default: $CMUX_WORKSPACE_ID) @@ -3994,29 +4350,81 @@ struct CMUXCLI { Usage: cmux browser [--surface <id|ref|index> | <surface>] <subcommand> [args] Browser automation commands. Most subcommands require a surface handle. + A surface can be passed as `--surface <handle>` or as the first positional token. + `open`/`open-split`/`new`/`identify` can run without an explicit surface. Subcommands: - open [url] Create browser split (or navigate if surface given) - open-split [url] Create browser in a new split - goto|navigate <url> Navigate to URL [--snapshot-after] - back|forward|reload History navigation [--snapshot-after] - url|get-url Get current URL - snapshot Get DOM snapshot [--interactive|-i] [--cursor] [--compact] [--max-depth <n>] [--selector <css>] - eval <script> Evaluate JavaScript - wait Wait for condition [--selector] [--text] [--url-contains] [--timeout-ms] - click|dblclick|hover <sel> Mouse actions [--snapshot-after] - type <selector> <text> Type text [--snapshot-after] - fill <selector> [text] Fill input [--snapshot-after] - press|keydown|keyup <key> Keyboard actions [--snapshot-after] - get <property> [selector] Get page properties (url|title|text|html|value|attr|count|box|styles) - find <strategy> <query> Find elements (role|text|label|placeholder|testid|first|last|nth) - identify Identify browser surface + open|open-split|new [url] [--workspace <id|ref|index>] [--window <id|ref|index>] + open/open-split/new default to $CMUX_WORKSPACE_ID when --workspace is omitted and --window is not set + goto|navigate <url> [--snapshot-after] + back|forward|reload [--snapshot-after] + url|get-url + focus-webview | is-webview-focused + snapshot [--interactive|-i] [--cursor] [--compact] [--max-depth <n>] [--selector <css>] + eval [--script <js> | <js>] + wait [--selector <css>] [--text <text>] [--url-contains <text>|--url <text>] [--load-state <interactive|complete>] [--function <js>] [--timeout-ms <ms>|--timeout <seconds>] + click|dblclick|hover|focus|check|uncheck|scroll-into-view [--selector <css> | <css>] [--snapshot-after] + type|fill [--selector <css> | <css>] [--text <text> | <text>] [--snapshot-after] + press|key|keydown|keyup [--key <key> | <key>] [--snapshot-after] + select [--selector <css> | <css>] [--value <value> | <value>] [--snapshot-after] + scroll [--selector <css>] [--dx <n>] [--dy <n>] [--snapshot-after] + screenshot [--out <path>] + get <url|title|text|html|value|attr|count|box|styles> [...] + text|html|value|count|box|styles|attr: [--selector <css> | <css>] + attr: [--attr <name> | <name>] + styles: [--property <name>] + is <visible|enabled|checked> [--selector <css> | <css>] + find <role|text|label|placeholder|alt|title|testid|first|last|nth> [...] + role: [--name <text>] [--exact] <role> + text|label|placeholder|alt|title|testid: [--exact] <text> + first|last: [--selector <css> | <css>] + nth: [--index <n> | <n>] [--selector <css> | <css>] + frame <main|selector> [--selector <css>] + dialog <accept|dismiss> [text] + download [wait] [--path <path>] [--timeout-ms <ms>|--timeout <seconds>] + cookies <get|set|clear> [--name <name>] [--value <value>] [--url <url>] [--domain <domain>] [--path <path>] [--expires <unix>] [--secure] [--all] + storage <local|session> <get|set|clear> [...] + tab <new|list|switch|close|<index>> [...] + console <list|clear> + errors <list|clear> + highlight [--selector <css> | <css>] + state <save|load> <path> + addinitscript|addscript [--script <js> | <js>] + addstyle [--css <css> | <css>] + viewport <width> <height> + geolocation|geo <latitude> <longitude> + offline <true|false> + trace <start|stop> [path] + network <route|unroute|requests> ... + route <pattern> [--abort] [--body <text>] + unroute <pattern> + screencast <start|stop> + input <mouse|keyboard|touch> [args...] + input_mouse | input_keyboard | input_touch + identify [--surface <id|ref|index>] Example: cmux browser open https://example.com cmux browser surface:1 navigate https://google.com cmux browser --surface surface:1 snapshot --interactive """ + // Legacy browser aliases — point users to `cmux browser --help` + case "open-browser": + return "Legacy alias for 'cmux browser open'. Run 'cmux browser --help' for details." + case "navigate": + return "Legacy alias for 'cmux browser navigate'. Run 'cmux browser --help' for details." + case "browser-back": + return "Legacy alias for 'cmux browser back'. Run 'cmux browser --help' for details." + case "browser-forward": + return "Legacy alias for 'cmux browser forward'. Run 'cmux browser --help' for details." + case "browser-reload": + return "Legacy alias for 'cmux browser reload'. Run 'cmux browser --help' for details." + case "get-url": + return "Legacy alias for 'cmux browser get-url'. Run 'cmux browser --help' for details." + case "focus-webview": + return "Legacy alias for 'cmux browser focus-webview'. Run 'cmux browser --help' for details." + case "is-webview-focused": + return "Legacy alias for 'cmux browser is-webview-focused'. Run 'cmux browser --help' for details." default: return nil } diff --git a/tests/test_cli_subcommand_help_regressions.py b/tests/test_cli_subcommand_help_regressions.py new file mode 100644 index 00000000..1d2b031c --- /dev/null +++ b/tests/test_cli_subcommand_help_regressions.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Regression tests for CLI subcommand help coverage and accuracy.""" + +from __future__ import annotations + +import re +import subprocess +from pathlib import Path + + +def get_repo_root() -> Path: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + return Path(result.stdout.strip()) + return Path.cwd() + + +def require(content: str, needle: str, message: str, failures: list[str]) -> None: + if needle not in content: + failures.append(message) + + +def extract_switch_commands(content: str, start_index: int = 0) -> tuple[set[str], int]: + marker = "switch command {" + marker_index = content.find(marker, start_index) + if marker_index == -1: + return set(), -1 + + open_brace = content.find("{", marker_index) + if open_brace == -1: + return set(), -1 + + depth = 1 + cursor = open_brace + 1 + while cursor < len(content) and depth > 0: + char = content[cursor] + if char == "{": + depth += 1 + elif char == "}": + depth -= 1 + cursor += 1 + + block = content[open_brace + 1:cursor - 1] + commands: set[str] = set() + collecting_case = False + case_lines: list[str] = [] + + for line in block.splitlines(): + stripped = line.strip() + if stripped.startswith("case "): + collecting_case = True + case_lines = [line] + elif collecting_case: + case_lines.append(line) + + if collecting_case and ":" in line: + case_text = "\n".join(case_lines) + commands.update(re.findall(r'"([^"]+)"', case_text)) + collecting_case = False + case_lines = [] + + return commands, cursor + + +def main() -> int: + repo_root = get_repo_root() + cli_path = repo_root / "CLI" / "cmux.swift" + if not cli_path.exists(): + print(f"FAIL: missing expected file: {cli_path}") + return 1 + + content = cli_path.read_text(encoding="utf-8") + failures: list[str] = [] + + require( + content, + 'if commandArgs.contains("--help") || commandArgs.contains("-h") {', + "Subcommand help pre-dispatch gate is missing", + failures, + ) + require( + content, + 'if dispatchSubcommandHelp(command: command, commandArgs: commandArgs) {', + "Subcommand help dispatch call is missing", + failures, + ) + require( + content, + "print(\"Unknown command '\\(command)'. Run 'cmux help' to see available commands.\")", + "Subcommand help fallback unknown-command line is missing", + failures, + ) + require( + content, + "print(\"Unknown command '\\(command)'. Run 'cmux help' to see available commands.\")\n return", + "Subcommand help fallback must return before command execution", + failures, + ) + + dispatch_commands, next_index = extract_switch_commands(content, 0) + subcommand_usage_commands, _ = extract_switch_commands(content, next_index if next_index != -1 else 0) + if not dispatch_commands: + failures.append("Failed to parse main dispatch switch command list") + if not subcommand_usage_commands: + failures.append("Failed to parse subcommandUsage switch command list") + + missing_help_entries = sorted(dispatch_commands - subcommand_usage_commands) + if missing_help_entries: + failures.append( + "Missing subcommandUsage entries for dispatch command(s): " + + ", ".join(missing_help_entries) + ) + + # Regression checks for concrete help text that previously drifted from dispatch logic. + for needle, message in [ + ('case "help":', "Missing subcommandUsage entry for help"), + ("Usage: cmux help", "help subcommand usage text is missing"), + ("Usage: cmux move-workspace-to-window --workspace <id|ref|index> --window <id|ref|index>", "move-workspace-to-window help must document index handles"), + ("--tab <id|ref|index> Target tab (accepts tab:<n> or surface:<n>; default: $CMUX_TAB_ID, then $CMUX_SURFACE_ID, then focused tab)", "tab-action help must document CMUX_TAB_ID/CMUX_SURFACE_ID fallback"), + ("--workspace <id|ref|index> Workspace to rename (default: current/$CMUX_WORKSPACE_ID)", "rename-workspace help must document CMUX_WORKSPACE_ID fallback"), + ("text|html|value|count|box|styles|attr: [--selector <css> | <css>]", "browser get help must document --selector"), + ("attr: [--attr <name> | <name>]", "browser get attr help must document --attr"), + ("styles: [--property <name>]", "browser get styles help must document --property"), + ("role: [--name <text>] [--exact] <role>", "browser find role help must document --name/--exact"), + ("text|label|placeholder|alt|title|testid: [--exact] <text>", "browser find text-like help must document --exact"), + ("nth: [--index <n> | <n>] [--selector <css> | <css>]", "browser find nth help must document --index/--selector"), + ("route <pattern> [--abort] [--body <text>]", "browser network route help must document --abort/--body"), + ]: + require(content, needle, message, failures) + + if failures: + print("FAIL: CLI subcommand help regression(s) detected") + for failure in failures: + print(f"- {failure}") + return 1 + + print("PASS: CLI subcommand help coverage and flag/env documentation are present") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())