refactor: single command-descriptor table for the CLI#76
Merged
Conversation
CT1 (nuclear-review): CLI command knowledge lived in three unsynchronized places — the kebab-case dispatch switch, the free-text usage() help string, and (implicitly) unknown-command detection. Nothing enforced sync between them, so a command could exist in one place and be silently missing from another. Introduces CommandDescriptor (name(s), help line(s), execute closure) and one commandDescriptors() table that is now the single source of truth for: dispatch, the grouped Commands: help block, and unknown-command handling. usage() generates its Commands: section from the table instead of a separately-maintained literal. Coverage: 108 command names in the table — 52 with logic inlined directly (previously inline switch-case bodies, now inline closures), 45 that call into existing bespoke handler functions (ssh, tree, browser, the 23-name tmux-compat group, etc.), and 11 pre-connection specials (version, welcome, shortcuts, feedback, themes, claude-teams, omo/omx/omc, codex, remote-daemon-status) that are documented in the table for help purposes only — their execution stays in run()'s existing pre-socket-connection if-chain, since some of them (version, welcome) must work even when Programa isn't running. Non-goal: this does not unify with the app-side v2 method switch in Sources/TerminalController.swift (processV2Command) — that's server code with different concerns (method routing vs. CLI UX) and is left as-is. Behavior is unchanged: same commands, same flags, same output, same exit codes. Help text is byte-identical to the prior usage() output (diffed old vs. new; verified via built binary). subcommandUsage()/dispatchSubcommandHelp() (the per-command detail text) are untouched — that mechanism sits outside CT1's three named unsynchronized places and unifying it is left for a follow-up.
4 tasks
arzafran
added a commit
that referenced
this pull request
Jul 8, 2026
CT3-lite (nuclear-review, final item): CLI/programa.swift had grown to 14,664 lines. Split CMUXCLI's methods along its existing domain seams into 8 new extension files, verbatim, along with the pbxproj wiring for the programa-cli target. No behavior change. Per-file line counts (post-split, includes new file boilerplate): CLI/programa.swift 6259 struct decl, args, run(), the #76 command-descriptor table, shared arg-parsing/handle-formatting/ socket-client helpers, usage and version/welcome text CLI/CLI+Markdown.swift 103 markdown commands CLI/CLI+SSH.swift 1329 ssh / remote relay CLI/CLI+Browser.swift 1310 browser command family CLI/CLI+Themes.swift 727 theme selection/config CLI/CLI+Tree.swift 544 tree command CLI/CLI+TmuxCompat.swift 1927 __tmux-compat translation layer CLI/CLI+AgentWrappers.swift 893 claude-teams/omo/omx/omc CLI/CLI+Hooks.swift 1660 Claude/Codex hook session logic ------------------------------------ total 14752 (original 14664 + 88 lines of import/extension boilerplate across the 8 new files) Stored properties stay in the main file (Swift forbids stored instance properties in extensions); only `let args: [String]` exists. Widened from `private` to `internal` only where the compiler forced it (cross-file references), 58 symbols total: Shared helpers in CLI/programa.swift now called from the new domain files: resolvePath, sanitizedFilenameComponent, bestEffortPruneTemporaryFiles, looksLikePath, authenticateClientIfNeeded, formatIDs, intFromAny, doubleFromAny, parseBoolString, isHandleRef, normalizeWindowHandle, normalizeWorkspaceHandle, normalizePaneHandle, normalizeSurfaceHandle, formatHandle, printV2Payload, resolveWorkspaceId, resolveSurfaceId, parseOption, optionValue, hasFlag, workspaceFromArgsOrEnv, textHandle, v2OKSummary, isUUID, jsonString, resolvedVersionInfo, parentSearchURL, candidateInfoPlistURLs, resolvedExecutableURL. Command entry points referenced by name in commandDescriptors()'s dispatch closures (which stay in the main file): runMarkdownCommand, runSSH, runSSHSessionEnd, runRemoteDaemonStatus, runBrowserCommand, runThemes, runTreeCommand, tmuxCompatDescriptors (static), runClaudeTeamsTmuxCompat, runTmuxCompatCommand, runClaudeTeams, runOMO, runOMX, runOMC, runClaudeHook, runCodexInstallHooks, runCodexUninstallHooks, runCodexHook, tmuxPruneCompatWorkspaceState, tmuxPruneCompatSurfaceState, resolveWorkspaceIdAllowingFallback, resolveSurfaceIdAllowingFallback. Cross-domain-file helpers: struct TmuxCompatFocusedContext and tmuxCompatFocusedContext, configureTmuxCompatEnvironment, createTmuxCompatShimDirectory (all in CLI+TmuxCompat.swift, called from CLI+AgentWrappers.swift); writeShimIfChanged (CLI+AgentWrappers.swift, called from CLI+TmuxCompat.swift); mergedNodeOptions (CLI+Hooks.swift, called from CLI+AgentWrappers.swift's configureAgentWrapperEnvironment). project.pbxproj: added the 8 new files to the programa-cli target's Sources build phase and CLI group (fresh B90000xx IDs, no collisions). Verified: programa-cli, programa, and programa-unit (build-for-testing) all build green under PROGRAMA_SKIP_ZIG_BUILD=1; the built CLI binary's --version and help output diffed byte-identical against a binary built from pre-split main.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
The Programa CLI knew about its own commands in three places that could silently drift apart: the dispatch switch, the free-text help string, and (implicitly) unknown-command detection. Nothing forced them to agree — today, 20 commands already exist in the dispatch switch but are missing from the help text. This PR collapses "does this command exist / what's its help line / how does it run" into one table, so that class of drift can't happen again.
CLI behavior is unchanged: same commands, same flags, same output, same exit codes, byte-identical help text.
Summary
CommandDescriptor(name(s), help line(s), execute closure) andCommandContext(the per-invocation bundle a command needs: args, socket client, flags).commandDescriptors()is now the single source of truth, read from three places:run()looks up the table by command name and calls itsexecuteclosure, replacing the old ~75-caseswitch.usage()'sCommands:section is generated by iterating the table in order, instead of a hand-maintained literal list.switch-case bodies; only their container changed).runSSH,runTreeCommand,runBrowserCommand, the 23-name tmux-compat group, etc.) — same functions, now reachable via a descriptor row instead of acase.version,welcome,shortcuts,feedback,themes,claude-teams,omo/omx/omc,codex,remote-daemon-status) — these still execute fromrun()'s existing pre-socket-connection branch (some, likeversion/welcome, must work even when Programa isn't running), but now have a table row purely sousage()has one place to read their help line from.ssh-session-end,debug-terminals,set-status/clear-status/list-status/set-progress/clear-progress/log/clear-log/list-log/sidebar-state,codex-hook,__tmux-compat, and the legacyopen-browser/navigate/browser-*/get-url/focus-webview/is-webview-focusedaliases). That gap is preserved as-is (empty help lines) rather than silently fixed, to keep this PR behavior-neutral — now it's an explicit, visible fact in the table instead of an implicit omission.Sources/TerminalController.swift(processV2Command) — that's server-side method routing with different concerns, left untouched.subcommandUsage()/dispatchSubcommandHelp()(the deep per-command--helptext switch) are untouched — that mechanism is outside the three places named in the original finding (which called out the dispatch switch and the free-textusage()/help strings specifically); unifying it is a reasonable follow-up but out of scope here.Test Plan
xcodebuild -scheme programa-cli— greenxcodebuild -scheme programa— greenxcodebuild -scheme programa-unit build-for-testing— greenusage()'s literal text fromgit show HEAD~1:CLI/programa.swift(pre-change), reconstructed the expected dedented output, and diffed it against the new table-generatedprograma --help/programa helpoutput from the built binary — identical, after fixing one ordering bug caught by the diff (new-workspaceandsshwere transposed relative to the original help order; fixed to match).--help,help, unknown-command fallback (prints usage +Unknown command: ..., exit 1),--version, andpingagainst a nonexistent socket (clean connection error) all behave as expected.tests_v2/) and E2E/UI tests, per repo policy — these run in CI, not locally.