-
Notifications
You must be signed in to change notification settings - Fork 0
systems cli
The garnish CLI handles everything outside a session: onboarding (garnish init), status, the unlock escape hatch, and the doctor diagnostic. It is split into dependency-injected command cores that run as fast unit tests, and a single composition root that binds those cores to the filesystem, child processes, Bun.build, and stdin. The durable state the CLI reads and writes lives under the Garnish-owned agent dir as pre-serialized JSON plus an append-only event log.
src/cli/
index.ts command cores (status/quest/unlock/doctor) + main dispatcher
real.ts composition root: runGarnish binds cores to the machine
init.ts onboarding wizard (garnish init)
state.ts loadInstalledState + createFsEventStore
src/bin.ts executable entrypoint (bun src/bin.ts or the {root}/bin/garnish shim)
| Abstraction | Where | Role |
|---|---|---|
runGarnish |
src/cli/real.ts |
Composition root. Parses argv, resolves the Garnish root, and dispatches to init, doctor, or main. |
CliDeps |
src/cli/index.ts |
Dependency slice for status/quest/unlock: graph, quests, store, now, catalog, runtimePaths, gateEffects. |
ProgressionStore |
src/cli/index.ts |
readEvents/appendEvents interface; synchronous or async. Backed by events.jsonl in real runs. |
DoctorDeps |
src/cli/index.ts |
Dependency slice for doctor: runtimeInstalled, reportedVersion, isolatedConfigPresent. |
CommandOutcome |
src/cli/index.ts |
{ text, exitCode } returned by every command core. |
loadInstalledState |
src/cli/state.ts |
Reads graph.json, quests.json, state.json from {agent_dir}/garnish/ and returns the parsed graph, quests, and events path. |
createFsEventStore |
src/cli/state.ts |
Synchronous fs-backed ProgressionStore over events.jsonl. |
src/bin.ts is the executable entrypoint. When run as the main module it calls runGarnish(process.argv.slice(2)), prints outcome.text, and exits with outcome.exitCode. runGarnish in src/cli/real.ts resolves the Garnish root ($GARNISH_ROOT, else ~/.garnish) and the repo root, then branches on argv[0]:
-
initdelegates torunInit, which builds real effects and callsinitCommand(see init wizard). -
doctorbuildsDoctorDepsfromruntimePathsand callsdoctorCommand. -
status,quest,unlock,cheatload installed state vialoadInstalledState, build aCliDepswithcreateFsEventStore, and handargvtomain(see CLI commands). - Anything else prints
usage()with exit code 2.
graph TD
BIN["src/bin.ts"] -->|"import.meta.main"| RUN["runGarnish<br/>src/cli/real.ts"]
RUN -->|"argv[0] == init"| INIT["initCommand<br/>src/cli/init.ts"]
RUN -->|"argv[0] == doctor"| DOC["doctorCommand"]
RUN -->|"status / quest / unlock / cheat"| MAIN["main dispatcher<br/>src/cli/index.ts"]
MAIN --> STATUS["statusCommand"]
MAIN --> QUEST["questCommand"]
MAIN --> UNLOCK["unlockCommand"]
DOC --> AD["adapter runtimePaths + handshake"]
The two-layer split keeps the command cores pure. src/cli/index.ts imports only adapter, core, progression, and verifier types, so statusCommand, questCommand, unlockCommand, doctorCommand, and main run against fakes in unit tests. src/cli/real.ts is the only file that touches node:fs, node:child_process, node:readline, and Bun.build.
The durable state layer in src/cli/state.ts is synchronous on purpose. loadInstalledState reads graph.json, quests.json, and state.json with readFileSync and validates each against a Zod schema (GraphFileSchema, QuestSchema, StateFileSchema). createFsEventStore returns a store whose readEvents splits events.jsonl into lines and parses each with ProgressionEventSchema, and whose appendEvents mkdirs the parent and appends one JSON object per line. The same store backs both the CLI and the bundled extension, so garnish status and the in-session HUD read the same source of truth.
On a successful init, src/cli/real.ts also writes a bin/garnish shim into the Garnish root so garnish … works inside the learner's session (where the extension's command probe resolves {root}/bin/garnish) and from any shell without PATH games.
-
Adapter (
src/adapter/):runtimePathscomputes the agent dir;handshakebacksdoctor;renderGateConfigandwriteGateConfigbackunlock's config regeneration;ensureRuntimeandcreateLaunchSpecbackinit. -
Loader (
src/loader/):loadPackvalidates each copied pack ininit. -
Progression (
src/progression/):foldEventsturnsevents.jsonlinto the state every command renders. -
Extension (
src/extension/):bundleExtensioninsrc/cli/real.tsrunsBun.buildonsrc/extension/entry.tsand writes the bundle into the agent dir duringinit.
To add a CLI command, add a case to main() in src/cli/index.ts plus a handler function that takes a CliDeps slice, then teach runGarnish in src/cli/real.ts to recognize the new verb. To change how commands bind to the machine (fs, child processes, stdin, bundling), edit src/cli/real.ts, the only composition root. The durable file format lives in src/cli/state.ts; change the schemas there and init's writes must follow.
| File | Role |
|---|---|
src/bin.ts |
Executable entrypoint; calls runGarnish and exits with the outcome. |
src/cli/real.ts |
Composition root; runGarnish, real effects, extension bundling, shim write. |
src/cli/index.ts |
Command cores, main dispatcher, usage. |
src/cli/init.ts |
Onboarding wizard; see init wizard. |
src/cli/state.ts |
loadInstalledState, createFsEventStore, file schemas. |
See init wizard for the onboarding flow and CLI commands for the status/quest/unlock/doctor cores. The adapter the CLI composes is documented in systems/adapter, and the progression engine that produces the state it renders is in systems/progression.