diff --git a/.changeset/environments-command-source-naming.md b/.changeset/environments-command-source-naming.md new file mode 100644 index 0000000000..abf87a3619 --- /dev/null +++ b/.changeset/environments-command-source-naming.md @@ -0,0 +1,22 @@ +--- +"@objectstack/cli": patch +--- + +fix(cli): `environments/*.ts` command sources no longer spell `os projects` in live `--help` output (#10967) + +`static override examples` is printed verbatim as part of oclif's `--help`, and all five +commands under `packages/cli/src/commands/environments/` (`bind`, `create`, `list`, `show`, +`switch`) still spelled the pre-v5.0-rename `os projects ` there — a user copy-pasting +straight from `os environments bind --help` hit `Error: Command projects:bind not found.` +(exit 2), the same dead command #10927 fixed in `packages/cli/README.md`, this time sourced +from the CLI binary itself. + +`examples` arrays and JSDoc headers now say `os environments `. The exported default +class on each file is renamed to match its real, file-path-derived command id +(`ProjectsBind` → `EnvironmentsBind`, etc.) — oclif's pattern-strategy loader derives a +command's id purely from its file path, never from the class name, so this rename changes +no runtime resolution; verified by building the CLI and running `--help` on all five +commands, plus one real invocation, after the rename. `environments.test.ts`'s imports and +`describe` title are updated to match, and gain a pin: every `examples` entry on these five +commands is checked against the CLI's actual file-tree-derived command-id set, so a future +topic rename that misses an `examples` string fails a test instead of shipping. diff --git a/packages/cli/src/commands/environments/bind.ts b/packages/cli/src/commands/environments/bind.ts index 8d2683a45b..d8cb782abd 100644 --- a/packages/cli/src/commands/environments/bind.ts +++ b/packages/cli/src/commands/environments/bind.ts @@ -9,7 +9,7 @@ import { createApiClient, requireAuth } from '../../utils/api-client.js'; import { formatOutput } from '../../utils/output-formatter.js'; /** - * `os projects bind` — bind a locally-compiled artifact to an existing + * `os environments bind` — bind a locally-compiled artifact to an existing * multi-environment server project. * * Equivalent to `PATCH /api/v1/cloud/environments/` with @@ -20,13 +20,13 @@ import { formatOutput } from '../../utils/output-formatter.js'; * Use `--build` to compile `objectstack.config.ts` first so the artifact * reflects the latest source. */ -export default class ProjectsBind extends Command { +export default class EnvironmentsBind extends Command { static override description = 'Bind a local objectstack artifact to an existing project'; static override examples = [ - '$ os projects bind --artifact ./dist/objectstack.json', - '$ os projects bind --artifact ./dist/objectstack.json --build', - '$ os projects bind --reseed', + '$ os environments bind --artifact ./dist/objectstack.json', + '$ os environments bind --artifact ./dist/objectstack.json --build', + '$ os environments bind --reseed', ]; static override args = { @@ -59,7 +59,7 @@ export default class ProjectsBind extends Command { }; async run(): Promise { - const { args, flags } = await this.parse(ProjectsBind); + const { args, flags } = await this.parse(EnvironmentsBind); try { const artifactRel = flags.artifact ?? './dist/objectstack.json'; diff --git a/packages/cli/src/commands/environments/create.ts b/packages/cli/src/commands/environments/create.ts index 7dc50c25f9..3d1fe9bfe3 100644 --- a/packages/cli/src/commands/environments/create.ts +++ b/packages/cli/src/commands/environments/create.ts @@ -7,21 +7,21 @@ import { formatOutput } from '../../utils/output-formatter.js'; import { readAuthConfig, writeAuthConfig } from '../../utils/auth-config.js'; /** - * `os projects create` — provision a new project. + * `os environments create` — provision a new project. * * Delegates to `ProjectProvisioningService.provisionProject` on the server. * On success, optionally activates the new project for the current session * and persists `activeEnvironmentId` into `~/.objectstack/credentials.json` * (unless `--no-activate` is passed). */ -export default class ProjectsCreate extends Command { +export default class EnvironmentsCreate extends Command { static override description = 'Provision a new project'; static override examples = [ - '$ os projects create --org 00000000-0000-0000-0000-000000000000 --name Staging', - '$ os projects create --org $ORG --name Dev --plan free', - '$ os projects create --org $ORG --name "Clone" --clone-from --no-activate', - '$ os projects create --org $ORG --name CRM --artifact ./examples/app-crm/dist/objectstack.json', + '$ os environments create --org 00000000-0000-0000-0000-000000000000 --name Staging', + '$ os environments create --org $ORG --name Dev --plan free', + '$ os environments create --org $ORG --name "Clone" --clone-from --no-activate', + '$ os environments create --org $ORG --name CRM --artifact ./examples/app-crm/dist/objectstack.json', ]; static override flags = { @@ -56,7 +56,7 @@ export default class ProjectsCreate extends Command { }; async run(): Promise { - const { flags } = await this.parse(ProjectsCreate); + const { flags } = await this.parse(EnvironmentsCreate); try { const { client, token } = await createApiClient({ url: flags.url, token: flags.token }); diff --git a/packages/cli/src/commands/environments/environments.test.ts b/packages/cli/src/commands/environments/environments.test.ts index ef5dcc1139..cc566beb3c 100644 --- a/packages/cli/src/commands/environments/environments.test.ts +++ b/packages/cli/src/commands/environments/environments.test.ts @@ -1,13 +1,18 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { describe, it, expect } from 'vitest'; -import ProjectsList from './list.js'; -import ProjectsShow from './show.js'; -import ProjectsCreate from './create.js'; -import ProjectsSwitch from './switch.js'; +import { readFileSync, readdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import ts from 'typescript'; +import EnvironmentsBind from './bind.js'; +import EnvironmentsList from './list.js'; +import EnvironmentsShow from './show.js'; +import EnvironmentsCreate from './create.js'; +import EnvironmentsSwitch from './switch.js'; /** - * Metadata-only smoke tests for the `os projects ...` commands. We do + * Metadata-only smoke tests for the `os environments ...` commands. We do * not run the commands end-to-end (that would require an oclif Config * with hooks wired up); instead we assert that each command is a * well-formed oclif Command class with the flags / args we expect. @@ -18,31 +23,40 @@ import ProjectsSwitch from './switch.js'; * and the Chrome DevTools MCP smoke test in the PR description. */ -describe('os projects commands', () => { +describe('os environments commands', () => { + describe('bind', () => { + it('has the expected description and flags', () => { + expect(EnvironmentsBind.description).toMatch(/bind/i); + expect(EnvironmentsBind.flags).toHaveProperty('artifact'); + expect(EnvironmentsBind.flags).toHaveProperty('build'); + expect(EnvironmentsBind.flags).toHaveProperty('reseed'); + }); + }); + describe('list', () => { it('has the expected description and flags', () => { - expect(ProjectsList.description).toMatch(/list/i); - expect(ProjectsList.flags).toHaveProperty('org'); - expect(ProjectsList.flags).toHaveProperty('status'); - expect(ProjectsList.flags).toHaveProperty('format'); + expect(EnvironmentsList.description).toMatch(/list/i); + expect(EnvironmentsList.flags).toHaveProperty('org'); + expect(EnvironmentsList.flags).toHaveProperty('status'); + expect(EnvironmentsList.flags).toHaveProperty('format'); }); }); describe('show', () => { it('requires an id arg', () => { - expect(ProjectsShow.args).toHaveProperty('id'); - expect((ProjectsShow.args as any).id.required).toBe(true); + expect(EnvironmentsShow.args).toHaveProperty('id'); + expect((EnvironmentsShow.args as any).id.required).toBe(true); }); }); describe('create', () => { it('requires --org and --name', () => { - expect((ProjectsCreate.flags as any).org.required).toBe(true); - expect((ProjectsCreate.flags as any).name.required).toBe(true); + expect((EnvironmentsCreate.flags as any).org.required).toBe(true); + expect((EnvironmentsCreate.flags as any).name.required).toBe(true); }); it('activates by default with --no-activate opt-out', () => { - const flag = (ProjectsCreate.flags as any).activate; + const flag = (EnvironmentsCreate.flags as any).activate; expect(flag.default).toBe(true); expect(flag.allowNo).toBe(true); }); @@ -50,14 +64,307 @@ describe('os projects commands', () => { describe('switch', () => { it('requires an id arg', () => { - expect(ProjectsSwitch.args).toHaveProperty('id'); - expect((ProjectsSwitch.args as any).id.required).toBe(true); + expect(EnvironmentsSwitch.args).toHaveProperty('id'); + expect((EnvironmentsSwitch.args as any).id.required).toBe(true); }); it('calls the activate endpoint by default with --no-remote opt-out', () => { - const flag = (ProjectsSwitch.flags as any).remote; + const flag = (EnvironmentsSwitch.flags as any).remote; expect(flag.default).toBe(true); expect(flag.allowNo).toBe(true); }); }); }); + +/** + * Pin (#10967): every `examples` entry on EVERY CLI command source names a + * command id THIS CLI ACTUALLY REGISTERS. + * + * ## The failure this exists to refuse + * + * `static override examples` is printed verbatim as part of `--help`, and + * oclif's `pattern`-strategy command loader derives a command's id purely + * from its file PATH — a `commandsDir`-relative `topic/.../command.js` + * becomes id `topic:...:command` (`processCommandIds` in the installed + * `@oclif/core`'s `lib/config/plugin.js`: `id = [...topics, command] + * .filter(Boolean).join(sep)`, `topics` = the file's directory segments, + * `command` = the basename unless it is literally `index`). The exported + * class name plays NO part in that derivation. So a topic-directory rename + * (`projects/` → `environments/`, v5.0) leaves every command resolving + * exactly as before while any `examples` string spelling the OLD topic + * silently stops being true — not a parse error, not a type error, nothing + * a build catches. A user who copy-pastes the stale line hits `Error: + * Command projects:bind not found.` (exit 2). That is the shape #10967 + * fixed under `environments/*.ts`; this pin targets the MECHANISM (an + * example naming an id this CLI does not register), not the literal string + * `os projects`, so it keeps working for a topic nobody has renamed yet — + * a grep for `os projects` would go green the instant someone renamed this + * topic again to something else, or renamed a DIFFERENT topic entirely. + * + * ## Why the registered-id set is derived from SOURCE, not the built plugin + * + * The direct route — `Config.load()` against the built CLI and reading + * `config.commandIDs` — is unreachable from this suite: `turbo.json` + * declares `test`'s `dependsOn` as `["^build"]`, dependencies' builds only, + * never this package's OWN, so `packages/cli/dist` (what oclif's pattern + * strategy actually scans) is not guaranteed to exist when this file runs — + * the same constraint `child-env-source-loader.pin.test.ts` documents for + * the same reason. So `registeredCommandIds()` below re-derives the id set + * from `src/commands/**\/*.ts` using oclif's own algorithm instead: + * `src/commands` is a 1:1 mirror of `dist/commands` (the build renames or + * relocates nothing), so the set computed here is the one oclif will + * register once the package is actually built — verified below by asserting + * it contains known ids from several unrelated topics, including the + * `index.ts` special case (`migrate/index.ts` → id `migrate`, no trailing + * command segment). What this does NOT cover: a future build step that + * renamed or dropped a file between `src` and `dist` would be invisible + * here — nothing in this package's build does that today. + * + * ## Population: EVERY command source, via AST — not via `import` + * + * The property is checked against every non-test file under + * `packages/cli/src/commands/**`, not just the five #10967 touches — a + * five-file population is exactly the set that is already correct, so it + * cannot catch the defect class returning anywhere else (it did not catch + * it in `register.ts`/`whoami.ts`/`logout.ts`, discovered by hand below). + * `examples` is read via TypeScript AST (`extractExamples`), not by + * `import`ing 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 being tested — the same reasoning + * `child-env-source-loader.pin.test.ts` gives for reading command sources + * as text/AST rather than executing them. + * + * `extractExamples` handles every invocation shape actually present in this + * package (catalogued by hand across all command sources before writing + * this): a plain string (`'$ os topic cmd ...'`), the oclif help-template + * form (`'<%= config.bin %> cmd ...'` — `config.bin` is `"os"`, + * `package.json`'s `oclif.bin`), either of those prefixed by one or more + * `ENV=value` assignments (`'$ OS_CLOUD_URL=http://localhost:4000 os + * package publish'`, 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 with + * its own message (not silently skipped) — "prefer failing to falling + * back" (AGENTS.md, Route & surface ownership §3): a shape this pin cannot + * parse is exactly the shape that could hide a stale command undetected. + * + * ## The one exclusion, and why it must self-retire + * + * `register.ts` / `whoami.ts` / `logout.ts` are root-level commands whose + * `examples` say `os auth register` / `os auth whoami` / `os auth logout`, + * though no `auth` topic has ever existed for them (confirmed via + * `--help`: `Error: Command auth:whoami not found.`) — the same defect + * class as #10967, found by scanning the whole tree, but not #10967's to + * fix (outside its dispatched file surface). Filed as #11221. `EXCLUDED` + * carves exactly those three files out of the main assertion below, but a + * silent, permanent exemption is its own defect — a file excluded here + * stops being checked by this pin forever, even after #11221 lands and the + * excluded condition no longer holds. So a second `it.each` re-runs the + * SAME predicate over the excluded files and asserts it still finds an + * unresolved entry: when #11221's fix removes the last one, that assertion + * goes red on purpose, and the failure message says to delete the entry. + * The pattern (map-of-reason + filtered main assertion + a "still needs + * its exclusion" retiring assertion) matches + * `packages/create-objectstack/src/starter-comments-self-contained.test.ts`'s + * `EXCLUDED`, which has retired this same way before (#11022). + */ +describe('#10967 pin: examples resolve to a real command id', () => { + const ENVIRONMENTS_DIR = fileURLToPath(new URL('.', import.meta.url)); + const COMMANDS_ROOT = path.resolve(ENVIRONMENTS_DIR, '..'); + + /** oclif's own `topicSeparator` for this CLI (`packages/cli/package.json` → `oclif.topicSeparator`). */ + const TOPIC_SEPARATOR = ' '; + + const isCommandSource = (name: string): boolean => + name.endsWith('.ts') + && !name.endsWith('.d.ts') + && !/\.(test|pin\.test|contract\.test|integration\.test)\.ts$/.test(name); + + /** Every command source file, as a path relative to `COMMANDS_ROOT` (posix separators). */ + function commandSourceFiles(): string[] { + const out: string[] = []; + const walk = (dir: string, prefix: string): void => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const rel = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + walk(path.join(dir, entry.name), rel); + continue; + } + if (!isCommandSource(entry.name)) continue; + out.push(rel); + } + }; + walk(COMMANDS_ROOT, ''); + return out.sort(); + } + + function registeredCommandIds(): Set { + const ids = new Set(); + for (const rel of commandSourceFiles()) { + const parts = rel.split('/'); + const base = parts[parts.length - 1].slice(0, -'.ts'.length); + const topics = parts.slice(0, -1); + const command = base === 'index' ? undefined : base; + const id = [...topics, command].filter((s): s is string => Boolean(s)).join(TOPIC_SEPARATOR); + ids.add(id); + } + return ids; + } + + /** + * The `examples` entries a command source declares, read from the AST — + * string literals directly, and a `command` property off any object-shaped + * entry (`start.ts`'s two annotated examples). See the file header for why + * this reads source instead of importing the command class. + */ + function extractExamples(absPath: string): string[] { + const text = readFileSync(absPath, 'utf8'); + const sourceFile = ts.createSourceFile(absPath, text, ts.ScriptTarget.Latest, true); + const found: string[] = []; + + const stringValue = (node: ts.Node): string | undefined => + ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : undefined; + + const visit = (node: ts.Node): void => { + if ( + ts.isPropertyDeclaration(node) + && node.name && ts.isIdentifier(node.name) && node.name.text === 'examples' + && node.initializer && ts.isArrayLiteralExpression(node.initializer) + ) { + for (const el of node.initializer.elements) { + const direct = stringValue(el); + if (direct !== undefined) { + found.push(direct); + continue; + } + if (ts.isObjectLiteralExpression(el)) { + for (const prop of el.properties) { + if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'command') { + const value = stringValue(prop.initializer); + if (value !== undefined) found.push(value); + } + } + } + } + } + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + return found; + } + + /** A leading `ENV_VAR=value ` assignment — value may be double- or single-quoted (and contain spaces). */ + const ENV_ASSIGNMENT = /^[A-Z_][A-Z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+/; + const BIN_PREFIXES = ['os ', '<%= config.bin %> '] as const; + + /** + * Strips a recognised CLI-invocation prefix (`$ `, then zero or more `ENV=value` + * assignments, then the bin name) and returns what follows — the command + * tokens. `undefined` means the line does not match any recognised shape. + */ + function stripInvocationPrefix(raw: string): string | undefined { + let s = raw.trim(); + if (s.startsWith('$ ')) s = s.slice(2); + while (ENV_ASSIGNMENT.test(s)) s = s.replace(ENV_ASSIGNMENT, ''); + for (const prefix of BIN_PREFIXES) { + if (s.startsWith(prefix)) return s.slice(prefix.length); + } + return undefined; + } + + /** True when `raw` (an `examples` entry) invokes a registered command. */ + function namesARegisteredCommand(raw: string, ids: ReadonlySet): boolean { + const rest = stripInvocationPrefix(raw); + if (rest === undefined) return false; + for (const id of ids) { + if (rest === id || rest.startsWith(`${id} `)) return true; + } + return false; + } + + /** + * Files with a KNOWN, currently-live instance of this defect class this + * card does not own the fix for (#11221). The retiring assertion below + * proves each entry is still load-bearing, not decorative. + */ + const EXCLUDED = new Map([ + ['register.ts', '#11221: examples say `os auth register`, no `auth` topic exists'], + ['whoami.ts', '#11221: examples say `os auth whoami`, no `auth` topic exists'], + ['logout.ts', '#11221: examples say `os auth logout`, no `auth` topic exists'], + ]); + + const sourceFiles = commandSourceFiles(); + const registeredIds = registeredCommandIds(); + + it('the walk reached real command sources across multiple topics (not vacuously empty)', () => { + expect(sourceFiles).toContain('environments/bind.ts'); + expect(sourceFiles).toContain('migrate/index.ts'); // the index.ts special case + expect(sourceFiles).toContain('data/query.ts'); + expect(sourceFiles).toContain('start.ts'); // the object-shaped `examples` entries + expect(sourceFiles.length).toBeGreaterThan(30); + }); + + it('the registered-id set derived from the walk carries the same known ids', () => { + expect(registeredIds.has('environments list')).toBe(true); + expect(registeredIds.has('environments bind')).toBe(true); + expect(registeredIds.has('migrate')).toBe(true); + expect(registeredIds.has('data query')).toBe(true); + expect(registeredIds.size).toBeGreaterThan(30); + }); + + it('anti-vacuity: a known-good example is correctly cleared, not just never rejected', () => { + expect(namesARegisteredCommand('$ os environments list', registeredIds)).toBe(true); + expect(namesARegisteredCommand('$ os environments bind --reseed', registeredIds)).toBe(true); + // The other two recognised invocation shapes, cleared the same way. + expect(namesARegisteredCommand('<%= config.bin %> verify --rls', registeredIds)).toBe(true); + expect( + namesARegisteredCommand('$ OS_CLOUD_URL=http://localhost:4000 os package publish', registeredIds), + ).toBe(true); + }); + + it('reverse-verification: the PRE-FIX line is rejected, and rejected for the declared reason', () => { + const preFix = '$ os projects bind --artifact ./dist/objectstack.json'; + // The reason it must fail: no file tree produces this id any more. + expect(registeredIds.has('projects bind')).toBe(false); + expect(registeredIds.has('projects')).toBe(false); + expect(namesARegisteredCommand(preFix, registeredIds)).toBe(false); + }); + + it('anti-vacuity: the walk actually found examples entries to check, not just files', () => { + const total = sourceFiles + .filter((f) => !EXCLUDED.has(f)) + .reduce((n, f) => n + extractExamples(path.join(COMMANDS_ROOT, f)).length, 0); + expect(total).toBeGreaterThan(50); + }); + + it.each(sourceFiles.filter((f) => !EXCLUDED.has(f)))( + '%s: every examples entry names a registered command', + (rel) => { + const examples = extractExamples(path.join(COMMANDS_ROOT, rel)); + const unresolved = examples.filter((line) => !namesARegisteredCommand(line, registeredIds)); + expect( + unresolved, + `${rel} has an examples entry that does not resolve to a command this CLI registers -- ` + + 'either it names an id this CLI does not have (a user who copy-pastes it from --help ' + + 'gets "Command ... not found."), or it uses an invocation shape this pin does not ' + + 'recognise ($ os ..., <%= config.bin %> ..., each optionally prefixed by ENV=value ' + + 'assignments) -- extend stripInvocationPrefix if a new, legitimate shape is needed.', + ).toEqual([]); + }, + ); + + it.each([...EXCLUDED.keys()])('%s still needs its exclusion (owned by #11221)', (rel) => { + const examples = extractExamples(path.join(COMMANDS_ROOT, rel)); + const unresolved = examples.filter((line) => !namesARegisteredCommand(line, registeredIds)); + expect( + unresolved.length, + `${rel} no longer has an unresolved examples entry -- remove it from EXCLUDED above so it ` + + 'is scanned like every other command source. An exclusion kept past its cause is how a ' + + 'file stops being checked without anyone deciding to stop checking it.', + ).toBeGreaterThan(0); + }); +}); diff --git a/packages/cli/src/commands/environments/list.ts b/packages/cli/src/commands/environments/list.ts index 165102f827..892dd9f7af 100644 --- a/packages/cli/src/commands/environments/list.ts +++ b/packages/cli/src/commands/environments/list.ts @@ -6,19 +6,19 @@ import { createApiClient, requireAuth } from '../../utils/api-client.js'; import { formatOutput } from '../../utils/output-formatter.js'; /** - * `os projects list` — list projects visible to the current session. + * `os environments list` — list projects visible to the current session. * * Filters by organization via `--org`. Output format is the same * table/json/yaml shape used by other metadata commands, for a * consistent DX. */ -export default class ProjectsList extends Command { +export default class EnvironmentsList extends Command { static override description = 'List projects visible to the current session'; static override examples = [ - '$ os projects list', - '$ os projects list --org 00000000-0000-0000-0000-000000000000', - '$ os projects list --format json', + '$ os environments list', + '$ os environments list --org 00000000-0000-0000-0000-000000000000', + '$ os environments list --format json', ]; static override flags = { @@ -35,7 +35,7 @@ export default class ProjectsList extends Command { }; async run(): Promise { - const { flags } = await this.parse(ProjectsList); + const { flags } = await this.parse(EnvironmentsList); try { const { client, token, environmentId: activeId } = await createApiClient({ diff --git a/packages/cli/src/commands/environments/show.ts b/packages/cli/src/commands/environments/show.ts index 758cbde498..6c4b4f726b 100644 --- a/packages/cli/src/commands/environments/show.ts +++ b/packages/cli/src/commands/environments/show.ts @@ -6,17 +6,17 @@ import { createApiClient, requireAuth } from '../../utils/api-client.js'; import { formatOutput } from '../../utils/output-formatter.js'; /** - * `os projects show ` — show detailed information for a single project. + * `os environments show ` — show detailed information for a single project. * * Renders the project row plus its database, active credential, and * membership row (same shape as `client.projects.get(id)`). */ -export default class ProjectsShow extends Command { +export default class EnvironmentsShow extends Command { static override description = 'Show detailed information for a project'; static override examples = [ - '$ os projects show 00000000-0000-0000-0000-000000000001', - '$ os projects show proj-123 --format json', + '$ os environments show 00000000-0000-0000-0000-000000000001', + '$ os environments show proj-123 --format json', ]; static override args = { @@ -35,7 +35,7 @@ export default class ProjectsShow extends Command { }; async run(): Promise { - const { args, flags } = await this.parse(ProjectsShow); + const { args, flags } = await this.parse(EnvironmentsShow); try { const { client, token } = await createApiClient({ url: flags.url, token: flags.token }); diff --git a/packages/cli/src/commands/environments/switch.ts b/packages/cli/src/commands/environments/switch.ts index 758889aaa6..dcbe0648f6 100644 --- a/packages/cli/src/commands/environments/switch.ts +++ b/packages/cli/src/commands/environments/switch.ts @@ -6,7 +6,7 @@ import { createApiClient, requireAuth } from '../../utils/api-client.js'; import { readAuthConfig, writeAuthConfig } from '../../utils/auth-config.js'; /** - * `os projects switch ` — set the active project for this CLI session. + * `os environments switch ` — set the active project for this CLI session. * * Calls `POST /api/v1/cloud/environments/:id/activate` to update the * server-side session, then persists `activeEnvironmentId` into @@ -14,12 +14,12 @@ import { readAuthConfig, writeAuthConfig } from '../../utils/auth-config.js'; * client they create via `createApiClient`) automatically target this * project. */ -export default class ProjectsSwitch extends Command { +export default class EnvironmentsSwitch extends Command { static override description = 'Activate a project for subsequent CLI calls'; static override examples = [ - '$ os projects switch 00000000-0000-0000-0000-000000000001', - '$ os projects switch proj-123 --no-remote', + '$ os environments switch 00000000-0000-0000-0000-000000000001', + '$ os environments switch proj-123 --no-remote', ]; static override args = { @@ -37,7 +37,7 @@ export default class ProjectsSwitch extends Command { }; async run(): Promise { - const { args, flags } = await this.parse(ProjectsSwitch); + const { args, flags } = await this.parse(EnvironmentsSwitch); try { const { client, token } = await createApiClient({ url: flags.url, token: flags.token });