fix(cli): make os create emit a project that installs outside this monorepo - #15535
Conversation
…onorepo `os create` is presented on four public documentation pages as a user-facing scaffolder, and every project it emitted was monorepo-shaped: `workspace:*` dependency specs, a `tsconfig.json` extending `'../../tsconfig.json'`, and a default output directory inside this repository. A developer who followed the docs got a project `pnpm install` refuses. The default emission is now standalone — published semver ranges pinned to the running CLI's own version, a self-contained `tsconfig.json`, a `pnpm-workspace.yaml` carrying the build approvals pnpm 11 requires, and the developer's own directory as the output location. The monorepo-internal placement survives as the explicit `--in-repo` flag. The pin is `scripts/create-scaffold-smoke.sh`, wired into `.github/workflows/os-create-smoke.yml`: it scaffolds every template into a temp directory outside the repository, installs from packed tarballs, and runs the project's own build and typecheck. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…ffold on CI Three follow-ons measured while building the pin for this card: - the `plugin` template emitted an `initialize` method. `Plugin` carries an index signature, so the excess property was accepted but got no contextual type: the scaffold failed its own `strict` type-check with TS7006, and the kernel loader refuses a plugin without `init` outright. It emits `init` now, and the kernel protocol docs lose the warning that told readers to rename it. - `test/create-plugin-docs-parity.test.ts` holds the template's emitted file set equal to the tree each of the three plugin doc pages prints. Both sides are derived, so a template that grows a file reddens every page that does not say so. The three pages are declared as cross-package test inputs and mirrored into turbo.json; `check-ci-filter-parity`'s rollback pin records them by name. - `scripts/create-scaffold-smoke.sh` avoids `mapfile`, which does not exist on the bash 3.2 floor `check:bash32-floor` holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
The `initialize` -> `init` change and its header paragraph were uncommitted when an ablation's restore leg ran `git checkout HEAD -- create.ts`, which is a normal, silent, exit-0 discard of a working tree that was the only copy. Both edits are re-applied here, byte for byte, and the tree is committed before the ablation is repeated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 599f9553cda27151aa70554934b3ac36a9b41038 && git checkout 599f9553cda27151aa70554934b3ac36a9b41038
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ed9d87653eb94b41c769206737110793ffd348be 7ab7671bfcd6b79acbdaef60b3d0f78ce6da134b && git checkout -B drift-repro ed9d87653eb94b41c769206737110793ffd348be && git merge --no-ff 7ab7671bfcd6b79acbdaef60b3d0f78ce6da134b
node scripts/docs-audit/affected-docs.mjs --json ed9d87653eb94b41c769206737110793ffd348be
|
Fixes #14824
os createis presented on four public documentation pages as a user-facing scaffolder. Every project it emitted was monorepo-shaped, so a developer who followed those docs got a projectpnpm installrefuses. The maintainer ruled that a documented developer-facing command must work for the developer who follows the docs — neither retired nor narrowed to an internal generator — so the default emission is now standalone, and a CI pin holds it that way.The defect, reproduced before it was fixed
Both templates rendered into a temporary directory outside this repository, then installed:
Identical for
example, which declares two such specs. The emittedtsconfig.jsoncarriedextends: "../../tsconfig.json"in both, and — measured, not assumed — that did not resolve even inside this monorepo for theplugintemplate:packages/plugins/PKG/../../tsconfig.jsonispackages/tsconfig.json, which does not exist. Every real plugin here spells../../../.What the emission is now
The default placement is standalone:
@objectstack/*specsworkspace:*^17.3.0— the running CLI's own versiontsconfig.jsonextends: "../../tsconfig.json"extendspackages/plugins/plugin-NAMEorexamples/NAME./plugin-NAMEor./NAME(or--dir)pnpm-workspace.yaml, rendered byinit's own rendererThe version comes from
getCliVersion()and the workspace settings fromrenderPnpmWorkspaceYaml()— both imported frominit.tsrather than restated, because a restatement is the two-producer defecttest/scaffold-workspace-consistency.test.tsalready exists to catch, and it is how these templates drifted in the first place.os inititself is untouched.The monorepo-internal placement survives as the explicit, documented
--in-repoflag:workspace:*specs, anextendsthat resolves to the repository root (derived from where the template lands, so it can no longer be wrong by transcription), and nopnpm-workspace.yaml— a nested one would declare the directory its own workspace root and sever the veryworkspace:*links that placement exists for. The command refuses the flag outright when the current directory is not a pnpm workspace root, rather than emit the one thing it is no longer allowed to emit.A fourth defect, found by the pin
Making the emission real made it type-check for the first time, and it did not:
The
plugintemplate wrote aninitializemethod.Plugincarries an index signature, so the excess property was accepted but got no contextual type — andpackages/core/src/plugin-loader.ts:384refuses a plugin withoutinitoutright (Plugin init function is required). The template emitsinitnow, and thewarncallout incontent/docs/protocol/kernel/index.mdxthat told readers to rename it by hand is replaced by an accurateinfonote about the lifecycle.The pin the ruling asked for
scripts/create-scaffold-smoke.shscaffolds every template — enumerated from the built CLI, never a hand list — into a temp directory outside the repository, pins each publishable package to itspnpm packtarball through the project's own overrides, installs, asserts nothing leaked to the registry, then runs the project'sbuildandtypecheck. The dependency specs inpackage.jsonare never rewritten: they are what is on trial. Tarballs are the honest stand-in for the registry because on a pull request the version the scaffold pins is by definition not published yet.It runs from
.github/workflows/os-create-smoke.yml, paths-filtered rather than label-gated opt-in, plus a nightly. The siblingpack-smoke-optin.ymlis label-gated because its defect class can only be recognised by the author; this one can only be introduced by editing a bounded, nameable set of files, and those files are thepaths:list.init.tsis in that list even though this gate never runsos init: the standalone emission calls its exports, so naming the consumer and not the producer is the shape of coupling that lets a gate sit green through the change that breaks it. Not a required context — a paths-filtered job cannot be.Ablation — both legs, proved on disk, from a committed tree
Predicted directions first, then measured. Restore leg for each:
git checkout HEAD -- ABSOLUTE_PATH, proved by blob-hash equality and an emptygit diff HEAD.Leg 1 — restore
workspace:*(predicted: the install pin reds, the schema/load pins stay green). The pre-fixcreate.tswas restored from the branch point (hash-object779e4fec vs HEAD 3beda2c1; injected marker present twice, the fix's own symbol absent). The smoke readspackages/cli/dist, so the package was rebuilt and the mutation confirmed to have reached the artifact —ablation-dist-preflight: "marker present in 1 built file". The pin then went RED, at the default-location limb, which fires before the manifest limb:The manifest limb's own red was measured separately, by running that assertion's real bytes against the same pre-fix emission:
workspace-protocol dependencies, which resolve nowhere outside this monorepo: @objectstack/spec: workspace:* / @objectstack/cli: workspace:*, exit 1 — green on the post-fix emission.test/scaffold-manifest-schema.test.tsstayed green throughout, as predicted.Leg 2 — restore
extends "../../tsconfig.json"alone (predicted: the typecheck half reds, the install half stays green). Held exactly:pnpm installexit 0,pnpm run typecheckexit 2 witherror TS5083: Cannot read file '.../tsconfig.json', and the smoke's tsconfig limb red while its dependency limb passed. The two halves are independently held.Restore proved: working tree clean against HEAD, and the fix's own exported symbol back in⚠️
dist("marker present in 2 built files").workspace:*is deliberately not usable as the absent-marker here — the fixed file still contains that literal on purpose, because it is what--in-repoemits.Verification
Local union re-run at
7ab7671bfcd, after the last commit:bash scripts/create-scaffold-smoke.sh— exit 0, both templates (transcript above).pnpm --filter @objectstack/cli exec vitest runover the scaffold suites — 7 files, 145 passed, 6 expected-fail.pnpm --filter @objectstack/cli typecheck— exit 0.pnpm lint(repo-wideeslint . --no-inline-config) — exit 0.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 109 commands after this diff) plus the 14 artifact-roster families whose silence this derivation says is evidence in neither direction. All green except two, both named below.Two ratchets moved and were updated as part of the diff, not around it:
check:bash32-floorcaughtmapfilein the new smoke script (bash 4.0; the floor is 3.2, what macOS ships). Rewritten without it.check-ci-filter-parity --self-testpins how many declarations a rollback ofcrosspkgwould uncover; the three newcontent/docsinputs move it 16 → 19. The count, the prose and the verdict line are updated and each new page is asserted by name, the convention that block already uses. Measured green at the branch point beforehand, so the move is this diff's and nothing else's.Two non-green results, neither a verdict on this tree:
check-required-contexts.mjs --verify-required-set— exit 2,NOT VERIFIED: the GitHub read answers 403 in this session. The gate's own text says exit 2 classifies the environment.check:docs-audit-scope— red onorigin/main, not from this diff. Its two self-test sources are byte-identical to the branch point and unchanged onorigin/main. Filed as check:docs-audit-scope is RED on main — affected-docs--self-testrefuses its own contract-declaration case #15529.Deliberately not in this PR
os createandos init— not ruled, and reserved by the ruling to be filed separately with a measurement. Filed as [finding] Measure whetheros createandos initshould converge — the shared surface is now four exports and one file map away #15531; that measurement is now available because the shared surface is fourinit.tsexports.init-template-comments-self-containedsweeps onlyos init's templates — the commentsos createships into a scaffolded project are unpinned #14823 (os create's injected comments are validated by nothing) — a separate defect that survives any outcome here, and is not addressed by this PR.@objectstackscope — a developer cannot publish to it, but the name is on none of the ruling's requirements and choosing a replacement is its own decision. Filed asos create pluginnames the scaffolded package@objectstack/plugin-NAME— a scope the developer it is scaffolded for cannot publish to #15530.Generated by Claude Code