Skip to content

refactor: single command-descriptor table for the CLI#76

Merged
arzafran merged 1 commit into
mainfrom
refactor/ct1-command-descriptor-table
Jul 8, 2026
Merged

refactor: single command-descriptor table for the CLI#76
arzafran merged 1 commit into
mainfrom
refactor/ct1-command-descriptor-table

Conversation

@arzafran

@arzafran arzafran commented Jul 8, 2026

Copy link
Copy Markdown
Member

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

  • Adds CommandDescriptor (name(s), help line(s), execute closure) and CommandContext (the per-invocation bundle a command needs: args, socket client, flags).
  • commandDescriptors() is now the single source of truth, read from three places:
    • Dispatch: run() looks up the table by command name and calls its execute closure, replacing the old ~75-case switch.
    • Help: usage()'s Commands: section is generated by iterating the table in order, instead of a hand-maintained literal list.
    • Unknown-command handling: falls through to the existing "Unknown command" message when no table entry (with a real executor) matches.
  • Coverage: 108 command names in the table.
    • 52 have their logic inlined directly in a closure (these were already one-shot switch-case bodies; only their container changed).
    • 45 call into existing handler functions unchanged (runSSH, runTreeCommand, runBrowserCommand, the 23-name tmux-compat group, etc.) — same functions, now reachable via a descriptor row instead of a case.
    • 11 are pre-connection specials (version, welcome, shortcuts, feedback, themes, claude-teams, omo/omx/omc, codex, remote-daemon-status) — these still execute from run()'s existing pre-socket-connection branch (some, like version/welcome, must work even when Programa isn't running), but now have a table row purely so usage() has one place to read their help line from.
  • 20 commands were already undocumented in the old help text (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 legacy open-browser/navigate/browser-*/get-url/focus-webview/is-webview-focused aliases). 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.
  • Non-goal (explicit, per the finding): does not unify with the app-side v2 method switch in Sources/TerminalController.swift (processV2Command) — that's server-side method routing with different concerns, left untouched.
  • subcommandUsage() / dispatchSubcommandHelp() (the deep per-command --help text switch) are untouched — that mechanism is outside the three places named in the original finding (which called out the dispatch switch and the free-text usage()/help strings specifically); unifying it is a reasonable follow-up but out of scope here.

Test Plan

  • xcodebuild -scheme programa-cli — green
  • xcodebuild -scheme programa — green
  • xcodebuild -scheme programa-unit build-for-testing — green
  • Help-text diff: captured usage()'s literal text from git show HEAD~1:CLI/programa.swift (pre-change), reconstructed the expected dedented output, and diffed it against the new table-generated programa --help / programa help output from the built binary — identical, after fixing one ordering bug caught by the diff (new-workspace and ssh were transposed relative to the original help order; fixed to match).
  • Manual smoke test against the built CLI binary: --help, help, unknown-command fallback (prints usage + Unknown command: ..., exit 1), --version, and ping against a nonexistent socket (clean connection error) all behave as expected.
  • Not run: python socket tests (tests_v2/) and E2E/UI tests, per repo policy — these run in CI, not locally.

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.
@arzafran arzafran merged commit 7bffe83 into main Jul 8, 2026
15 of 18 checks passed
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.
@arzafran arzafran deleted the refactor/ct1-command-descriptor-table branch July 10, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant