21 KiB
21 KiB
| summary | read_when | ||
|---|---|---|---|
| Commander CLI parsing redesign for Peekaboo |
|
Commander Migration Plan
1. Objectives
- Eliminate the vendored Apple ArgumentParser fork and its maintenance burden.
- Keep the ergonomics of property-wrapper-based command definitions while ensuring every command executes inside our
CommandRuntimeflow. - Centralize command metadata so docs, CLI help, agents, and regression tests share one source of truth.
- Add end-to-end CLI regression tests that shell out to the
peekaboobinary viaswift-subprocess.
2. Target Architecture
- Commander module (new Swift target shared by PeekabooCLI, AXorcist, and Tachikoma examples):
CommandDescriptortree representing commands, options, flags, and arguments.- Property wrappers (
@Option,@Argument,@Flag,@OptionGroup) that simply register metadata with a localCommandSignaturerather than parsing on their own. - Lightweight
ExpressibleFromArgumentprotocol (replacing Apple’sExpressibleByArgument) with conformances for primitives, enums, and Peekaboo types likeCaptureMode/CaptureFocus. CommandRouterinspired by Commander.js: tokenizes argv, traverses the descriptor tree, populates property wrappers, and dispatches to the appropriate command type.
- Runtime integration:
- Each command continues to conform to
AsyncRuntimeCommand; the router constructs the command, injects parsed values, createsCommandRuntime, and callsrun(using:)on the main actor. - Errors flow through existing
outputErrorhelpers; Commander emitsCommanderErrorcases (missing argument, unknown flag, etc.) that we map toPeekabooErrorIDs for consistent JSON output. - Help text uses the existing
CommandDescriptionbuilders already embedded in every command file, plus metadata fromCommandSignatureto display options/flags in Commander’s help output.
- Each command continues to conform to
- Shared metadata:
CommandRegistry(already inCLI/Configuration) feeds Commander so subcommand lists stay synchronized between CLI, docs, and agents.- Commander exposes a
describe()API sopeekaboo tools/peekaboo learnand MCP metadata reuse the same structured definitions.
3. Parsing Features & API Surface
- Options/flags: retain existing DSL (e.g.,
@Option(name: .customShort("v"), parsing: .upToNextOption)) and support the handful of strategies we actually use (singleValue,upToNextOption,remaining,postTerminator). - Negated flags: replicate ArgumentParser’s
inversionbehavior by allowing.prefixedNo/.prefixedEnableDisablenaming; Commander auto-generates--no-fooaliases when requested. - Option groups: Commander honors nested
@OptionGroupdeclarations, merging grouped options into help output exactly like Commander.js’.addOption(new Command())pattern. - Validation: property wrappers can throw
CommanderValidationError(message:)from theirloadhooks; router surfaces that as a user-facing error (with JSON codeINVALID_INPUT). - Custom parsing:
@Argument(transform:)keeps working by invoking the supplied closure once Commander has the raw string. - Standard runtime options:
CommandSignature.withStandardRuntimeFlags()injects-v/--verbose,--json(alias:--json-output), and--log-level <trace|verbose|debug|info|warning|error|critical>for every command so tooling can toggle logging consistently.
4. Execution Flow
runPeekabooCLI()builds the rootCommander.ProgramusingCommandRegistry.entriesand hands itCommandRuntime.Factoryfor runtime injection.- Commander parses
ProcessInfo.processName/CommandLine.arguments(minus the executable path) and resolves the command chain. - Parsed values hydrate the command instance via reflection (mirroring how Commander.js assigns option results).
- Commander constructs
CommandRuntimefromCommandRuntimeOptionsand callsrun(using:). - On failure, Commander prints Peekaboo-formatted errors; on
--help, it renders the curated help text while skipping execution.
5. Implementation Steps
- Bootstrap Commander module
- Create
Sources/Commanderwith descriptors, parser, tokenizer, and property wrappers. - Provide adapters for
@Option,@Flag,@Argument,@OptionGroup,@OptionGroup(title:), and@OptionGroup(help:). - Port the small helper protocols/types we rely on (
ExpressibleFromArgument,MainActorCommandDescription) directly into Commander and delete the last traces of the ArgumentParser compatibility shim.
- Create
- Wire PeekabooCLI
- Swap
import ArgumentParser->import Commanderacross CLI sources. - Update
Peekabooroot command to register subcommands via CommandRegistry instead of Apple’sCommandDescriptionarray. - Replace uses of
ArgumentParser.ValidationError/CleanExitwith Commander equivalents. - Remove Apple-specific extensions such as
MainActorParsableCommandsince Commander handles main-actor dispatch natively.
- Swap
- Update other packages
- Point AXorcist CLI (
AXorcist/Sources/axorc/AXORCMain.swift) and Tachikoma example CLIs at Commander; ensure they keep their current UX. - Delete
Vendor/swift-argument-parserand remove the dependency from every affectedPackage.swift(Peekaboo, AXorcist, Tachikoma, Examples).
- Point AXorcist CLI (
- Testing
- Add Swift Testing target
CommanderTestsfor the module itself (unit tests for option parsing, error cases, help rendering). - Add CLI regression tests under
Apps/CLI/Tests/CLIRuntimeTeststhat invoke the built binary viaswift-subprocess. Cover:peekaboo list apps --json-outputpeekaboo see --mode screen --path /tmp/test.png --json-output- Failure (unknown flag) and
--helpoutput snapshot checks.
- Ensure tests run in CI via tmux wrapper per AGENTS.md instructions.
- Add Swift Testing target
- Cleanup & documentation
- Remove the vendored folder, stale docs (
docs/argument-parser.md,docs/swift-argument-parser.mdalready deleted), and update any README/learn outputs referencing Apple’s parser. - Update
CommandRegistry/learncommand to mention Commander as the parsing layer.
- Remove the vendored folder, stale docs (
6. Rollout & Verification
- Build + run targeted CLI commands locally to confirm output matches current behavior (including JSON formatting and verbose logging).
- Re-run long tmux suites (
swift build, targetedswift testsubsets) to catch concurrency regressions. - Monitor the new CLI subprocess tests in CI; they become the primary guardrail against future “help-only” regressions.
- Document Commander’s API in-code (
Sources/Commander/README.mdor inline doc comments) so future commands know how to declare options.
7. Open Questions / Follow-Ups
- Do we need compatibility shims for third-party tools that still import Apple’s
ArgumentParser? If yes, expose a tiny transitional module that re-exports Commander types under the old names until everything migrates. - Should Commander expose a programmatic API for MCP/agents to request command metadata? (Likely yes; we can extend
CommandRegistry.definitions()to serialize Commander descriptors.) - Investigate reusing Commander for other binaries (e.g.,
axorc,tachikoma) once PeekabooCLI migration is stable.
With this plan, we fully control CLI parsing, remove the Swift 6 actor headaches, and finally have end-to-end tests that ensure the CLI actually executes commands instead of falling back to help text.
8. Implementation Stages
-
Module Scaffolding
- Create
Sources/Commandertarget with the foundational types: tokeniser, command descriptors, property wrappers, minimal dispatcher, andExpressibleFromArgument. - Wire Commander into
Package.swiftfiles (PeekabooCLI, AXorcist, Tachikoma) alongside existing dependencies while still leaving ArgumentParser in place so the old commands keep compiling. - Add placeholder unit tests (
CommanderTests) that exercise the tokenizer and descriptor builder. - ✅ Status (Nov 11, 2025): target, property wrappers, and initial signature tests are in place; Commander builds independently.
- Create
-
Dual-Wire PeekabooCLI
- Introduce an adapter layer that lets existing commands register with Commander (via
CommandRegistry) while still compiling against ArgumentParser property wrappers. - Update the CLI entry point (
runPeekabooCLI) to invoke Commander first; if parsing succeeds, run the command via CommandRuntime; otherwise temporarily fall back to ArgumentParser for unported commands. - Build the first concrete subcommand (e.g.,
RunCommand) purely on Commander to validate the flow end-to-end.- 🔄 In progress (Nov 11, 2025):
CommanderRegistryBuildernow emits both descriptors and normalized summaries solearn/commanderno longer import Commander (no more@OptionGroupcollisions), the diagnostics command prints those summaries, CommanderPilot runspeekaboo learn,peekaboo sleep,peekaboo clean, andpeekaboo runvia Commander, and the entire CLI builds cleanly again (swift build --package-path Apps/CLI) after tagging everyAsyncRuntimeCommandconformance with inline@MainActorand moving the protocol’srun(using:)requirement under@MainActor.CommanderCLIBinderexposesCommanderBindableCommand;SleepCommand,CleanCommand,RunCommand,ImageCommand,SeeCommand,ToolsCommand,list windows,list menubar, andpermissions(status + grant) all conform so Commander hydrates positional arguments plus their@Flag/@Optioninputs automatically, theCommanderBinderTeststarget covers success/error paths for each, and a newCLIRuntimeTeststarget (swift-subprocess) now runs thepeekaboo commanderandpeekaboo list windowsflows as an end-to-end binary smoke test. Next focus: keep rolling the binder helpers across CLI commands and extend the subprocess regression suite. - 🔄 Update (Nov 11, 2025 PM):
CommandDescriptornow tracks nested subcommand metadata (including default subcommands) andProgram.resolvereturns the full command path soCommanderRuntimeRoutercan hydrate the correctParsableCommandtype even for chains likepeekaboo list windows.CommandParserlearned proper--terminator semantics plus a catch-all.remainingsink so tail arguments no longer get swallowed by the preceding option. Commander summaries/diagnostics now emit hierarchical trees, and we have tmux-gatedswift test --package-path Apps/CLI --filter ParserTests+--filter CLIRuntimeSmokeTestslogs to prove both the Commander unit suite and the subprocess smoke tests pass with the new behavior. - 🔄 Update (Nov 11, 2025 evening): Every
windowsubcommand (close/minimize/maximize/move/resize/set-bounds/focus/list) plus theclick,type,press,scroll,drag,hotkey, andswipeinteraction commands now conform toCommanderBindableCommand. The binder seeds freshWindowIdentificationOptions/FocusCommandOptionsinstances so the OptionGroup wrappers stay happy, and theCommanderBinderTestssuite gained coverage + regression errors for those bindings. tmux logs:/tmp/commander-binder.logfor binder tests,/tmp/commander-tests.logfor Commander.Parser tests. - 🔄 Update (Nov 11, 2025 late PM): Added
CommanderSignatureProvidingso commands can describe their option/flag metadata without relying on Apple’s wrappers.image,see, everylistsubcommand,click,type,press,scroll,hotkey,move,drag,swipe,menu(click/click-extra/list/list-all),app(launch/quit/hide/unhide/switch/list/relaunch),permissions,tools,space(list/switch/move-window),dialog(click/input/file/dismiss/list),window(close/min/max/move/resize/set-bounds/focus/list), and the shared option groups (FocusCommandOptions,WindowIdentificationOptions) now publish full Commander signatures.CommanderRegistryBuilderflattens these option groups before emitting descriptors, and new binder tests assert thatProgram.resolve()understands real-world invocations across screenshot/vision/list/system/interaction workflows (peekaboo window focus --app Safari …,peekaboo dialog input --text …,peekaboo space move-window …, etc.). Commander is effectively parsing the entire CLI surface; remaining work is wiring MCP/agent-specific commands before removing the ArgumentParser fallback.
- 🔄 In progress (Nov 11, 2025):
-
Full Command Migration
- Convert every command in
Apps/CLIto use Commander wrappers exclusively; remove the fallback path once parity is confirmed. - Port AXorcist CLI and Tachikoma examples to Commander.
- Delete the vendor
swift-argument-parserfolder and scrub all imports/retroactive conformances referencing Apple’s APIs.
- Convert every command in
-
Regression Testing & Cleanup
- Add
swift-subprocess-based CLI regression tests that run the built binary to cover happy-path and failure-path scenarios. ✅CLIRuntimeTests(Nov 11, 2025) shells out topeekaboo commanderandpeekaboo list windowsto exercise the installed binary. - Expand Commander unit tests to include error cases, help rendering, and option-group behaviors.
- Run tmux-gated
swift build/swift testsuites, fix any stragglers, and document the migration status in AGENTS.md / release notes.
- Add
9. Progress Snapshot (Nov 11, 2025)
- Hierarchy-aware descriptors: Commander now builds a full command tree (root commands + subcommands + default-subcommand pointers).
Program.resolvewalks the tree, records the command path, and surfaces specificCommanderProgramErrorcases for missing/unknown subcommands. - Runtime routing:
CommanderRuntimeRouterreuses the resolved path to locate the rightParsableCommandtype, so downstream binders can hydrate nested commands without guessing. The diagnostics JSON mirrors this hierarchy forpeekaboo commander/peekaboo learnconsumers. - Parser polish: The tokenizer no longer feeds terminator tails into the preceding
.upToNextOption, and any signature that declares a.remainingoption automatically receives the--tail (matching how we model “implicit rest” arguments in CLI commands). - Binder coverage:
CommanderCLIBindernow hydrateswindow close/minimize/maximize/move/resize/set-bounds/focus/listplus the entire interaction/system surface:click/type/press/scroll/drag/hotkey/swipe/pointermove, menu (menu click/click-extra/list/list-all), Dock (dock launch/right-click/list/hide/show), dialog (dialog click/input/file/dismiss/list), high-levelappcommands (launch/quit/hide/unhide/switch/list/relaunch),spacemanagement (space list/switch/move-window),permissions(CLI + agent), and the fullconfigsuite (init/show/edit/validate/set-credential/add-provider/list-providers/test-provider/remove-provider/models-provider). Commander now owns essentially the entire CLI surface; the remaining work is wiring the agent/MCP command trees and flipping the runtime to prefer Commander end-to-end (with tmux logs in/tmp/commander-binder.logdemonstrating 55 passing binding tests). - Signature providers:
CommanderSignatureProvidinglets commands publish their metadata explicitly. The current adopters spanimage,see, alllistsubcommands, interaction verbs (click,type,press,scroll,hotkey,move,drag,swipe), system controllers (menu,app,window,dialog,space,permissions,tools,dock), plus the shared option groups (FocusCommandOptions,WindowIdentificationOptions). Every option/flag (app/pid/window-title/include-details/annotate/query/session/delay/tab/count/hold/direction/amount/modifiers/server filters/focus flags/etc.) now has Commander metadata, and the registry flattens these option groups so flags like--no-auto-focusand--space-switchparse correctly. Next up: cover MCP + agent entry points and begin routing the CLI through CommanderPilot so we can delete ArgumentParser entirely. - Tests executed:
swift test --package-path Apps/CLI --filter ParserTests(Commander unit suite, log/tmp/commander-tests.log),swift test --package-path Apps/CLI --filter CommanderBinderTests(log/tmp/commander-binder.log), andswift test --package-path Apps/CLI --filter CLIRuntimeSmokeTests(log/tmp/cli-runtime.log) all run via tmux. - Outstanding: Map the remaining CLI commands onto
CommanderBindableCommand, teach CommanderPilot (or the main entry point) to route additional command families through Commander, and start deleting the ArgumentParser vendored tree once parity + subprocess coverage exists for every command.
Progress 2025-11-11 – Build Stabilization & Tests
- Dropped the
Sendableconstraint from Commander’s property wrappers andCommanderParsableso@MainActorCLI helper structs (e.g.,WindowIdentificationOptions,FocusCommandOptions) can register metadata without tripping#ConformanceIsolation. ConditionalSendableextensions keep the wrappers sendable when possible. - Exposed
CommandParserpublicly and pointedParsableCommand.parse(_:)at Commander so legacy unit tests keep working without reviving ArgumentParser. This also unlockedToolsCommandTests, which now readCommandDescriptiondirectly instead of calling the deletedhelpMessage()helpers. - Fixed
SeeCommand’s capture switch to cover the.multiand.areacases Commander now parses, preventing fatal fallthroughs, and alignedWindowIdentificationOptionsbindings with the shared metadata helpers. swift build --package-path Apps/CLInow succeeds from a clean tree, andswift test --package-path Apps/CLI --filter CommanderBinderTestspasses (see session log timestamp 20:34 local); CommanderBinder continues to verify ~70 binding scenarios after the refactor.- Added
executePeekabooCLI(arguments:)so in-process automation tests can exercise the Commander runtime without resurrectingparseAsRoot.InProcessCommandRunnernow routes through that helper, and the same error-printing path as the shipping CLI is reused for test assertions. - Reintroduced
helpMessage()via a lightweightCommandHelpRendererthat inspectsCommandSignaturemetadata, so the automation suites (List/MCP/Tools) can keep verifying help content purely through Commander descriptors. - Revived the
peekabooTestssuites (ClickCommandAdvancedTests) by removing their*.disabledsuffixes and updating them to use Commander-era helpers; they now validate command metadata, parsing, and help output without importing ArgumentParser. swift build --package-path Apps/CLInow succeeds from a clean tree, andswift test --package-path Apps/CLI --filter CommanderBinderTestspasses (see session log timestamp 20:34–20:41 local); CommanderBinder continues to verify ~70 binding scenarios after the refactor.scripts/run-commander-binder-tests.shtees every CommanderBinder test run into/tmp/commander-binder.log, adding a UTC-stamped header before appending the fresh output so investigators can diff multiple runs without re-running the suite.- With those suites green again, MCP/agent coverage now spans: (1) binder-level resolution tests for
serveplus Commander metadata snapshots viapeekabooTests, and (2) CLI automation helpers hittingexecutePeekabooCLI. Once we confirm no other modules import ArgumentParser, we can deleteVendor/swift-argument-parserand scrub the dependency graph. CLIRuntimeSmokeTestsnow shell out via swift-subprocess forpeekaboo list apps --json-output,peekaboo list windows --json-output(error path),peekaboo sleep, andpeekaboo mcp --help. That gives us fast end-to-end coverage that Commander is powering the MCP command surfaces without pinging live MCP servers.- Commander is now a standalone Swift package under
/Commander. Apps/CLI, AXorcist, Tachikoma (including Examples and Agent CLI), and PeekabooExternalDependencies all depend on it instead of the vendored swift-argument-parser tree. The vendor folder has been deleted. - New Commander unit tests (
TokenizerTests,CommandDescriptionTests) cover single-letter options, combined flags, the--terminator, and regression coverage for the metadata builders. CLIRuntimeSmokeTestsgained MCP help coverage and agent dry-run scenarios so we exercise Commander on those code paths without real credentials.
Next up (owner: whoever picks up the baton):
- Harden retroactive conformances. The CLI emits warnings for the Commander argument conformances (
CaptureMode,ImageFormat,CaptureFocus). Either adopt Swift’s@retroactivesupport once it lands or find another way (e.g., intermediate wrapper types) to silence the warnings. - Surface Commander as a documented dependency. Update AGENTS.md/other guides to call out the new
/Commanderpackage (partly done) and describe how other repos should depend on it. - Broaden subprocess coverage. Add additional swift-subprocess scenarios for MCP
serve(stdio failure) and agent session listing/resume so CI keeps exercising those flows without external credentials.