|
| 1 | +--- |
| 2 | +"@objectstack/client": minor |
| 3 | +"@objectstack/cli": minor |
| 4 | +--- |
| 5 | + |
| 6 | +feat(client,cli)!: `client.projects.*` becomes `client.environments.*`, the scoped sub-client becomes `ScopedEnvironmentClient`, and the unwrap keys follow the wire (#12866, #12882, ADR-0006 D2) |
| 7 | + |
| 8 | +<!-- adr-0087: not-required (runtime-interface-only packages/client/src/index.ts#ObjectStackClient) The renamed surface is a member of a published runtime TypeScript class and the response-key shapes it declares inline. There is no Zod schema, no `packages/spec` declaration, no authorable key and no stored representation behind either half — measured 2026-08-28: zero `projects` envelope contracts anywhere in `packages/spec/src`, positive control being that `environments` hits do exist there. So `objectstack migrate meta` has nothing to visit and there is no tombstone to mint. The channel that reaches every affected consumer is the COMPILER, at the call site, which is strictly more precise than a ledger line; the wire half is carried by the paired control-plane release in the same coordinated window. MEASURED CAVEAT, recorded here rather than worked around: this claim is REFUSED by check-adr-0087-registration at step 4, because `packages/spec/src/api/contract.zod.ts` names `ObjectStackClient` in a JSDoc PROSE comment (line 164, describing what `unwrapResponse` keys on) while neither declaring nor importing it, and the predicate does not strip comments before scanning a metadata surface for references. Steps 1-3 pass. The disposition is left stated rather than swapped for `no-migration-prescription`, which would mechanically pass only through a detector blind spot while contradicting the migration table below it — the exact anti-pattern this gate's own header records as #8299. --> |
| 9 | + |
| 10 | + |
| 11 | +**BREAKING** public-API rename on `@objectstack/client`, and a breaking change to |
| 12 | +the `--format json` payload of the `os environments` command family. It lands |
| 13 | +after the v17.0.0 cut, so the lockstep launch-window convention ships it as |
| 14 | +`minor` (`scripts/check-changeset-no-major.mjs`); the version number is not the |
| 15 | +migration signal here, this entry is. |
| 16 | + |
| 17 | +This is the **SDK half** of one coordinated cross-repo rename. The **producer |
| 18 | +half** is the cloud control plane, which renames the same field keys on the same |
| 19 | +endpoints. Neither half ships alone: shipping the SDK half by itself is |
| 20 | +ADR-0006 D3, permanently declined, as is any mapping layer between the two |
| 21 | +spellings. |
| 22 | + |
| 23 | +## Migration |
| 24 | + |
| 25 | +**No aliases exist.** The old namespace is gone, not deprecated — there is no |
| 26 | +`client.projects` getter, no `res.project ?? res.environment` hedge, and none is |
| 27 | +coming (ADR-0006 D3 declined a mapping layer with reasons; the v5.0 rename rule |
| 28 | +「no aliases」 is the standing one). Every call site moves in one edit. |
| 29 | + |
| 30 | +### Method namespace |
| 31 | + |
| 32 | +| before | after | |
| 33 | +| --- | --- | |
| 34 | +| `client.projects.list(…)` | `client.environments.list(…)` | |
| 35 | +| `client.projects.get(id)` | `client.environments.get(id)` | |
| 36 | +| `client.projects.create(req)` | `client.environments.create(req)` | |
| 37 | +| `client.projects.update(id, patch)` | `client.environments.update(id, patch)` | |
| 38 | +| `client.projects.delete(id, opts)` | `client.environments.delete(id, opts)` | |
| 39 | +| `client.projects.activate(id)` | `client.environments.activate(id)` | |
| 40 | +| `client.projects.rotateCredential(…)` | `client.environments.rotateCredential(…)` | |
| 41 | +| `client.projects.updateHostname(…)` | `client.environments.updateHostname(…)` | |
| 42 | +| `client.projects.updateVisibility(…)` | `client.environments.updateVisibility(…)` | |
| 43 | +| `client.projects.listRevisions(…)` | `client.environments.listRevisions(…)` | |
| 44 | +| `client.projects.listBranches(id)` | `client.environments.listBranches(id)` | |
| 45 | +| `client.projects.renameBranch(…)` | `client.environments.renameBranch(…)` | |
| 46 | +| `client.projects.deleteBranch(…)` | `client.environments.deleteBranch(…)` | |
| 47 | +| `client.projects.retryProvisioning(id)` | `client.environments.retryProvisioning(id)` | |
| 48 | +| `client.projects.listDrivers()` | `client.environments.listDrivers()` | |
| 49 | +| `client.projects.packages.*` | `client.environments.packages.*` | |
| 50 | + |
| 51 | +The URL paths are unchanged — they were already on the `environments` spelling |
| 52 | +(`/api/v1/cloud/environments/…`). Only the method namespace and the response |
| 53 | +field keys move. |
| 54 | + |
| 55 | +### Response keys |
| 56 | + |
| 57 | +| before | after | where | |
| 58 | +| --- | --- | --- | |
| 59 | +| `res.projects` | `res.environments` | `list` (the `total` key is unchanged) | |
| 60 | +| `res.project` | `res.environment` | `get`, `update`, `activate`, `updateHostname`, `updateVisibility`, `retryProvisioning` | |
| 61 | + |
| 62 | +The joined blocks on `get` (`database`, `credential`, `membership`, |
| 63 | +`organization`) keep their names, as do every `packages.*` key, the |
| 64 | +`delete`/`listBranches`/`renameBranch`/`deleteBranch` payloads (already |
| 65 | +`environmentId`-keyed), and `listRevisions`. |
| 66 | + |
| 67 | +### Two declarations that were false before this change |
| 68 | + |
| 69 | +Measured 2026-08-28 against the cloud repo's `main`, and corrected here rather |
| 70 | +than carried forward under a new spelling: |
| 71 | + |
| 72 | +- **`create` never answered a `project` key at all.** `POST /api/v1/cloud/environments` |
| 73 | + has always answered `{ environment, warnings, durationMs, hostnameAssignment? }`. |
| 74 | + The old `{ project: any; database: any }` declaration was not merely |
| 75 | + pre-rename, it was wrong against the running control plane — and |
| 76 | + `os environments create` read `res.project.id` through it, so the default |
| 77 | + `--activate` silently never activated and the table output printed |
| 78 | + `undefined`. Both are fixed by this rename. |
| 79 | +- **`create` declares no `database` key.** That route does not send one; the key |
| 80 | + was declared NON-optional, so `res.database.driver` typechecked and threw. The |
| 81 | + method that really answers a `database` block is `get`, which keeps it. |
| 82 | + |
| 83 | +The keys `create` does send beside `environment` (`warnings`, `durationMs`, |
| 84 | +`hostnameAssignment`) are deliberately still undeclared — adding them is new |
| 85 | +published surface and a separate decision. |
| 86 | + |
| 87 | +### The environment-scoped sub-client (#12882) |
| 88 | + |
| 89 | +The fourth `project`-spelled surface on the same class, folded in by the same |
| 90 | +maintainer ruling. ADR-0006's D1 census named three surfaces and missed this one; |
| 91 | +it was an oversight, not a deliberate retention. |
| 92 | + |
| 93 | +| before | after | |
| 94 | +| --- | --- | |
| 95 | +| `client.project(id)` | `client.environment(id)` | |
| 96 | +| `ScopedProjectClient` (exported class) | `ScopedEnvironmentClient` | |
| 97 | + |
| 98 | +Same no-alias rule: neither old spelling survives. `client.project(id)` is not a |
| 99 | +deprecated method, it is gone, and the exported class is gone under its old name |
| 100 | +— a `import { ScopedProjectClient }` fails at the import line, which is the |
| 101 | +loudest and most precise channel this change has. |
| 102 | + |
| 103 | +Nothing about the behaviour moves: the scoped client still prefixes |
| 104 | +`/api/v1/environments/:environmentId/...`, still exposes the same `data` / `meta` |
| 105 | +/ `batch` / `packages` shape, and the thrown guard message becomes |
| 106 | +`[ObjectStack] environment(id): environmentId is required`. |
| 107 | + |
| 108 | +**Deliberately NOT renamed, because each is a different surface needing its own |
| 109 | +decision:** `setProjectId` / `getProjectId` on the client — `getProjectId` is a |
| 110 | +cross-package protocol contract that `packages/runtime` and |
| 111 | +`packages/metadata-protocol` both speak, so it is a coordinated rename, not a |
| 112 | +local one — and the REST API config keys `enableProjectScoping` / |
| 113 | +`projectResolution`, which are live keys read by `packages/cli/src/commands/serve.ts`. |
| 114 | +The docblocks that name them are worded so they stay true. |
| 115 | + |
| 116 | +Note for whoever compiles the release notes: four other pending changesets in |
| 117 | +this release describe methods on `ScopedProjectClient` under its old name |
| 118 | +(`client-unannotated-return-erasure`, `client-saveitem-ifmatch-header`, |
| 119 | +`client-meta-saveitem-query-options`, `client-precise-sdk-return-types`). They |
| 120 | +were accurate when written and are deliberately left alone; this entry is the one |
| 121 | +that renames the class. |
| 122 | + |
| 123 | +### JSDoc |
| 124 | + |
| 125 | +The `create` docblock claimed the server delegates to |
| 126 | +`ProjectProvisioningService.provisionProject`. That spelling has zero hits in the |
| 127 | +control plane (measured 2026-08-28). Both this SDK's docblock and |
| 128 | +`os environments create`'s now name the **endpoint** instead, which is the one |
| 129 | +identifier an in-repo reader can verify — the class lives in a repo this one |
| 130 | +never compiles against, so no gate here could ever have caught the rot. |
| 131 | + |
| 132 | +## CLI |
| 133 | + |
| 134 | +`os environments list | show | create | switch | bind` follow the same rename. |
| 135 | +No flag, argument, exit code or command id changes. `--format json` / `--format yaml` |
| 136 | +payloads are `formatOutput(res, …)` straight from the control-plane response, so |
| 137 | +their top-level keys change with the wire: a script reading `.projects` or |
| 138 | +`.project` from those payloads reads `.environments` / `.environment` instead. |
0 commit comments