Uh oh!
There was an error while loading. Please reload this page.
cli: environments/*.ts command sources no longer spell os projects in --help - #11227
Conversation
…ts in live --help output Fixes#10967 static override examples arrays, JSDoc headers, and exported class names in packages/cli/src/commands/environments/{bind,create,list,show,switch}.ts still spelled the pre-v5.0-rename `os projects <cmd>` — oclif prints examples verbatim as part of --help, so a user copy-pasting straight from `os environments bind --help` hit `Error: Command projects:bind not found.` - examples arrays and JSDoc headers: os projects -> os environments (all five files, 21 occurrences). - exported class names renamed to match their file-path-derived command id (ProjectsBind -> EnvironmentsBind, etc.) -- oclif's pattern-strategy loader derives a command's id purely from its file path, confirmed by reading processCommandIds() in @oclif/core and by building the CLI and running --help/a real invocation on all five commands after the rename. - environments.test.ts's imports and describe title updated to match, and gains a pin: every examples entry on these five commands is checked against the CLI's actual file-tree-derived command-id set (not a grep for the literal string "os projects"), so a future topic rename that misses an examples string fails a test instead of shipping. Anti-vacuity and reverse-verification (both the pre-fix line as a specimen, and a live edit-run-restore cycle) are documented in the test file's own comment.
📓 Docs Drift CheckThis PR changes 1 package(s): 1 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 — 23 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 b5d1288c9fe58409fd68af2a49e021a4bb2a5fff && git checkout b5d1288c9fe58409fd68af2a49e021a4bb2a5fff
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 38cf397ea9b1b5aea338805e7559f70576b88fb1 b84a25665443e1720d8d33ef292b9391034c31f2 && git checkout -B drift-repro 38cf397ea9b1b5aea338805e7559f70576b88fb1 && git merge --no-ff b84a25665443e1720d8d33ef292b9391034c31f2
node scripts/docs-audit/affected-docs.mjs --json 38cf397ea9b1b5aea338805e7559f70576b88fb1
|
PM review on #11227: the pin's population was a hardcoded 5-file map -- exactly the set already correct -- so it could not catch this defect class returning anywhere else, including in the register.ts/whoami.ts/logout.ts files #11221 already tracks live. Widen it to every command source under packages/cli/src/commands/**, walked the same way registeredCommandIds() already walks the tree for the id universe. examples are now read via TypeScript AST (extractExamples), not by importing every command module, to avoid making this file's cost and failure surface track the whole package's transitive import graph. extractExamples/stripInvocationPrefix now handle every invocation shape actually present in the package: plain '$ os ...', the oclif help-template form '<%= config.bin %> ...', either prefixed by one or more ENV=value assignments (including a double-quoted value containing spaces), and the { command, description } object form start.ts uses twice. The three #11221-owned files are carved out via an EXCLUDED map (file -> reason), matching the pattern in packages/create-objectstack/src/starter-comments-self-contained.test.ts's EXCLUDED: a filtered main assertion, plus a second it.each that re-runs the same predicate over the excluded files and asserts it still finds an unresolved entry -- so when #11221 lands, that assertion goes red and says to delete the entry, instead of the exclusion silently living forever. Verified: the widened pin passes (71 tests, up from 14). Reverse-verified the exclusion is load-bearing, not decorative -- temporarily cleared EXCLUDED and confirmed all three files fail by name for the expected reason (their real, currently-unresolved os auth ... examples), then restored. Re-verified the original bind.ts reverse-verification still works under the new population (a reintroduced stale example fails only that one file, all others still pass). Re-ran the full local gate battery (18 gates) clean.
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10967
Premise check
The card's central claim — oclif derives a command's registered id purely from its file
path, never from the exported class name — was re-derived, not inherited, before any
class rename:
processCommandIds()in the installed@oclif/core@4.13.3'slib/config/plugin.js:id = [...topics, command].filter(Boolean).join(sep), wheretopicscomes frompath.parse(file).dir.split('/')andcommandis the file'sbasename (dropped when it is literally
index). The class name never enters thiscomputation.
packages/cli/package.json'soclifblock confirmscommands.strategy: "pattern",commands.target: "./dist/commands",topicSeparator: " "— i.e.environments/bind.ts→ id
environments:bind, invoked asos environments bind.@objectstack/cliafter the class renames and ran the realbinary — all five commands resolve, and
--helpnow prints the correctedos environments …examples:Command … not found— proves resolution, not just--helptext.)Premise held.
premise_still_valid: true.The fix
Three parts, matching the claim comment's scope ruling (class renames ruled in, not
deferred):
static override examples—os projects …→os environments …, all five files(
bind.ts,create.ts,list.ts,show.ts,switch.ts), 21 occurrences total.environments.test.ts's imports anddescribetitle —ProjectsBind→EnvironmentsBind,ProjectsCreate→EnvironmentsCreate,ProjectsList→EnvironmentsList,ProjectsShow→EnvironmentsShow,ProjectsSwitch→EnvironmentsSwitch.Left alone, deliberately:
list.ts'sconsole.log('\nProjects (…):\n')— a table-modeoutput label, not a command-name reference, so outside the three-part scope the claim
comment ruled on.
The pin — widened after PM review
v1 checked a hardcoded 5-file map (this PR's own files). PM review correctly called
this out: that population is exactly the set already correct, so it could not catch the
defect class returning anywhere else — including in
register.ts/whoami.ts/logout.ts,which the very same widening had already surfaced as a live instance (filed as #11221).
v2 (current) checks every command source under
packages/cli/src/commands/**,walked the same way
registeredCommandIds()already walks the tree for the id universe.Mechanism, not token. The registered-id set is derived from the source tree using
oclif's own
processCommandIdsalgorithm (topics = directory segments, command =basename,
indexdropped) — not a grep for the literal stringos projects, so it keepsworking for a topic nobody has renamed yet.
examplesread via AST, notimport.extractExamplesparses each command source'sstatic override examplesarray off the TypeScript AST rather than importing all ~60command modules and reading
Cmd.examplesoff the live class — several commands pullheavy transitive graphs at module load (database drivers,
@objectstack/client,@objectstack/runtime), so importing every one just to read one static array would makethis file's cost and failure surface track the whole package's import graph instead of the
property under test (the same reasoning
child-env-source-loader.pin.test.tsgives forreading command sources as text/AST).
Every invocation shape actually present in the package, catalogued by hand across all
command sources before writing this, is handled: plain
'$ os topic cmd ...'; the oclifhelp-template form
'<%= config.bin %> cmd ...'(config.binis"os",package.json'soclif.bin); either prefixed by one or moreENV=valueassignments, including adouble-quoted value containing spaces (
start.ts'sOS_ARTIFACT_URL="...#sha256=(64 hex chars)" <%= config.bin %> start); and the{ command, description }object form (start.ts, two entries). An entry matching noneof these is graded a failure, not silently skipped — "prefer failing to falling back"
(AGENTS.md, Route & surface ownership §3).
The one exclusion, and why it must self-retire.
register.ts/whoami.ts/logout.tsare carved out via anEXCLUDEDmap (file → reason, naming #11221) — the samepattern as
packages/create-objectstack/src/starter-comments-self-contained.test.ts'sEXCLUDED(map + filtered main assertion + a second
it.eachthat re-runs the same predicate overthe excluded files and asserts it still finds an unresolved entry). When #11221 lands,
that retiring assertion goes red on purpose and its message says to delete the entry —
visible and temporary instead of silent and permanent.
Anti-vacuity + reverse-verification, built into the suite:
clear the check.
os projects bind (project-id) --artifact ./dist/objectstack.json) is fed through the same predicate and asserted to fail,because
registeredIds.has('projects bind')is false — the declared reason.> 50, so awalk that silently stopped finding anything would itself fail.
Manually performed, on top of the built-in checks:
os projects bind …line intobind.ts's realexamplesarray, re-ran the widened suite (71 tests) — exactly 1 failure, namingenvironments/bind.ts, all 70 others still green — then restored and confirmed 71/71green again.
EXCLUDEDto prove the exclusion is load-bearing rather thandecorative: re-ran — exactly the 3 excluded files failed, each naming its own real,
currently-unresolved
os auth …examples verbatim, all 68 others still green — thenrestored
EXCLUDEDand confirmed 71/71 green again.Gates
All run under the shared verify lock; verdict lines quoted, not bare exit codes. Final
head is
b84a2566(the widened-pin commit); the 19-gate local battery below was re-runin full at this head after the widening, not only at the earlier commit.
pnpm --filter @objectstack/cli typecheck—tsc --noEmit,os-verify-lock: VERDICT command-exit 0.pnpm --filter @objectstack/cli build—tsc -p tsconfig.build.json,os-verify-lock: VERDICT command-exit 0.packages/cli/node_modules/.bin/vitest run src/commands/environments/environments.test.ts(the package's own
testscript, invoked viapnpm --filter … test --, does not scopeto one file the way I expected and re-runs the whole 137-file suite instead — noting this
so the next reader isn't fooled by it) —
Test Files 1 passed (1),Tests 71 passed (71).The full-package run also passed clean before the widening:
pnpm --filter @objectstack/cli test→Test Files 160 passed (160),Tests 1765 passed (1765),VERDICT command-exit 0.node scripts/pm/dispatch-gates.mjs(no paths — merge-base diff, re-derived at headb84a2566, same 7-file diff surface) named 14 local gates + 5 convention-triggered ones(this diff edits a test file). All 19 re-run at the widened-pin head, all exit 0 — verdict
lines:
check:changeset-gate-self-tests(all self-tests pass),check:cross-package-test-inputs(OK: 13 package(s) read outside themselves, all declared),check:objectui-changeset,check:published-files(69 publishable package(s) … declare a files whitelist),check:slot-lookup(ratchet holds … none new),check:test-source-alias,check:type-source-resolution,check-adr-0087-registration(this PR adds no declared-breaking changeset),check-changeset-no-major,check-ci-filter-parity,check-empty-changeset,check-plugin-teardown-shape,check-affected-docs,check:query-options-erasure(
ratchet holds … none new),check:type-check-coverage(@objectstack/cli'sTEST_DEBT entry unchanged at 1031 hidden files / 1484 raw errors — confirmed my edited
file sits under
src/, which that ledger's own note says "always compiled" / alreadyread by
pnpm --filter @objectstack/cli typecheck, not the siblingtest/tree the1484-error entry actually measures),
check:engine-double-contract(384 pinned, 133 in the DEBT ledger, 2 exempt),check:where-matcher(282 matcher(s) discovered … none new),check:nul-bytes(no raw ASCII control bytes).pnpm check:cross-package-test-inputs— run explicitly per standing instruction, on topof the dispatch-gates listing above:
104 self-test cases passed,OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob. Thepin reads only inside
packages/cli/src/commands(its own package), so no newcross-package declaration was needed.
pnpm check:type-check-debt --re-measure— first attempt refused (--re-measure cannot run: 1 workspace dependenc(ies) … have no built type entry point on disk), exactly thedocumented trap. Built the full workspace closure
(
pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'— 70tasks successful) and re-ran (before the pin was widened):
check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 241.5s, 1896 raw tsc error(s) total, none above its recorded number.Not re-run a second time after the wideningcommit (the ~4-minute re-measure): the widened file lives entirely under
packages/cli/src/commands/**, whichcheck:type-check-coverage(run at the widenedhead, above) confirms is unaffected — same 1031/1484 TEST_DEBT numbers for
@objectstack/clibefore and after, andpnpm --filter @objectstack/cli typecheck(also re-run at the widened head) stays clean. (A handful of informational "-N, can be
lowered" notes from that run, on packages this PR never touches —
plugin-approvals,runtime,plugin-auth,trigger-record-change— are pre-existingimprovements from unrelated work; not lowered here, out of scope.)
Out of scope
os auth ...— that topic does not resolve (Command auth:whoami not found.) #11221 (filed, unassigned):register.ts/whoami.ts/logout.ts— same defectclass,
os auth (cmd)examples that have never resolved (root-level commands, noauthtopic). Outside packages/cli: environments/*.ts command source still names
os projectsin examples, class names, and docstrings — leaks into live--helpoutput #10967's dispatched file surface — the widened pin now names themexplicitly via
EXCLUDEDrather than silently missing them.list.ts's\nProjects (N):\ntable-mode output label — not a command-name reference,left as-is (see "The fix" above).
Generated by Claude Code