diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f7bc35..a9dc1c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.6.1 + +- Negotiate development-session capabilities with the control plane instead + of requiring one hard-coded capability vector. +- Accept boolean development capability values so a newly enabled platform + capability cannot turn a successful response into a client-side schema + failure. + ## 0.6.0 - Add `doctor` with redacted session, endpoint, CLI, and deployed-platform diagnostics. diff --git a/README.md b/README.md index da168c8..eb135aa 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,12 @@ offline source bundle, but cannot connect to or deploy through OpenCloud. ## Install a pinned release -OpenCloud application skills pin an exact CLI release. To install `v0.6.0` in +OpenCloud application skills pin an exact CLI release. To install `v0.6.1` in an isolated task directory: ```bash -OPENCLOUD_CLI_VERSION="v0.6.0" -OPENCLOUD_CLI_PACKAGE="opencloud-cli-0.6.0.tgz" +OPENCLOUD_CLI_VERSION="v0.6.1" +OPENCLOUD_CLI_PACKAGE="opencloud-cli-0.6.1.tgz" OPENCLOUD_CLI_DIR="$(mktemp -d)" curl -fsSLo "$OPENCLOUD_CLI_DIR/$OPENCLOUD_CLI_PACKAGE" \ diff --git a/package-lock.json b/package-lock.json index c60a81c..f86e04f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@opencloud/cli", - "version": "0.6.0", + "version": "0.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@opencloud/cli", - "version": "0.6.0", + "version": "0.6.1", "dependencies": { "playwright": "1.62.0" }, diff --git a/package.json b/package.json index 753a319..706bfb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@opencloud/cli", - "version": "0.6.0", + "version": "0.6.1", "description": "Versioned command-line client for building, deploying, and verifying OpenCloud applications", "type": "module", "bin": { @@ -18,7 +18,7 @@ "scripts": { "build": "node scripts/build.mjs", "typecheck": "tsc -p tsconfig.json --noEmit", - "test": "vitest run src vendor/contracts/src", + "test": "vitest run src vendor/contracts/src vendor/control-plane-client/src", "lint": "npm run typecheck", "prepack": "npm run build" }, diff --git a/src/index.ts b/src/index.ts index a38c4c1..ecebdd0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,7 @@ import { type OpenCloudSession, } from "./session-store.js"; -const CLI_VERSION = "0.6.0"; +const CLI_VERSION = "0.6.1"; const program = new Command() .name("opencloud") diff --git a/vendor/contracts/src/control-plane.test.ts b/vendor/contracts/src/control-plane.test.ts index 072b01b..2ad252c 100644 --- a/vendor/contracts/src/control-plane.test.ts +++ b/vendor/contracts/src/control-plane.test.ts @@ -55,4 +55,41 @@ describe("controlPlaneOperations", () => { expect(controlPlaneOperations.deployDraft.idempotency).toBe("required"); expect(controlPlaneOperations.verifyApp.idempotency).toBe("required"); }); + + it("treats development capabilities as negotiated booleans", () => { + const session = { + id: "11111111-1111-4111-8111-111111111111", + appId: "22222222-2222-4222-8222-222222222222", + draftId: "33333333-3333-4333-8333-333333333333", + status: "active", + previewUrl: "https://dev-example.opencloud.ai", + baseDeploymentId: null, + activeRevision: null, + verification: null, + capabilities: { + frontend: true, + database: true, + functions: true, + productionSecrets: false, + cron: false, + storageSandbox: false, + syntheticAuth: false, + }, + createdAt: "2026-08-05T00:00:00.000Z", + updatedAt: "2026-08-05T00:00:00.000Z", + lastActivityAt: "2026-08-05T00:00:00.000Z", + expiresAt: "2026-08-06T00:00:00.000Z", + }; + + for (const [name, enabled] of Object.entries(session.capabilities)) { + const parsed = controlPlaneOperations.getDevSession.output.parse({ + ...session, + capabilities: { + ...session.capabilities, + [name]: !enabled, + }, + }); + expect(parsed.capabilities).toMatchObject({ [name]: !enabled }); + } + }); }); diff --git a/vendor/contracts/src/control-plane.ts b/vendor/contracts/src/control-plane.ts index 67c8a1d..ed5357b 100644 --- a/vendor/contracts/src/control-plane.ts +++ b/vendor/contracts/src/control-plane.ts @@ -198,13 +198,13 @@ export const devSessionOutput = z }) .nullable(), capabilities: z.object({ - frontend: z.literal(true), - database: z.literal(true), - functions: z.literal(true), - productionSecrets: z.literal(false), - cron: z.literal(false), - storageSandbox: z.literal(false), - syntheticAuth: z.literal(false), + frontend: z.boolean(), + database: z.boolean(), + functions: z.boolean(), + productionSecrets: z.boolean(), + cron: z.boolean(), + storageSandbox: z.boolean(), + syntheticAuth: z.boolean(), }), createdAt: z.string(), updatedAt: z.string(), diff --git a/vendor/control-plane-client/src/index.test.ts b/vendor/control-plane-client/src/index.test.ts new file mode 100644 index 0000000..b997230 --- /dev/null +++ b/vendor/control-plane-client/src/index.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { + OPEN_CLOUD_CLIENT_CAPABILITIES_HEADER, + OPEN_CLOUD_DEV_SESSION_CAPABILITIES_V2, + OpenCloudClient, +} from "./index.js"; + +describe("OpenCloudClient", () => { + it("advertises support for negotiated development capabilities", async () => { + const requests: RequestInit[] = []; + const fetcher = async ( + _input: string | URL | Request, + init?: RequestInit, + ) => { + requests.push(init ?? {}); + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }; + const client = new OpenCloudClient({ + apiUrl: "https://api.opencloud.ai", + token: "test-token", + fetch: fetcher, + }); + + await client.get("/v1/apps"); + + const init = requests[0]; + expect(new Headers(init?.headers).get(OPEN_CLOUD_CLIENT_CAPABILITIES_HEADER)) + .toBe(OPEN_CLOUD_DEV_SESSION_CAPABILITIES_V2); + }); +}); diff --git a/vendor/control-plane-client/src/index.ts b/vendor/control-plane-client/src/index.ts index be16f4f..54573fc 100644 --- a/vendor/control-plane-client/src/index.ts +++ b/vendor/control-plane-client/src/index.ts @@ -6,6 +6,14 @@ import { type ControlPlaneOperationOutput, } from "@opencloud/contracts"; +export const OPEN_CLOUD_CLIENT_CAPABILITIES_HEADER = + "x-opencloud-client-capabilities"; +export const OPEN_CLOUD_DEV_SESSION_CAPABILITIES_V2 = + "dev-session-capabilities-v2"; + +const OPEN_CLOUD_CLIENT_CAPABILITIES = + OPEN_CLOUD_DEV_SESSION_CAPABILITIES_V2; + export interface ClientOptions { apiUrl: string; token?: string | undefined; @@ -142,6 +150,8 @@ export class OpenCloudClient { headers: { authorization: `Bearer ${this.options.token}`, "idempotency-key": idempotencyKey, + [OPEN_CLOUD_CLIENT_CAPABILITIES_HEADER]: + OPEN_CLOUD_CLIENT_CAPABILITIES, }, body: form, signal: AbortSignal.timeout(120_000), @@ -172,6 +182,8 @@ export class OpenCloudClient { accept: "application/json", ...(body === undefined ? {} : { "content-type": "application/json" }), ...(idempotencyKey ? { "idempotency-key": idempotencyKey } : {}), + [OPEN_CLOUD_CLIENT_CAPABILITIES_HEADER]: + OPEN_CLOUD_CLIENT_CAPABILITIES, }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), signal: AbortSignal.timeout(timeoutMs),