Repository navigation
Commit bf36edd
Fixes #21542
Clause-②: no
`os init` and `os compile` printed ten refusals twice: once as the
command's own `✗` line on stdout, and again as oclif's `Error:` block on
stderr, because the command handed the same sentence to `this.error`.
Each now prints its `✗` line and the hint under it once, and ends in
`this.exit(2)`, the status `this.error` raised. Stdout is unchanged;
stderr no longer repeats it; every exit status is still 2.
## The shape, and why it is this one
Triage's ruling asks for one shape at every site, through the split
`isReportedError` guards, with no second mechanism and no per-site
variant. The shape the family's text faces already use is: the command
renders its own refusal with `printError`, and ends in `this.exit(n)`.
`validate`, `info`, `diff`, `lint`, `verify`, `i18n check`, `i18n
extract`, `generate` and `migrate meta` all end that way, and
`isReportedError` is the guard a catch-all uses for a refusal a helper
already wrote to stderr. This PR applies that shape at all ten sites. No
helper is added, and `utils/format.ts` is read, not edited.
- **Which stream the one copy is on.** The dispatch read the split as
"the human text goes on stderr once". That is the helper half of it:
`printErrorToStderr`'s docblock says a command that has already decided
it renders the text face keeps `printError` on stdout, and stderr is for
shared paths that cannot see `--json` (`resolveConfigPath`). `os init`
declares no `--json` at all, and `os compile`'s `--json` branch returns
above every site here. So the surviving copy stays where the `✗` line
and its hint always were. Stdout already held everything stderr carried,
so nothing is lost.
- **Why `init.ts`'s catch-all has no `isReportedError` guard.**
`compile.ts`'s catch-all already carries it, because
`resolveConfigPath()` can reach it. Only `ConfigRefusalError`
(`config.ts`) and the SDUI manifest error (`sdui-manifest.ts`) carry the
marker, and nothing in `init.ts`'s `try` reaches either: it imports
neither, and `validateScaffold` has its own loader. A guard there would
be a branch no run can take.
- **Why exit 2.** `this.error(msg)` exits 2 and `this.exit(2)` is the
same status. The dispatch's A4 and the ruling's second pin both say the
statuses stay, and the platform checklist already names oclif's 2 on `os
compile`'s human path as legitimate.
## Population: a symbol walk, not a grep
ts-morph over `packages/cli/src/commands` at `fd5a1cd597` with the type
checker: calls resolving to oclif's `Command.error`, and calls resolving
to `printError` or `printErrorToStderr` in `utils/format.ts`, grouped by
outermost function.
- 16 `this.error` call sites in 5 files; 57 command files call a refusal
printer. Functions holding both: 2, `init.ts run()` and `compile.ts
run()`.
- Those two hold 10 `this.error` sites: the card's 8, plus init's
scaffold self-test and dependency install refusals inside its `try`. The
ruling's third bullet makes any pair beyond the 8 ride this landing, and
they do.
- The three `datasource` commands (`introspect`, `list-tables`,
`validate`) hold the other 6 `this.error` sites and call no refusal
printer. Their detail lines go through `this.log` and the one sentence
is oclif's, so they are not pairs.
- After this PR the same walk finds 0 functions holding both, and 6
`this.error` sites in 3 files.
## Readings at the public door
Spawned `bin/run-dev.js` (the source entry; it shares the published
entry's oclif `handle()` and `flush()`) from a scratch directory, before
and after. Before, the source entry prints oclif's stack beneath the
same sentence; the published entry prints `› Error: …`, as the card
measured it.
```text
os init demo -t bogus before: stdout " ✗ Unknown template: bogus" + " Available: app, plugin, empty"
stderr "Error: Unknown template: bogus" + stack exit 2
after: stdout the same two lines exit 2, stderr empty
os compile (config throws at load)
before: stdout " ✗ probe: the config module threw at load"
stderr "Error: probe: the config module threw at load" + stack exit 2
after: stdout the same line exit 2, stderr empty
```
All ten sites, each reached through a real run (fixtures are in the e2e
pin's header). Copies of the sentence across both streams, and the exit
status:
| Site | Reached by | Copies | Exit |
| --- | --- | --- | --- |
| init: unknown template | `init demo -t bogus` | 2 to 1 | 2 to 2 |
| init: invalid project name | `init Bad_Name` | 2 to 1 | 2 to 2 |
| init: target not empty | `init demo` over a non-empty `demo/` | 2 to 1
| 2 to 2 |
| init: current directory name invalid | `init` in a directory named
`Bad Cwd` | 2 to 1 | 2 to 2 |
| init: config already exists | `init` beside an `objectstack.config.ts`
| 2 to 1 | 2 to 2 |
| init: scaffold self-test rejects (in the try) | `init demo -p npm`,
stub `npm` that installs nothing | 2 to 1 | 2 to 2 |
| init: dependency install fails (in the try) | `init demo -p npm`, stub
`npm` exiting 1 | 2 to 1 | 2 to 2 |
| init: catch-all | `init --no-install` with a file named `src` | 2 to 1
| 2 to 2 |
| compile: runtime bundle refused | `compile --runtime-bundle`, config
importing `./helper` that only `helper.jsx` satisfies | 2 to 1 | 2 to 2
|
| compile: catch-all | `compile`, config throwing at load | 2 to 1 | 2
to 2 |
`os build` extends `Compile` and has no refusal of its own, so it
inherits both compile rows. The published `bin/run.js`, built from this
tree, was run for the init unknown-template and compile catch-all rows
too: exit 2, stdout once, stderr empty.
## Pins
- `packages/cli/test/refusal-renders-once.e2e.test.ts` (nightly): spawns
the CLI through each of the ten sites and asserts the exit status is 2,
the refusal's subject occurs once across both streams, exactly one `✗`
line is printed, and the hint under it survives. Every spawn is paid in
`beforeAll`; no case is clocked.
- `packages/cli/test/refusal-renders-once.test.ts` (queue, unit): the
structural half over every command module. No function that calls a
refusal printer (an identifier bound by an import from
`utils/format.js`, aliases followed) also calls `this.error`. Nine
fixtures pin the scan, and three floors keep it from going vacuous (60
command modules, 50 files calling a printer, 3 files raising
`this.error`), set under what the symbol walk counted on this tree: 65,
57 and 3. The population is discovered, so the next command that repeats
the shape is in it.
- `packages/cli/test/exit-signal.pin.test.ts`: its `this.error`
real-site anchor followed the sites to `this.exit(2)` (init's two in-try
refusals, compile's and build's bundling refusal). `SITE_FLOOR` stays
131 (both spellings are seeds), and the pin stays green; the
`this.error` seed itself is still covered by its fixtures.
## Reverse verification
Run on the committed fix, through `scripts/ablation-replace.mjs` in wrap
mode under the lock, restoring one site's pairing at a time. Both halves
read the source (`bin/run-dev.js` runs from `src/`, the structural pin
reads text), so no `dist/` is on the path and no rebuild leg applies.
- Leg A, `init.ts` unknown-template site back to `this.error(...)`:
anchor 1 to 0, blob `929642fba1ff` to `edab7c113e65`. Structural pin red
(1 failed, naming `init.ts run()`); e2e red on exactly that case (1
failed, 9 passed: "expected 2 to be 1"); exit-signal pin green. Restore
proven: blob equals HEAD (`929642fba1ff`) and `git diff HEAD` is empty.
- Leg B, `compile.ts` runtime-bundle site back to
`this.error(err.message)`: anchor 1 to 0, blob `25d3ea870831` to
`42e44c372e8e`. Structural pin red (2 failed: the pair, and the
exit-signal anchor "each ends in `this.exit(2)`"); e2e red on exactly
the bundling case (1 failed, 9 passed). Restore proven: blob equals HEAD
(`25d3ea870831`), `git diff HEAD` empty, `git status` clean.
- Direction observed: red in both legs, the normal one.
## Verification
At `d83eb007b0` unless noted; every heavy run went through
`os-verify-lock.sh`.
- `pnpm --filter @objectstack/cli typecheck`: exit 0 (`tsc --noEmit`,
and `check:test-typecheck` OK with its ledger unchanged: 3 files, 28
errors).
- Unit: `refusal-renders-once.test.ts`, `exit-signal.pin.test.ts` and
`vitest-tiers-partition.test.ts`: 3 files, 142 tests passed. Driven pin
with `OS_TEST_TIERS=nightly`: 10 of 10 passed.
- Every test file that drives `os init`, `os compile` or `os build`,
selected by import of the command module or by spawn argv (run at
`228259e373`; the later commit changes one comment line in a test file):
31 queue-tier files, 485 passed and 6 failed, all 6 in
`published-subpath-console.pin.test.ts`. Those six failed because this
worktree's `packages/cli/dist` had been built with `OS_SKIP_DTS=1` and
carried no declarations; after a declaration build the file passed 14 of
14. 18 nightly e2e files with `OS_TEST_TIERS=nightly`: 210 passed.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, no paths, at `d83eb007b0`: 66 derived, all
66 run. Three first answered exit 3 or 124 for environment reasons
(`check:dual-build-cjs-loads` and `check:i18n-coverage` said
PREREQUISITE NOT MET because packages outside the cli closure had no
`dist/`; `check:type-check-debt` hit my own 420 s cap). After building
the missing packages with declarations and a longer cap, all three
exited 0. `--ran`: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN.
- Lint, as a proven narrowing: ESLint with inline config disabled over
the 5 touched TypeScript files, population read from ESLint's own config
(`isPathIgnored` and `calculateConfigForFile`): 5 files in the JSON
results, 0 errors, 0 warnings. The `.changeset` file is outside ESLint's
population. Type-aware linting is off for each file (no
`parserOptions.project`; the config header says it is never enabled),
and the diff touches no ESLint config, so it cannot move a verdict on an
untouched file. Whole-repo `pnpm lint` is left to CI.
- Not re-derived on a fresh tree: the branch is based on `fd5a1cd597`
and `origin/main` is 7 commits ahead. The derivation tool reports one
input changed upstream, `scripts/engine-double-contract.pinned.json`
(the `check:engine-double-contract` family, which ran green here; this
diff adds no fake engine). The upstream commits touch `packages/cli`
only under `migrate/` and two utils, and add no `printError` or
`this.error` line under `src/commands`.
## Acceptance notes
- The declaration in `utils/format.ts` above `CliExitCode` reads "The
only two exit codes this CLI has: 0 success, 1 failure". Ten refusals
exit 2, as they always did, and the platform checklist
(`docs/qa/platform-checklist/areas/cli.json`) blesses oclif's 2 on `os
compile`'s human path. This PR keeps 2, as the ruling's pin says. Noted,
not filed: the docblock's scope is the `emitJson` slot's type.
- The same checklist file describes that exit 2 as coming from
`compile.ts`'s `this.error()` in seven strings. The status it blesses is
unchanged; only the named mechanism now reads stale. Not edited here
(outside this card's file surface, and the file carries a revision
history of its own).
- `os compile`'s catch-all still answers exit 1 for a refusal a helper
already wrote (the `isReportedError` branch) and exit 2 for any other.
That asymmetry predates this PR and is kept.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent f9f9f91 commit bf36edd
6 files changed
Lines changed: 578 additions & 24 deletions
File tree
- .changeset
- packages/cli
- src/commands
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1041 | 1041 | | |
1042 | 1042 | | |
1043 | 1043 | | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
| 1047 | + | |
| 1048 | + | |
1044 | 1049 | | |
1045 | 1050 | | |
1046 | | - | |
| 1051 | + | |
1047 | 1052 | | |
1048 | 1053 | | |
1049 | 1054 | | |
| |||
1204 | 1209 | | |
1205 | 1210 | | |
1206 | 1211 | | |
1207 | | - | |
1208 | | - | |
| 1212 | + | |
| 1213 | + | |
1209 | 1214 | | |
1210 | 1215 | | |
1211 | 1216 | | |
1212 | 1217 | | |
1213 | 1218 | | |
1214 | 1219 | | |
| 1220 | + | |
| 1221 | + | |
| 1222 | + | |
1215 | 1223 | | |
1216 | 1224 | | |
1217 | | - | |
| 1225 | + | |
1218 | 1226 | | |
1219 | 1227 | | |
1220 | 1228 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1148 | 1148 | | |
1149 | 1149 | | |
1150 | 1150 | | |
| 1151 | + | |
| 1152 | + | |
| 1153 | + | |
| 1154 | + | |
| 1155 | + | |
| 1156 | + | |
| 1157 | + | |
| 1158 | + | |
1151 | 1159 | | |
1152 | 1160 | | |
1153 | 1161 | | |
1154 | | - | |
| 1162 | + | |
1155 | 1163 | | |
1156 | 1164 | | |
1157 | 1165 | | |
| |||
1169 | 1177 | | |
1170 | 1178 | | |
1171 | 1179 | | |
1172 | | - | |
| 1180 | + | |
1173 | 1181 | | |
1174 | 1182 | | |
1175 | 1183 | | |
| |||
1179 | 1187 | | |
1180 | 1188 | | |
1181 | 1189 | | |
1182 | | - | |
| 1190 | + | |
1183 | 1191 | | |
1184 | 1192 | | |
1185 | 1193 | | |
| |||
1191 | 1199 | | |
1192 | 1200 | | |
1193 | 1201 | | |
1194 | | - | |
| 1202 | + | |
1195 | 1203 | | |
1196 | 1204 | | |
1197 | 1205 | | |
1198 | 1206 | | |
1199 | 1207 | | |
1200 | 1208 | | |
1201 | 1209 | | |
1202 | | - | |
| 1210 | + | |
1203 | 1211 | | |
1204 | 1212 | | |
1205 | 1213 | | |
| |||
1357 | 1365 | | |
1358 | 1366 | | |
1359 | 1367 | | |
1360 | | - | |
| 1368 | + | |
1361 | 1369 | | |
1362 | 1370 | | |
1363 | 1371 | | |
| |||
1386 | 1394 | | |
1387 | 1395 | | |
1388 | 1396 | | |
1389 | | - | |
| 1397 | + | |
1390 | 1398 | | |
1391 | 1399 | | |
1392 | 1400 | | |
1393 | 1401 | | |
1394 | | - | |
| 1402 | + | |
1395 | 1403 | | |
1396 | 1404 | | |
1397 | 1405 | | |
1398 | | - | |
| 1406 | + | |
1399 | 1407 | | |
1400 | 1408 | | |
1401 | 1409 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
115 | | - | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
116 | 118 | | |
117 | 119 | | |
118 | 120 | | |
| |||
480 | 482 | | |
481 | 483 | | |
482 | 484 | | |
483 | | - | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
484 | 488 | | |
485 | 489 | | |
486 | 490 | | |
| |||
648 | 652 | | |
649 | 653 | | |
650 | 654 | | |
651 | | - | |
652 | | - | |
653 | | - | |
654 | | - | |
655 | | - | |
656 | | - | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
657 | 660 | | |
658 | | - | |
| 661 | + | |
659 | 662 | | |
660 | 663 | | |
661 | 664 | | |
| |||
961 | 964 | | |
962 | 965 | | |
963 | 966 | | |
964 | | - | |
965 | | - | |
| 967 | + | |
| 968 | + | |
966 | 969 | | |
967 | 970 | | |
968 | 971 | | |
| |||
0 commit comments