From 1270 Lines to State Machines: Rebuilding a Developer CLI
We had 28 state machines in the frontend. The developer CLI had zero.
This site's browser code uses pure, testable state machines for everything: SPA navigation, scroll spy, theme switching, copy feedback, sidebar resizing. Factory functions. Callback injection. Guard clauses. No global state. 28 of them in src/lib/, each under 100 lines, each independently testable.
Meanwhile, the developer workflow tool — the CLI that builds, tests, serves, audits, commits, and deploys the site — was a single 1270-line file with 6 mutable globals, nested wizard state tracked through booleans, and zero unit tests.
Same developer. Same project. Two completely different architectures.
This series tells the story of bringing the frontend's state machine pattern to the CLI — and what happened when we did.
The Journey
Part I: The Monolith That Worked — Why a 1270-line file with
waitingForBuildMenubooleans was fine until it wasn't.Part II: "We Already Solved This 28 Times" — The moment we realized the frontend's state machine pattern was the answer to the CLI's architecture problem. And why we didn't use xstate.
Part III: Four Machines Replace Six Globals — Building
runner-machine,server-machine,tui-machine, andwizard-machinewith mermaid state diagrams, guard clauses, and before/after diffs.Part IV: 81 Tests, Zero Terminals — The
ConsoleIOinterface, mock callbacks, and how we test a full CLI without spawning a single process.Part V: Commander.js as a Thin Shell — Pure functions, DI composition root,
.workrc.jsonconfig, and why the CLI framework is the thinnest layer.Part VI: Three Modes, One Machine — Panels, Keys, and Command mode all driven by the same
tui-machine. Renderers as pure functions. Docker-build style output.
Impact
| Metric | Before | After |
|---|---|---|
| Entry point | 1270 lines | 30 lines |
| Mutable globals | 6 | 0 |
| Files | 1 | 24 source + 15 test |
| Max file size | 1270 lines | < 150 lines |
| Unit tests | 0 | 81 |
| Test duration | untestable | 600ms |
| Processes spawned in tests | N/A | 0 |
--help |
60 hand-written lines | Auto-generated at every level |
The same developer who architected the frontend's 28 state machines applied the exact same pattern to rebuild the CLI tooling. No framework switch. No new library. Just the same TypeScript pattern, in a different domain.