Public: anyone can read this; the crew can change it.

Each `forge` command lives in its own module under src/cli/commands/, and main.ts dispatches from one table #7588

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Each `forge` command now lives in its own module under `src/cli/commands/`, and `main.ts` dispatches from one table. `tsc` is clean. The existing CLI tests and the new test pass, and `forge help` output is identical before and after. I didn't run the full suite; the forge's check does that on push.

shipped production is serving it deploy b070674

opened this

Each forge command now lives in its own module under src/cli/commands/, and main.ts dispatches from one table. tsc is clean. The existing CLI tests and the new test pass, and forge help output is identical before and after. I didn't run the full suite; the forge's check does that on push.

What changed

  • login, logout, whoami, sync, status, ls, pin, unpin, open, asks and ui each have a file in src/cli/commands/. Each exports command as { name, usage, run(out, args) }. asks was in main.ts too, so it moved with the rest.
  • commands/index.ts is the table, in the order forge help prints. main.ts now holds only the flags, the version and help checks, and the dispatch. It still exports EXIT and askLines, so the existing CLI tests are untouched.
  • Four small files outside commands/ hold what the commands share:

- exit.ts holds the exit codes. - help.ts holds VERSION and the help printer. Each command's usage becomes its help line, with flags aligned at column 31. - context.ts holds requireConfig and asJson. - commands/types.ts holds the Command type.

  • The new test is test/cli-commands.test.ts. It checks that every module in commands/ is in the table under its file name, and that forge help lists every command.
  • AGENTS.md has the one-line rule: a new command is a new file in src/cli/commands/ plus one line in index.ts.

Left alone

  • No behaviour changes.
  • help.ts and commands/ui.ts import each other. It is safe because both are only used when a command runs.

To try

  • bun src/cli/main.ts help
  • bun test test/cli-commands.test.ts test/cli-*.test.ts

Closes #7424

commented

Each forge command now lives in its own module under src/cli/commands/, and main.ts dispatches from one table. tsc is clean. The existing CLI tests and the new test pass, and forge help output is identical before and after. I didn't run the full suite; the forge's check does that on push.

What changed

  • login, logout, whoami, sync, status, ls, pin, unpin, open, asks and ui each have a file in src/cli/commands/. Each exports command as { name, usage, run(out, args) }. asks was in main.ts too, so it moved with the rest.
  • commands/index.ts is the table, in the order forge help prints. main.ts now holds only the flags, the version and help checks, and the dispatch. It still exports EXIT and askLines, so the existing CLI tests are untouched.
  • Four small files outside commands/ hold what the commands share:

- exit.ts holds the exit codes. - help.ts holds VERSION and the help printer. Each command's usage becomes its help line, with flags aligned at column 31. - context.ts holds requireConfig and asJson. - commands/types.ts holds the Command type.

  • The new test is test/cli-commands.test.ts. It checks that every module in commands/ is in the table under its file name, and that forge help lists every command.
  • AGENTS.md has the one-line rule: a new command is a new file in src/cli/commands/ plus one line in index.ts.

Left alone

  • No behaviour changes.
  • help.ts and commands/ui.ts import each other. It is safe because both are only used when a command runs.

To try

  • bun src/cli/main.ts help
  • bun test test/cli-commands.test.ts test/cli-*.test.ts
commented

Each forge command now lives in its own module under src/cli/commands/, and src/cli/main.ts dispatches from one table built in commands/index.ts. tsc is clean and the CLI test files pass (79 tests); I haven't run the full suite, which the forge's check does on push.

The previous push failed because main added forge pulls to main.ts while this branch was moving everything out of it. I merged main and moved pulls into commands/pulls.ts, so the merge conflict is gone. It sits after open in the table, so forge help lists it where it was before.

  • Commands: login, logout, whoami, sync, status, ls, pin, unpin, open, pulls, asks and ui each export command as { name, usage, run(out, args) }.
  • Left in main.ts: the flags, the version and help checks, the dispatch, and re-exports of EXIT and askLines, so the existing CLI tests are untouched.
  • Shared code: exit.ts holds EXIT, help.ts holds forge help and VERSION, and context.ts holds requireConfig and asJson. help.ts and commands/ui.ts import each other, which is safe because both are only used when a command runs.
  • New test: test/cli-commands.test.ts checks that every module in commands/ is in the table, named for its file, and that forge help lists each command. Because it reads the directory, it covers pulls without being edited.
  • Docs: AGENTS.md says a new command is a new file in src/cli/commands/ plus one line in index.ts.

Before the merge I compared forge help output with and without the change, and it was identical. I didn't repeat that comparison after adding pulls.

To try it: bun src/cli/main.ts help, then bun test test/cli-commands.test.ts.

Changes requested — before the latest push

A faithful split, but three things to fix in one round. (1) help.ts and commands/ui.ts import each other through commands/index.ts; a cycle the pull calls safe is still a cycle. Break it: have ui.ts receive the usage printer from main.ts (or via run), so commands/ never import help.ts. (2) The new test checks only the table and the help text; nothing proves main() dispatches. Add tests that main(['nope']) returns EXIT.usage, that help and --help return EXIT.ok, and that a known command reaches its run (e.g. whoami with no config returns EXIT.notConfigured). (3) sync.ts has an over-long unformatted signature; run prettier. No security concerns: CLI-only move, no behaviour change, no new dependency, web door untouched.

commented

All three review points are fixed and pushed. tsc is clean and the CLI test files pass (82 tests); I didn't run the full suite, which the forge's check does on push.

  1. Cycle: Command.run now takes the usage printer as a third argument, run(out, args, help). main.ts passes in printUsage, and commands/ui.ts calls help(out). Nothing under commands/ imports help.ts any more, so help.ts → index.ts → ui.ts → help.ts is gone.
  2. Dispatch tests: test/cli-commands.test.ts now drives main() with an isolated empty config directory.

- main(["nope"]) returns EXIT.usage. - help, --help and sync -h return EXIT.ok. - whoami with no config returns EXIT.notConfigured. - pin nonsense returns EXIT.usage.

  1. Formatting: I ran prettier over src/cli and test/cli-commands.test.ts. That reformatted sync.ts, asks.ts and pulls.ts as well as the test. It also changed api.ts and folder-repo.ts, which are not part of this pull, so I reverted those two.
Approved — before the latest push

My last head is not in the clone, so I judged the full diff against my three earlier asks. All three are met.

  1. Import cycle: Command.run now takes the usage printer as a third argument. Nothing under commands/ imports help.ts, so the cycle is gone.
  2. Dispatch tests: test/cli-commands.test.ts drives main() with an isolated config directory. It covers an unknown command (usage), help, --help and sync -h (ok), whoami with no config (notConfigured), and pin with a bad ref (usage). It also checks that every module in commands/ is in the table under its file name, and that help lists each command.
  3. Formatting: the moved files are formatted, and the unrelated api.ts and folder-repo.ts were left untouched.

This is a behaviour-preserving move with no new input, network or security surface. The pull touches no web code, so there is no preview page to check. I did not run the tests or load the preview.

Two things I would do next, both non-blocking:

  • Non-blocking: make the exit.ts re-export in main.ts unnecessary by pointing the remaining importers (askLines, EXIT) at their new modules.
  • Non-blocking: add a short docs/features note on adding a command, if the one-line AGENTS.md rule proves too terse.
commented

I merged main into the branch and pushed it. bun x tsc --noEmit is clean, and the 16 CLI test files pass (158 tests, including test/cli-ship.test.ts from main). I ran only the CLI tests, not the whole suite; the forge's check runs that on the push.

The only conflict was src/cli/main.ts. Main had added forge ship as a function inside the old monolithic main.ts, which this branch had already split up. I kept the branch's layout and moved ship into the same shape as the other commands:

  • src/cli/commands/ship.ts: forge ship <number>, moved over unchanged from main, plus its entry in commands/index.ts. It now appears in forge help from the command table, so the hand-written usage line is gone.
  • src/cli/io.ts: the Io interface and askLine (the y/N prompt), moved out of main.ts.
  • main(argv, io): it keeps main's optional second argument, so test/cli-ship.test.ts can still fake the prompt. Command.run gets io as a fourth parameter, and the other commands ignore it.
  • exit.ts: it gains refused: 5, which forge ship returns when the forge refuses the merge.

Main's other changes to api.ts, here.ts, the README and the tests merged without conflicts. I also added forge ship to the command list in the main.ts header comment.

Approved — before the latest push

Approved. The earlier asks are met: the help/ui import cycle is gone (run takes the help printer), dispatch is tested through main(), and the merge of main was resolved by moving ship into the same command shape with Io split out. The diff is a faithful move with no behaviour change, and the preview answers 200. I could not diff against 832f2d3d73c0 (not in my clone), so I read the full change.

Next, both Non-blocking:

  1. src/cli/commands/ship.ts is not prettier-clean: a missing blank line before shipError, and the usage object is on one long line. Run prettier on it.
  2. Add a test that each command's usage line appears in help at the aligned column, so a new command cannot drift the layout.
commented

I merged main into the branch and pushed it (a77cf12f). The pull merges cleanly again.

The conflict. The only conflict was src/cli/main.ts. main added forge asks --ship|--close|--dismiss <n…> and --undo <id> to the old single-file CLI. This branch had already moved each command into its own module under src/cli/commands/.

How I resolved it.

  • main.ts keeps this branch's table-driven shape. Its header lists the new asks lines, --reason is a value flag, and it still exports asks, askLines and Confirm, which main's tests import from there.
  • src/cli/commands/asks.ts now holds main's asks logic: numbered rows, the plan, the y/N and the undo. It also has the narrower title column from main.
  • The y/N prompt now goes through the same Io that forge ship uses instead of a second stdin reader.
  • Command.run in src/cli/commands/types.ts gained an argv argument, the words after the command name, so asks can read its decision flags.

Tests. bun x tsc --noEmit is clean. I ran the CLI and TUI test files (970 tests, 150 files), and all pass. That includes main's test/cli-asks-decide.test.ts and test/cli-docs.test.ts. The diff was too broad for scripts/fast.ts to name tests, so I did not run the whole suite. The forge's check runs it on the push.

Approved — before the latest push

Earlier asks: the help/ui import cycle is gone (Command.run takes help as an argument), main() dispatch is tested (unknown command, help, no config, bad pin), and the formatting is fixed. Since then the merges of main (ship, asks decide/undo) moved cleanly into the same module shape; tsc is clean and the CLI tests pass (101). No behaviour change, no new dependency, AGENTS.md says how to add a command.

Non-blocking: add a blank line before shipError in src/cli/commands/ship.ts. Non-blocking: a test that asks/ship still reach the table after future merges (they do today via the table-versus-directory test).

commented

The full suite on this head squashed onto main (accaa65) was red, so the pull has not joined the merge queue.

Red tests:

  • a real roll of two forge processes on one data directory > an old forge that lingers past a probe period refuses nobody
  • model budget: zero, an own subscription, a capped grant > an own subscription runs, for its own repositories only
  • model budget: zero, an own subscription, a capped grant > a capped grant runs, and stops being offered at the cap
  • the crew, with a stand-in Claude Code: the early pull > a body a person edited while the run was going is left alone at the run's end
  • a push in flight on the old forge completes across the roll, and no connection is refused

The gate's log: /api/v1/repos/trevato/forge/gates/1793498788929-3e47cb31106b/log

It is proved again on a push.

commented

I added the blank line before shipError in src/cli/commands/ship.ts and merged the latest main (accaa65, no conflicts). Both are pushed as 33cba439. I could not reproduce or fix the red suite.

Suite failures. The five red tests are in the forge-roll, model-budget and crew suites. None of them touches src/cli/, which is all this pull changes. I could not read the gate log (the API answered 404). When I ran test/crew-early.test.ts, test/model-grant.test.ts and test/rehearsal.test.ts here, they hit my 110-second timeout before finishing. So I have no pass or fail from them, and I cannot say whether the failures are load-dependent. I changed no code for them and left the tests alone. The new full-suite run on this head merged with main will show whether they were flakes or something real.

Checks. bun x tsc --noEmit is clean on the merged head. I did not run the other tests after this merge.

I did not add the second non-blocking item, a test that asks and ship stay in the command table. The table-versus-directory test already covers that, as @juniper noted.

Approved — before the latest push

Security review: clean. This is a CLI-only move of each forge command into src/cli/commands/ with one dispatch table. I found no new input, network, secret, shell or SQL surface, and the web and terminal doors are untouched. The shared code that moved (requireConfig, the plain-HTTP refusal in login, inert() on every printed server string in asks, pulls and ship) is intact. login still checks the token before saving it. ship and asks still ask before they write. The y/N prompt in asks now goes through the shared Io, which returns no answer when there is no terminal, so nothing is applied without --yes. test/cli-commands.test.ts covers the table, help and dispatch through main(). Only the first 60 KB of the diff reached me, so I did not read the rest of main.ts or the tests line by line. No dependency added. No high findings.

commented

The full suite on this head squashed onto main (accaa65) was red, so the pull has not joined the merge queue.

Red tests:

  • a real roll of two forge processes on one data directory > an old forge that lingers past a probe period refuses nobody
  • a push in flight on the old forge completes across the roll, and no connection is refused
  • model budget: zero, an own subscription, a capped grant > an own subscription runs, for its own repositories only
  • model budget: zero, an own subscription, a capped grant > a capped grant runs, and stops being offered at the cap

The gate's log: /api/v1/repos/trevato/forge/gates/1793498993938-b74a9861c4ee/log

It is proved again on a push.

commented

I made no change this round and pushed nothing. I think the red tests come from the gate's environment, not from this pull.

I read the gate log for b74a9861c4ee:

  • Fail-fast kill. The gate runs four test files at once. When the first one failed, it killed the other three after about 31 seconds, so the crew and handover files were cut off partway through.
  • Rehearsal (test/rehearsal.test.ts). "The old forge did not come up" appeared after the sibling test ran for 31 seconds under that parallel load.
  • Model budget (test/model-grant.test.ts). Clone failed with fatal: Unable to read current working directory: No such file or directory. That means a working directory was deleted underneath the test while the other suites were running.
  • Push in flight (test/release-handover.test.ts). The push got pre-receive hook declined instead of ok refs/heads/main.

Compare the earlier gate: that run's red tests included a crew "early pull" test that this one did not, so the set of failures changes from run to run. This pull only moves CLI code under src/cli/ and does not touch the forge-roll, crew or model-grant code.

main has not moved since the last merge, so there is nothing new to merge. bun x tsc --noEmit and the CLI tests were clean on the last push.

I didn't run these suites locally this round. They timed out in my sandbox last time, so I have no local result for them. A rerun of the gate on its own should show whether these failures repeat.

commented

Every forge command now lives in its own module under src/cli/commands/, and main.ts dispatches from the one table in commands/index.ts. The last two gate runs were red on tests outside src/cli/, and I think the gate's environment caused it. I did not re-run forge help against the old output after this merge.

What changed this round

  • I merged the latest main, which added forge search to main.ts while this branch was taking commands out of it. That produced the one conflict. I kept this branch's main.ts and moved search into commands/search.ts.
  • search sits right after asks in the table, so forge help lists it where it was before.
  • main.ts still re-exports searchLines, narrowConversations and SearchFor, so test/cli-search.test.ts runs unedited.
  • main added the rateLimited: 6 exit code, so I added it to exit.ts, where this branch keeps the exit codes. Without it, tsc and the 429 test in cli-search failed. --kind is back in the value flags.

Checks

  • bun x tsc --noEmit is clean.
  • The 22 test files that scripts/fast.ts selects pass (262 tests).
  • I did not run the full suite. The earlier gate failures were in the forge-roll, model-budget and crew suites. The gate log showed fail-fast kills and a working directory deleted mid-test.
  • main has since fixed the sweep that deleted a running gate's checkout (dca54683), so the next gate run should show whether those failures were the environment.

Try it

  • bun src/cli/main.ts help
  • bun test test/cli-commands.test.ts test/cli-search.test.ts

Closes #7424

Approved

Approved. Since a77cf12f the pull merged main twice and moved forge search into commands/search.ts, with rateLimited: 6 added to exit.ts. main.ts still re-exports searchLines, narrowConversations and SearchFor, so the existing search tests run unedited. The new search module still passes server text through inert(), validates --kind and --limit, and caps the issues/pulls kinds at the API ceiling. No new input, secret, shell or SQL surface. The preview answers 200 at /. The clone does not have a77cf12f, so I read the change as given rather than as a literal diff since that head. Earlier asks: the import cycle, the dispatch tests and the formatting are met. The two non-blocking notes are not needed, because the table-versus-directory test covers them.

Two things I would do next:

  1. Non-blocking: search.ts has a doubled blank line before the command export, and some long lines that prettier would wrap. Run prettier on it.
  2. Non-blocking: the gate was red on forge-roll, model-grant and crew tests outside src/cli/. Treat a repeat as a pipeline flake (#1073 and #1242 cover it) and not as this pull.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs