Repository navigation
Commit 41e84c0
fix(cli): the os dev watcher prints why a recompile was refused (#23867)
Fixes #23820
Clause-②: no
## What changed
`os dev`'s watch-recompile loop (`compileAndPing` in
`packages/cli/src/commands/dev.ts`) now captures the `compile` child's
stdout as well as its stderr. On a non-zero exit it prints both under
the `✗ compile failed` line: the same lines `pnpm build` prints, with no
`--verbose` needed. Nothing else changes:
- A successful recompile prints exactly what it printed before. The
compile's progress output stays hidden on success.
- `--verbose` still passes both streams straight through, so a failure
prints nothing twice.
- A failed compile still writes no artifact and still triggers no
restart.
## Measured: why the refusal never appeared
Setup: a fresh `os init my-app -t app --no-install` scaffold (namespace
`my_app`), made with the CLI built from this branch. Dependencies are
`18.0.0-next.2` from npm. An object named `task` is wired into
`src/objects`.
- `os compile` exits 2. It writes 10 lines (517 bytes) to stdout and 0
bytes to stderr, so the refusal is on stdout. `pnpm build` prints the
same 10 lines and exits 2.
- `os dev --fresh -p RANDOM` boots, the server answers, and the object
is saved. **Before** (base `1eac7ad751`):
```
↻ recompiling (2 files)...
✗ compile failed (3782ms)
```
- **After** (this branch, same scaffold, same save):
```
↻ recompiling (2 files)...
✗ compile failed (4012ms)
◆ Compile
────────────────────────────────────────
→ Loading configuration...
✗ defineStack namespace-prefix validation failed (1 issue):
✗ Object 'task' is missing the package namespace prefix. Rename it to
'my_app_task' (namespace = 'my_app').
Every object.name must be `${manifest.namespace}_${shortName}`. This is
the only supported writing style — the platform does not provide ns()
helpers or factory wrappers.
```
Both runs were torn down by their own PID, and the port had no listener
afterwards.
## Where the fix lives: the watcher (A), not the producer (B)
- **`compile` writes to stdout on purpose.** It renders every text-face
refusal through `printError`, which writes to stdout: 12 call sites in
`compile.ts`, and the printer is used by 61 command files.
`packages/cli/src/utils/format.ts` states this as the convention.
`printErrorToStderr` exists only for shared helpers that cannot see
`--json`, and "a command that has already decided it is rendering the
text face should keep writing to stdout".
- **Moving refusals to stderr (B) breaks more than it fixes.** It goes
against that convention, either for `compile` alone or for every
command. It also changes the streams every other caller sees: `os dev`'s
boot compile, `os start` and `os environments bind` spawn `compile` with
inherited streams. The e2e suites that spawn it would see it too.
- **The watcher is the only caller that threw stdout away**, so the fix
is a change at that one site (A).
## What is printed
- **The whole output, not only the refusal block.** The watcher prints
the compile's stdout, then its stderr. It drops leading blank lines and
trailing whitespace and filters nothing else.
- On the card's case, 4 of the 10 lines are the compile's header and
progress: the blank line, `◆ Compile`, the rule and `→ Loading
configuration...`. The other 6 lines are the refusal, which has two
paragraphs. The noise is small, and the last progress line also tells
you which step refused.
- Picking "the refusal" out would mean `dev.ts` parsing `compile`'s
glyphs. That is a second reader of another command's prose, and it would
silently drop output whenever that prose changed. So it is not done.
- **stdout comes before stderr.** They arrive through two separate
pipes, so their interleaving cannot be recovered. stdout goes first
because that is where the refusal is.
- **`maxBuffer` is now 64 MiB.** spawnSync's default is 1 MiB, and a
child whose pipe fills the buffer is killed. With stdout now piped, a
large but successful compile would have turned into a failed one. For
scale, the successful compile on the scaffold writes 736 bytes.
## Surface
- **Within the claim's surface.** The diff touches `dev.ts`
`compileAndPing` only (its `spawnSync` options and its failure line),
adds one pin and one changeset.
- **Not touched.** `compile.ts` and every refusal producer. No other
region of `dev.ts`: the `resolveDevDatabase` and database-directory
`mkdirSync` regions belong to #23737 (seat 1's PR #23851), and this PR
leaves them alone.
## Tests
**Pin.**
`packages/cli/src/commands/dev-watch-compile-refusal.pin.test.ts`, unit
tier: it spawns a fixture, never the CLI entry, and boots no kernel.
How it is built:
- **Real code under test.** The pin drives the real
`startWatchRecompile` and `compileAndPing`. It fakes only chokidar, plus
a stub for `./serve.js`: the watch loop never reads `serve.ts`, and
loading it costs the whole runtime graph.
- **Fixture scripts stand in for `compile`.** `binPath` points at them,
so the refusal text belongs to the fixture and no CLI prose is pinned.
- **Inheritance is observed.** Each fixture records the identity (device
and inode) of its fd 1 and fd 2. So `--verbose` inheritance is checked
against the test process's own streams, not read off the spawn options.
What it pins:
1. A refusal written to stdout only (exit 2) lands on the failure line,
and the whole stdout appears in order. No restart is requested.
2. With output on stdout and stderr (exit 1), both appear, stdout first.
3. CONTROL: a successful recompile prints exactly two lines, as before.
The fixture's progress output does not appear, and `onRebuildLanded`
fires with the label.
4. CONTROL, `--verbose`: the child's fd 1 and fd 2 are the test
process's own, and the failure line repeats nothing.
**Ablation**, on `4f4f529e9` through `scripts/ablation-replace.mjs`. The
test imports `./dev.js` from `src`, so there is no dist leg.
- **Mutation.** stdio reverts to `['ignore', 'ignore', 'pipe']`, the
pre-fix capture. The anchor count went 1 to 0, and the blob changed from
`4ada8ac0d61a` to `b74790b2dffb`.
- **Result.** Cases 1 and 2 went red: `expected ' ✗ compile failed
(46ms)' to contain 'FIXTURE-REFUSAL: …'`, the card's symptom exactly.
Cases 3 and 4 stayed green.
- **Restore.** Blob equal to HEAD, and `git diff HEAD` empty.
**Local runs.** All at `4f4f529e9` unless noted, through
`scripts/pm/os-verify-lock.sh`. The box is shared, so timings are
shared-box seconds.
- `pnpm --filter @objectstack/cli run typecheck`: exit 0. That covers
`tsc --noEmit` and `check:test-typecheck`.
- `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2` over the pin and the seven unit files that import or
read `dev.ts` (`child-env-source-loader.pin`, `artifact-child-env.pin`,
`dev-mcp-connect-hint-origin`, `port-contract-single-source`,
`console-route-ledger.conformance`, `strip-arg-separator`,
`test/commands`): exit 0, 8 files, 143 tests passed.
- The pin alone: 4 of 4 passed. The file's wall time on this shared box
went from 30.7 s to 13.8 s once `serve.js` was stubbed. Import accounts
for 12 s of that and the tests for 1.5 s.
- `pnpm exec turbo run build --filter='@objectstack/cli...'
--concurrency=2`: exit 0, at `1aa612afc`. That was the merge of
`origin/main`, and `dev.ts` has not changed since.
- `pnpm exec eslint --no-inline-config` on both changed `.ts` files:
exit 0. The repo-wide lint run is CI's `Lint & Repo Gates`.
**Gate families.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derives 64. Reconciled with `--ran` (every
line carries its exit code): 64 accounted, 62 run with exit 0. Two are
NOT MEASURED, each exiting 3 with `PREREQUISITE NOT MET`. Both need
packages outside the `@objectstack/cli` closure built:
- `check:dual-build-cjs-loads` lists `studio`, `client-react`,
`connector-slack` and others with no `dist/`.
- `check:i18n-coverage` needs `examples/app-showcase`'s
`connector-slack` built.
CI's `Lint & Repo Gates` runs both. The four artifact-roster gates whose
roster sits under a changed path all exited 0: `check-changeset-fixed`,
`check:authz-resolver`, `check:error-code-casing` and
`check:filter-alias-parity`. So did `check:nul-bytes`.
**Declared to CI, not run locally.**
- The `integration` tier. The diff touches no integration-tier file and
no spawn entry (`bin/`, `test/helpers/serve-process.ts`).
`test/dev-no-watch.pin.test.ts` imports `dev.ts` and is
integration-tier.
- `test:repo`.
## Acceptance notes
- **Signals and spawn errors are still not explained.** A compile killed
by a signal (for example the OOM killer) still prints `✗ compile failed`
with whatever output it produced, and does not name the signal.
`r.error` (a spawn error) is not printed either. Both were already true
and are not reproduced here: noted, not filed.
- **The objectos quickstart workaround can be removed after release.**
The quickstart's line "If it prints only `✗ compile failed`, run `pnpm
build` to see why" is obsolete once this ships in an `@objectstack/cli`
release. That is the objectos repo's edit to make.
---
_Generated by [Claude
Code](https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a35f7a6 commit 41e84c0
3 files changed
Lines changed: 318 additions & 3 deletions
File tree
- .changeset
- packages/cli/src/commands
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
997 | 997 | | |
998 | 998 | | |
999 | 999 | | |
| 1000 | + | |
| 1001 | + | |
| 1002 | + | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
| 1013 | + | |
| 1014 | + | |
| 1015 | + | |
1000 | 1016 | | |
1001 | 1017 | | |
1002 | 1018 | | |
1003 | | - | |
| 1019 | + | |
| 1020 | + | |
| 1021 | + | |
| 1022 | + | |
| 1023 | + | |
1004 | 1024 | | |
1005 | 1025 | | |
1006 | 1026 | | |
1007 | | - | |
1008 | | - | |
| 1027 | + | |
| 1028 | + | |
| 1029 | + | |
| 1030 | + | |
| 1031 | + | |
| 1032 | + | |
| 1033 | + | |
1009 | 1034 | | |
1010 | 1035 | | |
1011 | 1036 | | |
| |||
0 commit comments