Parent
#32
What to build
factory parses its own arguments with effect/unstable/cli instead of four hand-rolled parsers.
Every existing command keeps its current surface — init, serve, start, runs, log, run,
with the same flags, the same defaults, the same exit codes and the same $FACTORY_URL fallback —
but help text is generated rather than hand-maintained, flag values are validated before they reach
a command body, and the argv layer becomes testable.
Today's parsers disagree with each other in ways that are defects, not style: parseFlags walks
argv at stride 2 and so structurally cannot represent a boolean flag (one --watch and every
subsequent pair is misread), which is why factory start needed a second, different parser;
init's flags are parsed inline in the import.meta.main block; and serve passes an unvalidated
Number(portRaw) straight to Bun.serve, so --port abc becomes NaN. Because usageError
calls process.exit inside a parser, none of this can be tested, and every current CLI test
bypasses argv and calls the command functions with structured options.
The command bodies themselves stay as they are — plain async functions — wrapped at the boundary.
This ticket does not change what any command does.
See ADR 0009 §5 (docs/adr/0009-imperative-workflows-effect-boundary.md) for why the CLI is the
one place the boundary moves first.
Acceptance criteria
Blocked by
None (can start immediately).
Parent
#32
What to build
factoryparses its own arguments witheffect/unstable/cliinstead of four hand-rolled parsers.Every existing command keeps its current surface —
init,serve,start,runs,log,run,with the same flags, the same defaults, the same exit codes and the same
$FACTORY_URLfallback —but help text is generated rather than hand-maintained, flag values are validated before they reach
a command body, and the argv layer becomes testable.
Today's parsers disagree with each other in ways that are defects, not style:
parseFlagswalksargv at stride 2 and so structurally cannot represent a boolean flag (one
--watchand everysubsequent pair is misread), which is why
factory startneeded a second, different parser;init's flags are parsed inline in theimport.meta.mainblock; andservepasses an unvalidatedNumber(portRaw)straight toBun.serve, so--port abcbecomesNaN. BecauseusageErrorcalls
process.exitinside a parser, none of this can be tested, and every current CLI testbypasses argv and calls the command functions with structured options.
The command bodies themselves stay as they are — plain async functions — wrapped at the boundary.
This ticket does not change what any command does.
See ADR 0009 §5 (
docs/adr/0009-imperative-workflows-effect-boundary.md) for why the CLI is theone place the boundary moves first.
Acceptance criteria
USAGEstring works identically, including--watch,$FACTORY_URL, and the--clone/--git-name/--git-emailgroup--portand any other numeric flag reject a non-numeric value with a clear error instead of passingNaNdownstreamUSAGEconstant is goneprocess.exitcall remains inside a parsing functionbun run checkpassesBlocked by
None (can start immediately).