Skip to content

cli: environments/*.ts command sources no longer spell os projects in --help - #11227

Merged
os-elon merged 2 commits into
mainfrom
claude/issue-10967-environments-command-naming
Aug 23, 2026
Merged

cli: environments/*.ts command sources no longer spell os projects in --help#11227
os-elon merged 2 commits into
mainfrom
claude/issue-10967-environments-command-naming

Conversation

@os-elon

@os-elonos-elon commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

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:

  • Read processCommandIds() in the installed @oclif/core@4.13.3's
    lib/config/plugin.js: id = [...topics, command].filter(Boolean).join(sep), where
    topics comes from path.parse(file).dir.split('/') and command is the file's
    basename (dropped when it is literally index). The class name never enters this
    computation.
  • packages/cli/package.json's oclif block confirms commands.strategy: "pattern",
    commands.target: "./dist/commands", topicSeparator: " " — i.e. environments/bind.ts
    → id environments:bind, invoked as os environments bind.
  • Empirically: built @objectstack/cliafter the class renames and ran the real
    binary — all five commands resolve, and --help now prints the corrected os environments … examples:
    $ node packages/cli/bin/run.js environments bind --help
    EXAMPLES
    $ os environments bind (project-id) --artifact ./dist/objectstack.json
    $ node packages/cli/bin/run.js environments list
    Authentication required. Please run `os login` or set OS_TOKEN environment variable.
    
    (fails on auth, not Command … not found — proves resolution, not just --help text.)

Premise held. premise_still_valid: true.

The fix

Three parts, matching the claim comment's scope ruling (class renames ruled in, not
deferred):

  1. static override examplesos projects …os environments …, all five files
    (bind.ts, create.ts, list.ts, show.ts, switch.ts), 21 occurrences total.
  2. JSDoc headers — same swap, same five files.
  3. Exported class names + environments.test.ts's imports and describe title —
    ProjectsBindEnvironmentsBind, ProjectsCreateEnvironmentsCreate,
    ProjectsListEnvironmentsList, ProjectsShowEnvironmentsShow,
    ProjectsSwitchEnvironmentsSwitch.

Left alone, deliberately: list.ts's console.log('\nProjects (…):\n') — a table-mode
output 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 processCommandIds algorithm (topics = directory segments, command =
basename, index dropped) — not a grep for the literal string os projects, so it keeps
working for a topic nobody has renamed yet.

examples read via AST, not import.extractExamples parses each command source's
static override examples array off the TypeScript AST rather than importing all ~60
command modules and reading Cmd.examples off the live class — several commands pull
heavy transitive graphs at module load (database drivers, @objectstack/client,
@objectstack/runtime), so importing every one just to read one static array would make
this 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.ts gives for
reading 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 oclif
help-template form '<%= config.bin %> cmd ...' (config.bin is "os", package.json's
oclif.bin); either prefixed by one or more ENV=value assignments, including a
double-quoted value containing spaces (start.ts's
OS_ARTIFACT_URL="...#sha256=(64 hex chars)" <%= config.bin %> start); and the
{ command, description } object form (start.ts, two entries). An entry matching none
of 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.ts are carved out via an EXCLUDED map (file → reason, naming #11221) — the same
pattern as
packages/create-objectstack/src/starter-comments-self-contained.test.ts's EXCLUDED
(map + filtered main assertion + a second it.each that re-runs the same predicate over
the 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:

  • A known-good example from each of the three recognised invocation shapes is asserted to
    clear the check.
  • The literal pre-fix line (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.
  • A total-examples-found count across the non-excluded population is asserted > 50, so a
    walk that silently stopped finding anything would itself fail.

Manually performed, on top of the built-in checks:

  • Temporarily reintroduced the pre-fix os projects bind … line into bind.ts's real
    examples array, re-ran the widened suite (71 tests) — exactly 1 failure, naming
    environments/bind.ts, all 70 others still green — then restored and confirmed 71/71
    green again.
  • Temporarily cleared EXCLUDED to prove the exclusion is load-bearing rather than
    decorative: re-ran — exactly the 3 excluded files failed, each naming its own real,
    currently-unresolved os auth … examples verbatim, all 68 others still green — then
    restored EXCLUDED and 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-run
in full at this head after the widening, not only at the earlier commit.

  • pnpm --filter @objectstack/cli typechecktsc --noEmit, os-verify-lock: VERDICT command-exit 0.
  • pnpm --filter @objectstack/cli buildtsc -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 test script, invoked via pnpm --filter … test --, does not scope
    to 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 head
    b84a2566, 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's
    TEST_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" / already
    read by pnpm --filter @objectstack/cli typecheck, not the sibling test/ tree the
    1484-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 top
    of 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. The
    pin reads only inside packages/cli/src/commands (its own package), so no new
    cross-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 the
    documented trap. Built the full workspace closure
    (pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' — 70
    tasks 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 widening
    commit (the ~4-minute re-measure): the widened file lives entirely under
    packages/cli/src/commands/**, which check:type-check-coverage (run at the widened
    head, above) confirms is unaffected — same 1031/1484 TEST_DEBT numbers for
    @objectstack/cli before and after, and pnpm --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-existing
    improvements from unrelated work; not lowered here, out of scope.)

Out of scope


Generated by Claude Code

…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.
@github-actions

github-actionsBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 15 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via os environments bind (command), os environments create (command), os environments list (command), os environments show (command), os environments switch (command))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via os environments create (command))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 38cf397ea9b1b5aea338805e7559f70576b88fb1packageMentionDocs.

Which tree this was computed on

This run read content/docs from b5d1288c9fe58409fd68af2a49e021a4bb2a5fff — the merge of head b84a25665443e1720d8d33ef292b9391034c31f2 into base 38cf397ea9b1b5aea338805e7559f70576b88fb1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 38cf397ea9b1b5aea338805e7559f70576b88fb1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 23, 2026
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.
@os-elon
os-elon marked this pull request as ready for review August 23, 2026 04:58
@os-elon
os-elon added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit e4a71d4Aug 23, 2026
35 checks passed
@os-elon
os-elon deleted the claude/issue-10967-environments-command-naming branch August 23, 2026 05:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

packages/cli: environments/*.ts command source still names os projects in examples, class names, and docstrings — leaks into live --help output

2 participants

@os-elon@claude