@supacloud/cli 0.75.1 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +14 -3
  2. package/dist/index.js +131 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -231,6 +231,7 @@ actions and also exposes the immutable prebuilt release workflow:
231
231
 
232
232
  ```bash
233
233
  supacloud-cli frontend list_releases --ref abc123 --id web
234
+ supacloud-cli frontend get_active_release --ref abc123 --id web
234
235
  supacloud-cli frontend get_release --ref abc123 --id web --release_id <sha256>
235
236
  supacloud-cli frontend upload_release --ref abc123 --id web --zip_path ./dist.zip
236
237
  supacloud-cli frontend activate_release --ref abc123 --id web \
@@ -238,6 +239,7 @@ supacloud-cli frontend activate_release --ref abc123 --id web \
238
239
  --expected_active_release_id absent \
239
240
  --expected_activation_id absent \
240
241
  --mutation_id <retry-stable-uuid-v4>
242
+ supacloud-cli frontend rollback --ref abc123 --id web --release_id <retained-sha256>
241
243
  ```
242
244
 
243
245
  `upload_release` hashes and streams an existing regular ZIP file without
@@ -245,10 +247,19 @@ buffering the full archive. The Management API binds the upload to that SHA-256,
245
247
  and the CLI reads the immutable release back before reporting success.
246
248
  `activate_release` uses both the observed active release and activation IDs as
247
249
  optimistic concurrency tokens, then verifies the authoritative active release.
248
- Use the values returned by `list_releases`; `absent` is valid only when no
249
- release has been activated. Production uploads and activations require the
250
+ Use the values returned by `get_active_release`; `absent` is valid only when no
251
+ release has been activated. Production uploads, activations and rollback require the
250
252
  normal exact `--confirm-production <ref>` value, and
251
- `SUPACLOUD_READ_ONLY=true` blocks both mutations.
253
+ `SUPACLOUD_READ_ONLY=true` blocks all mutations.
254
+
255
+ `get_active_release` automatically handles older servers returning HTTP 404
256
+ with a one-record history page and exact active artifact readback. It never
257
+ falls back for authentication, integrity or server failures.
258
+ `rollback` reads the current CAS identity and generates the mutation ID itself;
259
+ the retained target release must be explicit. It does not infer the previous
260
+ version from content-hash-sorted history, reupload assets or restore a database.
261
+ An uncertain result includes the mutation ID for read-only reconciliation and
262
+ must not be blindly retried.
252
263
 
253
264
  ### Immutable application releases
254
265
 
package/dist/index.js CHANGED
@@ -210793,8 +210793,8 @@ var ACTION_POLICY = {
210793
210793
  },
210794
210794
  secrets: { read: ["list"], write: ["upsert", "delete"] },
210795
210795
  frontend: {
210796
- read: ["list", "get", "build_logs", "list_frameworks", "list_records", "list_releases", "get_release"],
210797
- write: ["create", "update", "delete", "deploy_git", "deploy_upload", "redeploy", "add_domain", "remove_domain", "set_env", "upload_release", "activate_release"]
210796
+ read: ["list", "get", "build_logs", "list_frameworks", "list_records", "list_releases", "get_active_release", "get_release"],
210797
+ write: ["create", "update", "delete", "deploy_git", "deploy_upload", "redeploy", "add_domain", "remove_domain", "set_env", "upload_release", "activate_release", "rollback"]
210798
210798
  },
210799
210799
  task_events: { read: ["inspect_webhook"], write: ["register_webhook", "unregister_webhook"] },
210800
210800
  diagnostics: { read: ["list_checks", "get_run"], write: ["run_checks", "repair"] },
@@ -218159,12 +218159,15 @@ function releaseInventory(candidate, expected) {
218159
218159
  next_cursor: nextCursor
218160
218160
  };
218161
218161
  }
218162
- function releaseEndpoint(projectRef, deploymentId) {
218162
+ function deploymentEndpoint(projectRef, deploymentId) {
218163
218163
  if (!PROJECT_REF_PATTERN3.test(projectRef))
218164
218164
  throw new Error("'ref' is invalid for frontend releases");
218165
218165
  if (!DEPLOYMENT_ID_PATTERN.test(deploymentId))
218166
218166
  throw new Error("'id' is invalid for frontend releases");
218167
- return `/v1/projects/${encodeURIComponent(projectRef)}/frontend/deployments/${encodeURIComponent(deploymentId)}/releases`;
218167
+ return `/v1/projects/${encodeURIComponent(projectRef)}/frontend/deployments/${encodeURIComponent(deploymentId)}`;
218168
+ }
218169
+ function releaseEndpoint(projectRef, deploymentId) {
218170
+ return `${deploymentEndpoint(projectRef, deploymentId)}/releases`;
218168
218171
  }
218169
218172
  function releasePath(projectRef, deploymentId, releaseId) {
218170
218173
  if (!RELEASE_ID_PATTERN.test(releaseId))
@@ -218369,16 +218372,15 @@ function activationReceipt(candidate, input) {
218369
218372
  return release;
218370
218373
  }
218371
218374
  async function activeReleaseReadback(http, identity) {
218372
- const endpoint = releaseEndpoint(identity.projectRef, identity.deploymentId);
218373
- const inventoryRead = await http.get(`${endpoint}?limit=${RELEASE_LIST_LIMIT_MAX}`, {
218374
- maxJsonBytes: RESPONSE_MAX_BYTES
218375
- });
218376
- const inventory = inventoryRead.ok ? releaseInventory(inventoryRead.data, {
218377
- projectRef: identity.projectRef,
218378
- deploymentId: identity.deploymentId
218379
- }) : null;
218380
- if (!inventory || inventory.releases.length > RELEASE_LIST_LIMIT_MAX || inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
218381
- return { release: null, status: inventoryRead.status };
218375
+ const snapshot = await readActiveFrontendRelease(http, identity.projectRef, identity.deploymentId);
218376
+ if ("failure" in snapshot)
218377
+ return { release: null, status: snapshot.status };
218378
+ const inventory = snapshot.inventory;
218379
+ if (inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
218380
+ return { release: null, status: 200 };
218381
+ }
218382
+ if (snapshot.exactReadback) {
218383
+ return { release: inventory.releases[0] ?? null, status: 200 };
218382
218384
  }
218383
218385
  const releaseRead = await http.get(releasePath(identity.projectRef, identity.deploymentId, identity.releaseId), { maxJsonBytes: RESPONSE_MAX_BYTES });
218384
218386
  const release = releaseRead.ok ? releaseEnvelope(releaseRead.data, {
@@ -218388,6 +218390,103 @@ async function activeReleaseReadback(http, identity) {
218388
218390
  }) : null;
218389
218391
  return { release, status: releaseRead.status };
218390
218392
  }
218393
+ async function readActiveFrontendRelease(http, projectRef, deploymentId) {
218394
+ const response = await http.get(`${deploymentEndpoint(projectRef, deploymentId)}/active-release`, {
218395
+ maxJsonBytes: RESPONSE_MAX_BYTES
218396
+ });
218397
+ const inventory = response.ok && response.status === 200 ? releaseInventory(response.data, { projectRef, deploymentId }) : null;
218398
+ if (inventory) {
218399
+ if (inventory.next_cursor !== null || inventory.releases.length !== (inventory.active_release_id === null ? 0 : 1) || inventory.releases[0] && inventory.releases[0].release_id !== inventory.active_release_id) {
218400
+ return { failure: releaseReadFailure("frontend.get_active_release", response), status: response.status };
218401
+ }
218402
+ return { inventory, exactReadback: false };
218403
+ }
218404
+ if (response.ok || response.status !== 404) {
218405
+ return { failure: releaseReadFailure("frontend.get_active_release", response), status: response.status };
218406
+ }
218407
+ const historyResponse = await http.get(`${releaseEndpoint(projectRef, deploymentId)}?limit=1`, {
218408
+ maxJsonBytes: RESPONSE_MAX_BYTES
218409
+ });
218410
+ const history = historyResponse.ok && historyResponse.status === 200 ? releaseInventory(historyResponse.data, { projectRef, deploymentId }) : null;
218411
+ if (!history || history.releases.length > 1) {
218412
+ return {
218413
+ failure: releaseReadFailure("frontend.get_active_release", historyResponse),
218414
+ status: historyResponse.status
218415
+ };
218416
+ }
218417
+ if (history.active_release_id === null) {
218418
+ return { inventory: { ...history, releases: [], next_cursor: null }, exactReadback: false };
218419
+ }
218420
+ const exactResponse = await http.get(releasePath(projectRef, deploymentId, history.active_release_id), {
218421
+ maxJsonBytes: RESPONSE_MAX_BYTES
218422
+ });
218423
+ const release = exactResponse.ok && exactResponse.status === 200 ? releaseEnvelope(exactResponse.data, { projectRef, deploymentId, releaseId: history.active_release_id }) : null;
218424
+ if (!release) {
218425
+ return {
218426
+ failure: releaseReadFailure("frontend.get_active_release", exactResponse),
218427
+ status: exactResponse.status
218428
+ };
218429
+ }
218430
+ return { inventory: { ...history, releases: [release], next_cursor: null }, exactReadback: true };
218431
+ }
218432
+ async function getActiveFrontendRelease(http, projectRef, deploymentId) {
218433
+ const snapshot = await readActiveFrontendRelease(http, projectRef, deploymentId);
218434
+ return "failure" in snapshot ? snapshot.failure : toolResponse(snapshot.inventory);
218435
+ }
218436
+ async function readFrontendReleaseAuthority(http, projectRef, deploymentId) {
218437
+ return getActiveFrontendRelease(http, projectRef, deploymentId);
218438
+ }
218439
+ async function rollbackFrontendRelease(http, projectRef, deploymentId, releaseId) {
218440
+ if (!RELEASE_ID_PATTERN.test(releaseId))
218441
+ throw new Error("'release_id' must be a SHA-256 digest");
218442
+ const authority = await readActiveFrontendRelease(http, projectRef, deploymentId);
218443
+ if ("failure" in authority)
218444
+ return authority.failure;
218445
+ const inventory = authority.inventory;
218446
+ if (inventory.active_release_id === releaseId) {
218447
+ return toolResponse({
218448
+ project_ref: projectRef,
218449
+ deployment_id: deploymentId,
218450
+ active_release_id: releaseId,
218451
+ activation_id: inventory.active_activation_id,
218452
+ unchanged: true
218453
+ });
218454
+ }
218455
+ if (inventory.active_release_id === null || inventory.active_activation_id === null) {
218456
+ return releaseFailure("frontend.rollback", "NO_ACTIVE_RELEASE", 409);
218457
+ }
218458
+ const target = await getFrontendRelease(http, projectRef, deploymentId, releaseId);
218459
+ if (target.isError)
218460
+ return target;
218461
+ const mutationId = crypto.randomUUID();
218462
+ const activation = await activateFrontendRelease(http, {
218463
+ projectRef,
218464
+ deploymentId,
218465
+ releaseId,
218466
+ expectedActiveReleaseId: inventory.active_release_id,
218467
+ expectedActivationId: inventory.active_activation_id,
218468
+ mutationId
218469
+ });
218470
+ if (activation.isError) {
218471
+ return {
218472
+ ...toolResponse({
218473
+ ...JSON.parse(activation.content[0].text),
218474
+ operation: "frontend.rollback",
218475
+ mutation_id: mutationId,
218476
+ target_release_id: releaseId,
218477
+ expected_active_release_id: inventory.active_release_id,
218478
+ expected_activation_id: inventory.active_activation_id
218479
+ }),
218480
+ isError: true
218481
+ };
218482
+ }
218483
+ return toolResponse({
218484
+ ...JSON.parse(activation.content[0].text),
218485
+ rollback: true,
218486
+ previous_release_id: inventory.active_release_id,
218487
+ previous_activation_id: inventory.active_activation_id
218488
+ });
218489
+ }
218391
218490
  async function activateFrontendRelease(http, input) {
218392
218491
  const mutationId = input.mutationId;
218393
218492
  if (!MUTATION_ID_PATTERN.test(mutationId))
@@ -218454,7 +218553,7 @@ async function activationReadback(http, input, mutationId, activationStatus) {
218454
218553
  // src/shared/tools/frontend-tools.ts
218455
218554
  function registerFrontendTools(server2, http) {
218456
218555
  registerTool(server2, "frontend", `Frontend hosting and immutable prebuilt releases. Supports: static, react, vue, svelte, sveltekit, sveltekit-static, nextjs, nuxt, astro.
218457
- Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy, build_logs, add_domain, remove_domain, set_env, list_frameworks, list_records, list_releases, get_release, upload_release, activate_release`, {
218556
+ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy, build_logs, add_domain, remove_domain, set_env, list_frameworks, list_records, list_releases, get_active_release, get_release, upload_release, activate_release, rollback`, {
218458
218557
  action: withDescription(stringEnum([
218459
218558
  "list",
218460
218559
  "get",
@@ -218471,9 +218570,11 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
218471
218570
  "list_frameworks",
218472
218571
  "list_records",
218473
218572
  "list_releases",
218573
+ "get_active_release",
218474
218574
  "get_release",
218475
218575
  "upload_release",
218476
- "activate_release"
218576
+ "activate_release",
218577
+ "rollback"
218477
218578
  ]), "Action"),
218478
218579
  ref: optional(String2(), "Project ref"),
218479
218580
  id: optional(String2(), "Deployment ID"),
@@ -218489,7 +218590,7 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
218489
218590
  git_url: optional(String2(), "[deploy_git] Git repository URL"),
218490
218591
  branch: optional(String2(), "[deploy_git] Branch (default: main)"),
218491
218592
  zip_path: optional(String2(), "[deploy_upload/upload_release] Local ZIP file path"),
218492
- release_id: optional(String2(), "[get_release/activate_release] SHA-256 release ID"),
218593
+ release_id: optional(String2(), "[get_release/activate_release/rollback] SHA-256 release ID"),
218493
218594
  expected_active_release_id: optional(String2(), "[activate_release] Current release SHA-256 or absent"),
218494
218595
  expected_activation_id: optional(String2(), "[activate_release] Current activation UUIDv4 or absent"),
218495
218596
  mutation_id: optional(String2(), "[activate_release] Required retry-stable UUIDv4"),
@@ -218634,6 +218735,10 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
218634
218735
  need("id", id);
218635
218736
  need("release_id", release_id);
218636
218737
  return getFrontendRelease(http, ref, id, release_id);
218738
+ case "get_active_release":
218739
+ need("ref", ref);
218740
+ need("id", id);
218741
+ return getActiveFrontendRelease(http, ref, id);
218637
218742
  case "upload_release":
218638
218743
  need("ref", ref);
218639
218744
  need("id", id);
@@ -218654,6 +218759,11 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
218654
218759
  expectedActivationId: expected_activation_id,
218655
218760
  mutationId: mutation_id
218656
218761
  });
218762
+ case "rollback":
218763
+ need("ref", ref);
218764
+ need("id", id);
218765
+ need("release_id", release_id);
218766
+ return rollbackFrontendRelease(http, ref, id, release_id);
218657
218767
  default:
218658
218768
  text = `❌ Unknown action`;
218659
218769
  }
@@ -252167,7 +252277,7 @@ function registerDeployTools(server2, http, options = {}) {
252167
252277
  report("package", relative17(projectDirectory, outputDirectory) || ".");
252168
252278
  const archive = await createFrontendArchive(outputDirectory);
252169
252279
  try {
252170
- const inventory = payload(await listFrontendReleases(http, projectRef, selected.id, undefined, 100));
252280
+ const inventory = payload(await readFrontendReleaseAuthority(http, projectRef, selected.id));
252171
252281
  const activeReleaseId = typeof inventory.active_release_id === "string" ? inventory.active_release_id : null;
252172
252282
  const activeActivationId = typeof inventory.active_activation_id === "string" ? inventory.active_activation_id : null;
252173
252283
  if (archive.sha256 === activeReleaseId) {
@@ -252210,6 +252320,8 @@ function registerDeployTools(server2, http, options = {}) {
252210
252320
  deployment_id: selected.id,
252211
252321
  release_id: activated.active_release_id,
252212
252322
  activation_id: activated.activation_id,
252323
+ previous_release_id: activeReleaseId,
252324
+ previous_activation_id: activeActivationId,
252213
252325
  url: finalDeployment.deploymentUrl,
252214
252326
  file_count: archive.fileCount,
252215
252327
  source_bytes: archive.sourceBytes,
@@ -252492,7 +252604,7 @@ function registerRemoteDevTools(server2, options = {}) {
252492
252604
  // package.json
252493
252605
  var package_default = {
252494
252606
  name: "@supacloud/cli",
252495
- version: "0.75.1",
252607
+ version: "0.76.0",
252496
252608
  description: "Project-scoped CLI for SupaCloud users",
252497
252609
  type: "module",
252498
252610
  main: "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.75.1",
3
+ "version": "0.76.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",