Repository navigation
Commit a31ff2e
fix(cli): os init / os create emit overrides.vitest so new projects install on npm 10 (#23850)
Fixes #23805
Clause-②: no
`os init` (all three templates) and `os create plugin` now emit
`"overrides": { "vitest": "~4.0.0" }`, built from
`SCAFFOLD_VITEST_RANGE` through one new export,
`SCAFFOLD_NPM_OVERRIDES`. The scaffolded project now installs on npm 10,
the npm that Node 22 bundles.
`scripts/sync-scaffold-emission-policy.mjs` gains an `overrides.vitest`
`POLICY_STAMPS` row, so the blank template's override is now generated
from the same constant instead of being kept by hand. The template
file's bytes do not change, because it already reads `~4.0.0`.
## Upstream re-measured first, as the card asks
Read 2026-10-16T06:46Z with `npm view`:
- `vitest` dist-tags: `V4` = `4.1.12`, `latest` = `5.0.5`.
- `vitest@4.1.12` peers `"@vitest/browser-webdriverio": "^4.1.11"`
(optional). Every other `@vitest/*` peer is pinned to exactly `4.1.12`.
- `@vitest/browser-webdriverio` has no `4.1.12`. Its newest 4.x is
`4.1.11`, which peers `"vitest": "4.1.11"`.
- So the contradiction is still live upstream. The range itself is not
changed here.
## Every emission site, with its verdict
Measured 2026-10-16T06:57Z. Each manifest was installed in an empty
directory with a fresh `--cache`, against the published `18.0.0-next.2`.
"Before" is today's `main` emission.
| Site | declares `vitest` directly | before, npm 10.9.9 | verdict |
|---|---|---|---|
| `os init -t app` (`init.ts`, `renderScaffoldPackageJson`) | no | exit
1, `edgesOut` | in scope: emits the override |
| `os init -t plugin` (same renderer) | `~4.0.0` | exit 1, `edgesOut` |
in scope: emits the override |
| `os init -t empty` (same renderer) | no | exit 1, `edgesOut` | in
scope: emits the override |
| `os create plugin` standalone (`create.ts`) | `~4.0.0` | exit 0 |
emits the override (card criterion; keeps it equal to the declaration) |
| `os create plugin --in-repo` | `~4.0.0` | n/a (pnpm workspace member)
| no override: npm reads `overrides` only from the root manifest and
pnpm never reads it, so here it would do nothing |
| `create-objectstack` blank template | `~4.0.0` | already overridden
(landed earlier) | now stamped by the new `POLICY_STAMPS` row |
`os create` has one template (`plugin`), and `os init` has three (`app`,
`plugin`, `empty`), all rendered through `renderScaffoldPackageJson`. No
other `packages/cli` code writes a scaffold `package.json`.
### Why `-t app` and `-t empty` too, against the card's plugin-only
criterion
The dispatch assumed that a site emitting no `vitest` needs no override.
The measurement above shows otherwise: `-t app` and `-t empty` crash the
same way, because what all three templates share is `@objectstack/cli`
in `devDependencies`, and its closure carries optional `vitest` peers
(`@objectstack/metadata-core` peers `^4.0.0`, better-auth peers `^2 ||
^3 || ^4`). `-t app` is also the docs.objectos.ai quickstart's command,
measured failing 3 of 3 in the card's thread. `os create plugin` has no
`@objectstack/cli` in its closure, which is why it installed without the
override.
## The installs after the change
Each emitted manifest was produced by driving the real CLI (`tsx
bin/run-dev.js init NAME -t T --no-install` and `create plugin NAME`),
then installed in an empty directory with a fresh `--cache`:
| manifest | npm 10.9.9 | npm 11.21.1 | vitest in the tree |
|---|---|---|---|
| `os init -t app` | exit 0 | exit 0 | none |
| `os init -t plugin` | exit 0 | exit 0 | one copy, 4.0.18 |
| `os init -t empty` | exit 0 | exit 0 | none |
| `os create plugin` | exit 0 | exit 0 | one copy, 4.0.18 |
## Cost, measured
On an emitted `-t app` with the override (npm 10.9.9):
- A later bare `npm install -D vitest` is refused with `EOVERRIDE:
Override for vitest@* conflicts with direct dependency`.
- `npm install -D vitest@~4.0.0` installs (vitest 4.0.18).
The refusal is loud and names the conflict; without the override the
project could not install at all. The changeset gives the one-line fix.
`"$vitest"` is not used: it needs a direct dependency, and two of the
three `os init` templates have none.
## The stamp row
`POLICY_STAMPS` gains `{ key: 'overrides.vitest', block: 'overrides',
field: 'vitest', constant: 'SCAFFOLD_VITEST_RANGE' }`. The engine
already locates any `"BLOCK": { … }` object and one field inside it, so
no engine change was needed. The self-test:
- Every fixture template carries `overrides.vitest`.
- Battery B now parses both vitest stamps, instead of a substring check
that one stale copy could still pass.
- New battery J drifts the override alone while `devDependencies.vitest`
agrees. It checks that `--check` exits 1 naming `overrides.vitest`, that
the agreeing key is not blamed, that the rewrite touches only that span
byte for byte, and that a template dropping the `overrides` block exits
1 naming it.
- Roster floor 9 → 10.
`--check` on the real template, before and after a mutation (through
`scripts/ablation-replace.mjs`, which restores and checks the blob
hash):
- The blank template's `overrides.vitest` was mutated to `^4.1.0`.
`--check` then exits 1 with
`packages/create-objectstack/src/templates/blank/package.json:
overrides.vitest is "^4.1.0" but the policy declares "~4.0.0"`.
- Restored: blob `1bbbff43ce0f` equals HEAD, and `git diff HEAD` is
empty.
- Control: the same mutation under `main`'s script (`1ecb08a1c`, no row)
exits 0 (`2 carrier(s) emit the shared policy`). So on `main`, an
override drift that npm refuses with `EOVERRIDE` passed the gate.
## Pins
`packages/cli/test/scaffold-npm-overrides.test.ts` is a unit-tier file.
Every expectation is derived from the exported constants, with no
literal range:
- every `os init` template and `os create plugin` (standalone) emit
`overrides` equal to `SCAFFOLD_NPM_OVERRIDES`, whose `vitest` is
`SCAFFOLD_VITEST_RANGE`;
- every override that names a direct dependency equals that dependency's
spec (npm `EOVERRIDE`);
- `--in-repo` emits no `overrides`;
- a control checks that the harvest is non-empty and that the equality
case ranges over the two manifests that declare vitest.
Ablation, through `scripts/ablation-replace.mjs`. Each leg mutates, runs
the file, restores, and proves the restored blob equals HEAD:
- Deleting the override from `renderScaffoldPackageJson` fails the 3 `os
init` cases (3 failed / 8 passed).
- Deleting it from `create.ts` fails `os create plugin` (1 failed / 10
passed).
- Forcing it on in-repo fails the in-repo case (1 failed / 10 passed).
## Verification
All results below are at head `f8d18b092` unless a line says otherwise.
Exit codes were captured before any pipe.
- Gate union: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` names 85 commands, derived from merge base
`43567a8fc`. Run at `f8d18b092`: **85 of 85 exit 0**. `--ran`
reconciliation: exit 0, `85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN`.
- An earlier pass of the union, at `e521b5709`, found one real red:
`check:cross-package-test-inputs` exit 1. A comment in
`template-consistency.test.ts` spelled the stamp script's repo path,
which the gate reads as a cross-package input. The comment was reworded
in `f8d18b092`.
- `node scripts/sync-scaffold-emission-policy.mjs --self-test`: exit 0,
60 assertions, 10 batteries. `--check`: exit 0.
- `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2` over the 23 non-e2e test files that import
`commands/init` or `commands/create`, the new pin included: exit 0, **22
files / 501 tests passed**, at `d8a9a54f3`. The later commits touch no
`packages/cli` file.
- `test/generate-stack-reach.test.ts` is in the `integration` project.
That tier and the nightly `scaffold-emission-policy.e2e.test.ts` are
declared to CI.
- `pnpm --filter @objectstack/cli typecheck`: exit 0 (`tsc --noEmit`
plus `check:test-typecheck` OK), at `d8a9a54f3`.
- `pnpm --filter create-objectstack test`: exit 0, 18 files / 275 tests.
`typecheck`: exit 0.
- `pnpm exec eslint --no-inline-config` over the five changed source and
test files: exit 0. The repo-wide lint run belongs to the `Lint & Repo
Gates` check-run.
## Acceptance notes
- Finding, not fixed here: the published skill's copy-paste manifest has
the same crash. This is
`skills/objectstack-platform/references/bootstrap.md`, the "Complete
Working Example" `package.json` fence (`@objectstack/cli ^17.0.0`, no
override). Its `npm install` on npm 10.9.9 exits 1 with `Cannot read
properties of null (reading 'edgesOut')`, and with `overrides.vitest:
~4.0.0` added it exits 0 (measured 2026-10-16T08:10Z). `skills/**` is a
governed surface, and the stamp script's `PUBLISHED_RESTATEMENTS` leg
only reads that fence and checks only `devDependencies.typescript`. So
the fix is a hand edit landed the governed way, and adding
`overrides.vitest` to that row's `keys` comes with it. This is reported
for the seat to file, not filed from this PR.
- The Docker image (#23801) and the optional `vitest` peers of
`plugin-auth` / `metadata-core` (#23806) are untouched; both remain
open. The blank template's override from the PR for #23788 is unchanged
in bytes and is now generated.
- `create-objectstack`'s shipped bytes do not change (`files` is `dist`,
and the template is byte-identical), so the changeset names
`@objectstack/cli` alone.
---
_Generated by [Claude
Code](https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 30ce56e commit a31ff2e
6 files changed
Lines changed: 283 additions & 12 deletions
File tree
- .changeset
- packages
- cli
- src/commands
- test
- create-objectstack/src
- scripts
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
134 | 134 | | |
135 | 135 | | |
136 | 136 | | |
| 137 | + | |
137 | 138 | | |
138 | 139 | | |
139 | 140 | | |
| |||
408 | 409 | | |
409 | 410 | | |
410 | 411 | | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
411 | 419 | | |
412 | 420 | | |
413 | 421 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
390 | 390 | | |
391 | 391 | | |
392 | 392 | | |
393 | | - | |
394 | | - | |
395 | | - | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
396 | 400 | | |
397 | 401 | | |
398 | 402 | | |
| |||
415 | 419 | | |
416 | 420 | | |
417 | 421 | | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
418 | 472 | | |
419 | 473 | | |
420 | 474 | | |
| |||
495 | 549 | | |
496 | 550 | | |
497 | 551 | | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
498 | 555 | | |
499 | 556 | | |
500 | 557 | | |
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
221 | | - | |
222 | | - | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
223 | 226 | | |
224 | 227 | | |
225 | 228 | | |
| |||
0 commit comments