@supacloud/cli 0.33.0 → 0.34.1

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 +26 -0
  2. package/dist/index.js +557 -48
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -106,6 +106,32 @@ loopback development origins, with the default `:80` likewise omitted. Use
106
106
  `SUPACLOUD_PROJECT_REF` when it cannot be inferred from a managed
107
107
  `<ref>.api.*` application hostname.
108
108
 
109
+ ### Immutable frontend releases
110
+
111
+ The `frontend` command keeps the existing deployment, Git, and legacy ZIP
112
+ actions and also exposes the immutable prebuilt release workflow:
113
+
114
+ ```bash
115
+ supacloud-cli frontend list_releases --ref abc123 --id web
116
+ supacloud-cli frontend get_release --ref abc123 --id web --release_id <sha256>
117
+ supacloud-cli frontend upload_release --ref abc123 --id web --zip_path ./dist.zip
118
+ supacloud-cli frontend activate_release --ref abc123 --id web \
119
+ --release_id <sha256> \
120
+ --expected_active_release_id absent \
121
+ --expected_activation_id absent \
122
+ --mutation_id <retry-stable-uuid-v4>
123
+ ```
124
+
125
+ `upload_release` hashes and streams an existing regular ZIP file without
126
+ buffering the full archive. The Management API binds the upload to that SHA-256,
127
+ and the CLI reads the immutable release back before reporting success.
128
+ `activate_release` uses both the observed active release and activation IDs as
129
+ optimistic concurrency tokens, then verifies the authoritative active release.
130
+ Use the values returned by `list_releases`; `absent` is valid only when no
131
+ release has been activated. Production uploads and activations require the
132
+ normal exact `--confirm-production <ref>` value, and
133
+ `SUPACLOUD_READ_ONLY=true` blocks both mutations.
134
+
109
135
  ### Verified release controls
110
136
 
111
137
  `release` is an official CLI entry point for verified Management API controls
package/dist/index.js CHANGED
@@ -6514,8 +6514,8 @@ var ACTION_POLICY = {
6514
6514
  },
6515
6515
  secrets: { read: ["list"], write: ["upsert", "delete"] },
6516
6516
  frontend: {
6517
- read: ["list", "get", "build_logs", "list_frameworks", "list_records"],
6518
- write: ["create", "update", "delete", "deploy_git", "deploy_upload", "redeploy", "add_domain", "remove_domain", "set_env"]
6517
+ read: ["list", "get", "build_logs", "list_frameworks", "list_records", "list_releases", "get_release"],
6518
+ write: ["create", "update", "delete", "deploy_git", "deploy_upload", "redeploy", "add_domain", "remove_domain", "set_env", "upload_release", "activate_release"]
6519
6519
  },
6520
6520
  task_events: { read: ["inspect_webhook"], write: ["register_webhook", "unregister_webhook"] },
6521
6521
  diagnostics: { read: ["list_checks", "get_run"], write: ["run_checks", "repair"] },
@@ -6875,6 +6875,7 @@ class HttpTransport {
6875
6875
  async post(path, body, options) {
6876
6876
  const timeoutMs = validatedPostTimeout(options);
6877
6877
  const maxJsonBytes = validatedJsonResponseLimit(options?.maxJsonBytes);
6878
+ const responseTimeoutMs = validatedResponseTimeout(options?.responseTimeoutMs);
6878
6879
  try {
6879
6880
  if (maxJsonBytes === undefined) {
6880
6881
  return await this.mutationWithResponseReader("POST", path, serializedRequestBody(body), responseJsonOrNull, timeoutMs);
@@ -6884,7 +6885,7 @@ class HttpTransport {
6884
6885
  headers: this.headers(),
6885
6886
  body: serializedRequestBody(body)
6886
6887
  }, timeoutMs);
6887
- const data = await boundedResponseJson(response, maxJsonBytes);
6888
+ const data = await boundedResponseJson(response, maxJsonBytes, responseTimeoutMs);
6888
6889
  return data === null ? responseReadFailure(response.status) : { ok: response.ok, status: response.status, data };
6889
6890
  } catch (error) {
6890
6891
  return transportFailure(error);
@@ -6894,6 +6895,39 @@ class HttpTransport {
6894
6895
  const timeoutMs = validatedPostTimeout(options);
6895
6896
  return this.mutationWithResponseReader("POST", path, serializedRequestBody(body), releaseMutationResponseJson, timeoutMs);
6896
6897
  }
6898
+ async postBinary(path, body, options) {
6899
+ if (options.contentType !== "application/zip") {
6900
+ throw new Error("Binary HTTP content type is invalid");
6901
+ }
6902
+ if (!Number.isSafeInteger(options.contentLength) || options.contentLength < 1 || options.contentLength !== body.byteLength) {
6903
+ throw new RangeError("Binary HTTP body length is invalid");
6904
+ }
6905
+ if (!/^[0-9a-f]{64}$/u.test(options.contentSha256)) {
6906
+ throw new Error("Binary HTTP body SHA-256 is invalid");
6907
+ }
6908
+ const maxJsonBytes = validatedJsonResponseLimit(options.maxJsonBytes);
6909
+ const timeoutMs = validatedPostTimeout(options);
6910
+ const responseTimeoutMs = validatedResponseTimeout(options.responseTimeoutMs);
6911
+ try {
6912
+ const request = {
6913
+ method: "POST",
6914
+ headers: {
6915
+ Authorization: `Bearer ${this.token}`,
6916
+ "Content-Type": options.contentType,
6917
+ "Content-Length": String(options.contentLength),
6918
+ "x-supacloud-content-sha256": options.contentSha256,
6919
+ ...this.apiKey ? { apikey: this.apiKey } : {}
6920
+ },
6921
+ body: body.stream,
6922
+ duplex: "half"
6923
+ };
6924
+ const response = await fetchWithRetry(`${this.baseUrl}${path}`, request, timeoutMs);
6925
+ const data = await boundedResponseJson(response, maxJsonBytes, responseTimeoutMs);
6926
+ return data === null ? responseReadFailure(response.status) : { ok: response.ok, status: response.status, data };
6927
+ } catch (error) {
6928
+ return transportFailure(error);
6929
+ }
6930
+ }
6897
6931
  async patchReleaseMutation(path, body) {
6898
6932
  return this.mutationWithResponseReader("PATCH", path, serializedRequestBody(body), releaseMutationResponseJson);
6899
6933
  }
@@ -11034,9 +11068,418 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
11034
11068
  // src/shared/tools/frontend-tools.ts
11035
11069
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
11036
11070
  import { basename as basename3 } from "node:path";
11071
+
11072
+ // src/shared/tools/frontend-release-control.ts
11073
+ import { createHash as createHash3 } from "node:crypto";
11074
+ import { constants as fsConstants2 } from "node:fs";
11075
+ import { open } from "node:fs/promises";
11076
+ import { resolve as resolve3 } from "node:path";
11077
+ var PROJECT_REF_PATTERN3 = /^[A-Za-z0-9_-]{1,20}$/u;
11078
+ var DEPLOYMENT_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/u;
11079
+ var RELEASE_ID_PATTERN = /^[0-9a-f]{64}$/u;
11080
+ var MUTATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
11081
+ var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
11082
+ var ARCHIVE_MAX_BYTES = 100 * 1024 * 1024;
11083
+ var ARCHIVE_CHUNK_BYTES = 64 * 1024;
11084
+ var RESPONSE_MAX_BYTES = 1024 * 1024;
11085
+ var UPLOAD_REQUEST_TIMEOUT_MS = 10 * 60000;
11086
+ var MUTATION_RESPONSE_TIMEOUT_MS = 5000;
11087
+ var RELEASE_LIST_LIMIT_MAX = 100;
11088
+ function toolResponse(payload) {
11089
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
11090
+ }
11091
+ function releaseFailure(operation, code, status) {
11092
+ return {
11093
+ isError: true,
11094
+ content: [{
11095
+ type: "text",
11096
+ text: JSON.stringify({ ok: false, operation, error: { code, http_status: status } })
11097
+ }]
11098
+ };
11099
+ }
11100
+ function exactKeys(candidate, keys) {
11101
+ const actual = Object.keys(candidate).sort();
11102
+ const expected = [...keys].sort();
11103
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
11104
+ }
11105
+ function releaseRecord(candidate) {
11106
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
11107
+ return null;
11108
+ const record = candidate;
11109
+ const keys = [
11110
+ "schema",
11111
+ "project_ref",
11112
+ "deployment_id",
11113
+ "release_id",
11114
+ "sha256",
11115
+ "tree_sha256",
11116
+ "size_bytes",
11117
+ "file_count",
11118
+ "created_at",
11119
+ "kind"
11120
+ ];
11121
+ if (!exactKeys(record, keys) || record.schema !== "supacloud.frontend-release.v1" || typeof record.project_ref !== "string" || !PROJECT_REF_PATTERN3.test(record.project_ref) || typeof record.deployment_id !== "string" || !DEPLOYMENT_ID_PATTERN.test(record.deployment_id) || typeof record.release_id !== "string" || !RELEASE_ID_PATTERN.test(record.release_id) || record.sha256 !== record.release_id || typeof record.tree_sha256 !== "string" || !RELEASE_ID_PATTERN.test(record.tree_sha256) || !Number.isSafeInteger(record.size_bytes) || Number(record.size_bytes) < 1 || !Number.isSafeInteger(record.file_count) || Number(record.file_count) < 1 || !canonicalTimestamp(record.created_at) || record.kind !== "prebuilt_static")
11122
+ return null;
11123
+ return {
11124
+ project_ref: record.project_ref,
11125
+ deployment_id: record.deployment_id,
11126
+ release_id: record.release_id,
11127
+ sha256: record.release_id,
11128
+ tree_sha256: record.tree_sha256,
11129
+ size_bytes: Number(record.size_bytes),
11130
+ file_count: Number(record.file_count),
11131
+ created_at: record.created_at,
11132
+ kind: "prebuilt_static"
11133
+ };
11134
+ }
11135
+ function releaseEnvelope(candidate, expected) {
11136
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
11137
+ return null;
11138
+ const envelope = candidate;
11139
+ const release = exactKeys(envelope, ["project_ref", "deployment_id", "release"]) ? releaseRecord(envelope.release) : null;
11140
+ if (!release || envelope.project_ref !== expected.projectRef || envelope.deployment_id !== expected.deploymentId || release.project_ref !== expected.projectRef || release.deployment_id !== expected.deploymentId || release.release_id !== expected.releaseId)
11141
+ return null;
11142
+ return release;
11143
+ }
11144
+ function canonicalTimestamp(candidate) {
11145
+ if (typeof candidate !== "string" || !TIMESTAMP_PATTERN.test(candidate))
11146
+ return false;
11147
+ const milliseconds = Date.parse(candidate);
11148
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
11149
+ }
11150
+ function nullableIdentity(candidate, pattern) {
11151
+ if (candidate === null)
11152
+ return null;
11153
+ return typeof candidate === "string" && pattern.test(candidate) ? candidate : undefined;
11154
+ }
11155
+ function releaseInventory(candidate, expected) {
11156
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
11157
+ return null;
11158
+ const record = candidate;
11159
+ const keys = [
11160
+ "project_ref",
11161
+ "deployment_id",
11162
+ "active_release_id",
11163
+ "active_activation_id",
11164
+ "releases",
11165
+ "next_cursor"
11166
+ ];
11167
+ if (!exactKeys(record, keys) || typeof record.project_ref !== "string" || !PROJECT_REF_PATTERN3.test(record.project_ref) || typeof record.deployment_id !== "string" || !DEPLOYMENT_ID_PATTERN.test(record.deployment_id) || !Array.isArray(record.releases))
11168
+ return null;
11169
+ const activeReleaseId = nullableIdentity(record.active_release_id, RELEASE_ID_PATTERN);
11170
+ const activeActivationId = nullableIdentity(record.active_activation_id, MUTATION_ID_PATTERN);
11171
+ const nextCursor = nullableIdentity(record.next_cursor, RELEASE_ID_PATTERN);
11172
+ const releases = record.releases.map(releaseRecord);
11173
+ if (activeReleaseId === undefined || activeActivationId === undefined || nextCursor === undefined || activeReleaseId === null !== (activeActivationId === null) || releases.some((release) => release === null))
11174
+ return null;
11175
+ const verified = releases;
11176
+ if (new Set(verified.map((release) => release.release_id)).size !== verified.length || verified.some((release) => release.project_ref !== record.project_ref || release.deployment_id !== record.deployment_id) || expected && (record.project_ref !== expected.projectRef || record.deployment_id !== expected.deploymentId))
11177
+ return null;
11178
+ return {
11179
+ project_ref: record.project_ref,
11180
+ deployment_id: record.deployment_id,
11181
+ active_release_id: activeReleaseId,
11182
+ active_activation_id: activeActivationId,
11183
+ releases: verified,
11184
+ next_cursor: nextCursor
11185
+ };
11186
+ }
11187
+ function releaseEndpoint(projectRef2, deploymentId) {
11188
+ if (!PROJECT_REF_PATTERN3.test(projectRef2))
11189
+ throw new Error("'ref' is invalid for frontend releases");
11190
+ if (!DEPLOYMENT_ID_PATTERN.test(deploymentId))
11191
+ throw new Error("'id' is invalid for frontend releases");
11192
+ return `/v1/projects/${encodeURIComponent(projectRef2)}/frontend/deployments/${encodeURIComponent(deploymentId)}/releases`;
11193
+ }
11194
+ function releasePath(projectRef2, deploymentId, releaseId) {
11195
+ if (!RELEASE_ID_PATTERN.test(releaseId))
11196
+ throw new Error("'release_id' must be a SHA-256 digest");
11197
+ return `${releaseEndpoint(projectRef2, deploymentId)}/${releaseId}`;
11198
+ }
11199
+ function sameArchiveIdentity(left, right) {
11200
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
11201
+ }
11202
+ async function archiveSha256(handle, sizeBytes) {
11203
+ const hash2 = createHash3("sha256");
11204
+ const chunk = new Uint8Array(Math.min(sizeBytes, ARCHIVE_CHUNK_BYTES));
11205
+ for (let offset = 0;offset < sizeBytes; ) {
11206
+ const requested = Math.min(chunk.byteLength, sizeBytes - offset);
11207
+ const { bytesRead } = await handle.read(chunk, 0, requested, offset);
11208
+ if (bytesRead < 1)
11209
+ throw new Error("Frontend release archive changed while it was hashed");
11210
+ hash2.update(chunk.subarray(0, bytesRead));
11211
+ offset += bytesRead;
11212
+ }
11213
+ return hash2.digest("hex");
11214
+ }
11215
+ async function verifiedArchive(path) {
11216
+ const archivePath = resolve3(path);
11217
+ const handle = await open(archivePath, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
11218
+ try {
11219
+ const before = await handle.stat({ bigint: true });
11220
+ const sizeBytes = Number(before.size);
11221
+ if (!before.isFile() || sizeBytes < 1 || sizeBytes > ARCHIVE_MAX_BYTES) {
11222
+ throw new Error(`Frontend release archive must be a 1-${ARCHIVE_MAX_BYTES} byte regular file`);
11223
+ }
11224
+ const sha256 = await archiveSha256(handle, sizeBytes);
11225
+ const after = await handle.stat({ bigint: true });
11226
+ if (!sameArchiveIdentity(before, after)) {
11227
+ throw new Error("Frontend release archive identity changed while it was hashed");
11228
+ }
11229
+ return { handle, sizeBytes, sha256 };
11230
+ } catch (error) {
11231
+ await handle.close();
11232
+ throw error;
11233
+ }
11234
+ }
11235
+ function archiveStream(archive) {
11236
+ let offset = 0;
11237
+ return new ReadableStream({
11238
+ async pull(controller) {
11239
+ if (offset === archive.sizeBytes) {
11240
+ controller.close();
11241
+ return;
11242
+ }
11243
+ const length = Math.min(ARCHIVE_CHUNK_BYTES, archive.sizeBytes - offset);
11244
+ const chunk = new Uint8Array(length);
11245
+ const { bytesRead } = await archive.handle.read(chunk, 0, length, offset);
11246
+ if (bytesRead < 1)
11247
+ throw new Error("Frontend release archive changed while it was uploaded");
11248
+ offset += bytesRead;
11249
+ controller.enqueue(bytesRead === length ? chunk : chunk.subarray(0, bytesRead));
11250
+ }
11251
+ });
11252
+ }
11253
+ function releaseReadFailure(operation, response) {
11254
+ return releaseFailure(operation, response.ok ? "INVALID_RESPONSE" : "HTTP_ERROR", response.status);
11255
+ }
11256
+ async function listFrontendReleases(http, projectRef2, deploymentId, cursor, limit = 50) {
11257
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > RELEASE_LIST_LIMIT_MAX) {
11258
+ throw new Error("'limit' must be 1-100");
11259
+ }
11260
+ if (cursor !== undefined && !RELEASE_ID_PATTERN.test(cursor))
11261
+ throw new Error("'cursor' is invalid");
11262
+ const query = new URLSearchParams({ limit: String(limit) });
11263
+ if (cursor)
11264
+ query.set("cursor", cursor);
11265
+ const response = await http.get(`${releaseEndpoint(projectRef2, deploymentId)}?${query}`, {
11266
+ maxJsonBytes: RESPONSE_MAX_BYTES
11267
+ });
11268
+ const inventory = response.ok ? releaseInventory(response.data, { projectRef: projectRef2, deploymentId }) : null;
11269
+ if (!inventory || inventory.releases.length > limit) {
11270
+ return releaseReadFailure("frontend.list_releases", response);
11271
+ }
11272
+ return toolResponse(inventory);
11273
+ }
11274
+ async function getFrontendRelease(http, projectRef2, deploymentId, releaseId) {
11275
+ const response = await http.get(releasePath(projectRef2, deploymentId, releaseId), {
11276
+ maxJsonBytes: RESPONSE_MAX_BYTES
11277
+ });
11278
+ const release = response.ok ? releaseEnvelope(response.data, { projectRef: projectRef2, deploymentId, releaseId }) : null;
11279
+ if (!release) {
11280
+ return releaseReadFailure("frontend.get_release", response);
11281
+ }
11282
+ return toolResponse({ project_ref: projectRef2, deployment_id: deploymentId, release });
11283
+ }
11284
+ async function uploadFrontendRelease(http, projectRef2, deploymentId, archivePath) {
11285
+ const endpoint = releaseEndpoint(projectRef2, deploymentId);
11286
+ const archive = await verifiedArchive(archivePath);
11287
+ let response;
11288
+ try {
11289
+ response = await http.postBinary(endpoint, {
11290
+ stream: archiveStream(archive),
11291
+ byteLength: archive.sizeBytes
11292
+ }, {
11293
+ contentType: "application/zip",
11294
+ contentLength: archive.sizeBytes,
11295
+ contentSha256: archive.sha256,
11296
+ maxJsonBytes: RESPONSE_MAX_BYTES,
11297
+ timeoutMs: UPLOAD_REQUEST_TIMEOUT_MS,
11298
+ responseTimeoutMs: MUTATION_RESPONSE_TIMEOUT_MS
11299
+ });
11300
+ } finally {
11301
+ await archive.handle.close();
11302
+ }
11303
+ const release = response.ok ? releaseEnvelope(response.data, { projectRef: projectRef2, deploymentId, releaseId: archive.sha256 }) : null;
11304
+ if (!release) {
11305
+ if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
11306
+ return releaseFailure("frontend.upload_release", "HTTP_ERROR", response.status);
11307
+ }
11308
+ return uploadReadback(http, projectRef2, deploymentId, archive.sha256, response.status);
11309
+ }
11310
+ const readback = await http.get(`${endpoint}/${release.release_id}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
11311
+ const verified = readback.ok ? releaseEnvelope(readback.data, { projectRef: projectRef2, deploymentId, releaseId: release.release_id }) : null;
11312
+ if (!verified || verified.tree_sha256 !== release.tree_sha256) {
11313
+ return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", readback.status);
11314
+ }
11315
+ return toolResponse({ project_ref: projectRef2, deployment_id: deploymentId, release: verified });
11316
+ }
11317
+ async function uploadReadback(http, projectRef2, deploymentId, releaseId, uploadStatus) {
11318
+ const response = await http.get(releasePath(projectRef2, deploymentId, releaseId), {
11319
+ maxJsonBytes: RESPONSE_MAX_BYTES
11320
+ });
11321
+ const release = response.ok ? releaseEnvelope(response.data, { projectRef: projectRef2, deploymentId, releaseId }) : null;
11322
+ if (!release) {
11323
+ return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", uploadStatus);
11324
+ }
11325
+ return toolResponse({ project_ref: projectRef2, deployment_id: deploymentId, release });
11326
+ }
11327
+ function stableJson(candidate) {
11328
+ if (candidate === null || typeof candidate !== "object")
11329
+ return JSON.stringify(candidate);
11330
+ if (Array.isArray(candidate))
11331
+ return `[${candidate.map(stableJson).join(",")}]`;
11332
+ const record = candidate;
11333
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
11334
+ }
11335
+ function activationFingerprint(identity) {
11336
+ return createHash3("sha256").update(stableJson({
11337
+ project_ref: identity.projectRef,
11338
+ deployment_id: identity.deploymentId,
11339
+ release_id: identity.releaseId,
11340
+ expected_active_release_id: identity.expectedActiveReleaseId,
11341
+ activation_id: identity.mutationId,
11342
+ expected_activation_id: identity.expectedActivationId
11343
+ })).digest("hex");
11344
+ }
11345
+ function activationResourceKey(deploymentId) {
11346
+ return `v1/frontend_release/${Buffer.from(deploymentId, "utf8").toString("base64url")}`;
11347
+ }
11348
+ function publicMutationReceipt(candidate, expected) {
11349
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
11350
+ return null;
11351
+ const envelope = candidate;
11352
+ const mutation = envelope.mutation;
11353
+ const expectedKeys = [
11354
+ "project_ref",
11355
+ "mutation_id",
11356
+ "operation",
11357
+ "resource_key",
11358
+ "request_fingerprint",
11359
+ "principal",
11360
+ "status",
11361
+ "checkpoint",
11362
+ "receipt",
11363
+ "response_status",
11364
+ "failure_code",
11365
+ "lease",
11366
+ "completed_at",
11367
+ "created_at",
11368
+ "updated_at"
11369
+ ];
11370
+ if (!mutation || !exactKeys(envelope, ["project_ref", "mutation"]) || !exactKeys(mutation, expectedKeys) || envelope.project_ref !== expected.projectRef || mutation.project_ref !== expected.projectRef || mutation.mutation_id !== expected.mutationId || mutation.resource_key !== expected.resourceKey || mutation.request_fingerprint !== expected.requestFingerprint || typeof mutation.operation !== "string" || typeof mutation.status !== "string" || !(mutation.response_status === null || Number.isSafeInteger(mutation.response_status)) || !(mutation.failure_code === null || typeof mutation.failure_code === "string"))
11371
+ return null;
11372
+ return {
11373
+ operation: mutation.operation,
11374
+ status: mutation.status,
11375
+ responseStatus: mutation.response_status,
11376
+ failureCode: mutation.failure_code
11377
+ };
11378
+ }
11379
+ function activationReceipt(candidate, input) {
11380
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
11381
+ return null;
11382
+ const record = candidate;
11383
+ const release = releaseRecord(record.release);
11384
+ const mutation = record.mutation;
11385
+ if (!release || !mutation || !exactKeys(record, [
11386
+ "project_ref",
11387
+ "deployment_id",
11388
+ "active_release_id",
11389
+ "activation_id",
11390
+ "release",
11391
+ "mutation"
11392
+ ]) || !exactKeys(mutation, ["mutation_id", "status", "replayed"]) || record.project_ref !== input.projectRef || record.deployment_id !== input.deploymentId || record.active_release_id !== input.releaseId || record.activation_id !== input.mutationId || release.project_ref !== input.projectRef || release.deployment_id !== input.deploymentId || release.release_id !== input.releaseId || mutation.mutation_id !== input.mutationId || mutation.status !== "succeeded" || typeof mutation.replayed !== "boolean")
11393
+ return null;
11394
+ return release;
11395
+ }
11396
+ async function activeReleaseReadback(http, identity) {
11397
+ const endpoint = releaseEndpoint(identity.projectRef, identity.deploymentId);
11398
+ const inventoryRead = await http.get(`${endpoint}?limit=${RELEASE_LIST_LIMIT_MAX}`, {
11399
+ maxJsonBytes: RESPONSE_MAX_BYTES
11400
+ });
11401
+ const inventory = inventoryRead.ok ? releaseInventory(inventoryRead.data, {
11402
+ projectRef: identity.projectRef,
11403
+ deploymentId: identity.deploymentId
11404
+ }) : null;
11405
+ if (!inventory || inventory.releases.length > RELEASE_LIST_LIMIT_MAX || inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
11406
+ return { release: null, status: inventoryRead.status };
11407
+ }
11408
+ const releaseRead = await http.get(releasePath(identity.projectRef, identity.deploymentId, identity.releaseId), { maxJsonBytes: RESPONSE_MAX_BYTES });
11409
+ const release = releaseRead.ok ? releaseEnvelope(releaseRead.data, {
11410
+ projectRef: identity.projectRef,
11411
+ deploymentId: identity.deploymentId,
11412
+ releaseId: identity.releaseId
11413
+ }) : null;
11414
+ return { release, status: releaseRead.status };
11415
+ }
11416
+ async function activateFrontendRelease(http, input) {
11417
+ const mutationId = input.mutationId;
11418
+ if (!MUTATION_ID_PATTERN.test(mutationId))
11419
+ throw new Error("'mutation_id' must be a UUIDv4");
11420
+ if (input.expectedActiveReleaseId !== "absent" && !RELEASE_ID_PATTERN.test(input.expectedActiveReleaseId)) {
11421
+ throw new Error("'expected_active_release_id' is invalid");
11422
+ }
11423
+ if (input.expectedActivationId !== "absent" && !MUTATION_ID_PATTERN.test(input.expectedActivationId)) {
11424
+ throw new Error("'expected_activation_id' is invalid");
11425
+ }
11426
+ const endpoint = `${releasePath(input.projectRef, input.deploymentId, input.releaseId)}/activate`;
11427
+ const response = await http.post(endpoint, {
11428
+ expected_active_release_id: input.expectedActiveReleaseId,
11429
+ expected_activation_id: input.expectedActivationId,
11430
+ mutation_id: mutationId
11431
+ }, {
11432
+ maxJsonBytes: RESPONSE_MAX_BYTES,
11433
+ responseTimeoutMs: MUTATION_RESPONSE_TIMEOUT_MS
11434
+ });
11435
+ const release = activationReceipt(response.data, { ...input, mutationId });
11436
+ if (!response.ok || !release) {
11437
+ if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
11438
+ return releaseFailure("frontend.activate_release", "HTTP_ERROR", response.status);
11439
+ }
11440
+ return activationReadback(http, input, mutationId, response.status);
11441
+ }
11442
+ const readback = await activeReleaseReadback(http, { ...input, mutationId });
11443
+ if (!readback.release || readback.release.tree_sha256 !== release.tree_sha256) {
11444
+ return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", readback.status);
11445
+ }
11446
+ return toolResponse({
11447
+ project_ref: input.projectRef,
11448
+ deployment_id: input.deploymentId,
11449
+ active_release_id: input.releaseId,
11450
+ activation_id: mutationId,
11451
+ release: readback.release
11452
+ });
11453
+ }
11454
+ async function activationReadback(http, input, mutationId, activationStatus) {
11455
+ const mutationRead = await http.get(`/v1/projects/${encodeURIComponent(input.projectRef)}/mutations/${mutationId}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
11456
+ const mutationEnvelope = mutationRead.data;
11457
+ const mutation = mutationRead.ok ? publicMutationReceipt(mutationRead.data, {
11458
+ projectRef: input.projectRef,
11459
+ mutationId,
11460
+ resourceKey: activationResourceKey(input.deploymentId),
11461
+ requestFingerprint: activationFingerprint({ ...input, mutationId })
11462
+ }) : null;
11463
+ if (!mutation || mutationEnvelope?.project_ref !== input.projectRef || mutation.operation !== "frontend.release.activate" || mutation.status !== "succeeded" || mutation.responseStatus !== 200 || mutation.failureCode !== null) {
11464
+ return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", activationStatus);
11465
+ }
11466
+ const readback = await activeReleaseReadback(http, { ...input, mutationId });
11467
+ if (!readback.release) {
11468
+ return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", activationStatus);
11469
+ }
11470
+ return toolResponse({
11471
+ project_ref: input.projectRef,
11472
+ deployment_id: input.deploymentId,
11473
+ active_release_id: input.releaseId,
11474
+ activation_id: mutationId,
11475
+ release: readback.release
11476
+ });
11477
+ }
11478
+
11479
+ // src/shared/tools/frontend-tools.ts
11037
11480
  function registerFrontendTools(server, http) {
11038
- server.tool("frontend", `Frontend hosting (static sites & SSR). Supports: static, react, vue, svelte, sveltekit, sveltekit-static, nextjs, nuxt, astro.
11039
- Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy, build_logs, add_domain, remove_domain, set_env, list_frameworks, list_records`, {
11481
+ server.tool("frontend", `Frontend hosting and immutable prebuilt releases. Supports: static, react, vue, svelte, sveltekit, sveltekit-static, nextjs, nuxt, astro.
11482
+ 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`, {
11040
11483
  action: withDescription(stringEnum([
11041
11484
  "list",
11042
11485
  "get",
@@ -11051,7 +11494,11 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
11051
11494
  "remove_domain",
11052
11495
  "set_env",
11053
11496
  "list_frameworks",
11054
- "list_records"
11497
+ "list_records",
11498
+ "list_releases",
11499
+ "get_release",
11500
+ "upload_release",
11501
+ "activate_release"
11055
11502
  ]), "Action"),
11056
11503
  ref: optional(Type.String(), "Project ref"),
11057
11504
  id: optional(Type.String(), "Deployment ID"),
@@ -11066,9 +11513,37 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
11066
11513
  env_vars: optional(Type.Record(Type.String(), Type.String()), "[create/update/set_env] Environment variables"),
11067
11514
  git_url: optional(Type.String(), "[deploy_git] Git repository URL"),
11068
11515
  branch: optional(Type.String(), "[deploy_git] Branch (default: main)"),
11069
- zip_path: optional(Type.String(), "[deploy_upload] Local zip file path")
11516
+ zip_path: optional(Type.String(), "[deploy_upload/upload_release] Local ZIP file path"),
11517
+ release_id: optional(Type.String(), "[get_release/activate_release] SHA-256 release ID"),
11518
+ expected_active_release_id: optional(Type.String(), "[activate_release] Current release SHA-256 or absent"),
11519
+ expected_activation_id: optional(Type.String(), "[activate_release] Current activation UUIDv4 or absent"),
11520
+ mutation_id: optional(Type.String(), "[activate_release] Required retry-stable UUIDv4"),
11521
+ cursor: optional(Type.String(), "[list_releases] Last release SHA-256 cursor"),
11522
+ limit: optional(Type.Number(), "[list_releases] Page size, 1-100 (default 50)")
11070
11523
  }, async (args) => {
11071
- const { action, ref, id, name, framework, domain, build_command, output_dir, install_command, node_version, health_check_path, env_vars, git_url, branch, zip_path } = args;
11524
+ const {
11525
+ action,
11526
+ ref,
11527
+ id,
11528
+ name,
11529
+ framework,
11530
+ domain,
11531
+ build_command,
11532
+ output_dir,
11533
+ install_command,
11534
+ node_version,
11535
+ health_check_path,
11536
+ env_vars,
11537
+ git_url,
11538
+ branch,
11539
+ zip_path,
11540
+ release_id,
11541
+ expected_active_release_id,
11542
+ expected_activation_id,
11543
+ mutation_id,
11544
+ cursor,
11545
+ limit
11546
+ } = args;
11072
11547
  const need = (f, v) => {
11073
11548
  if (!v)
11074
11549
  throw new Error(`'${f}' required for '${action}'`);
@@ -11175,6 +11650,35 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
11175
11650
  need("id", id);
11176
11651
  text = ok(await http.get(`/v1/projects/${ref}/frontend/deployments/${id}/records`));
11177
11652
  break;
11653
+ case "list_releases":
11654
+ need("ref", ref);
11655
+ need("id", id);
11656
+ return listFrontendReleases(http, ref, id, cursor, limit);
11657
+ case "get_release":
11658
+ need("ref", ref);
11659
+ need("id", id);
11660
+ need("release_id", release_id);
11661
+ return getFrontendRelease(http, ref, id, release_id);
11662
+ case "upload_release":
11663
+ need("ref", ref);
11664
+ need("id", id);
11665
+ need("zip_path", zip_path);
11666
+ return uploadFrontendRelease(http, ref, id, zip_path);
11667
+ case "activate_release":
11668
+ need("ref", ref);
11669
+ need("id", id);
11670
+ need("release_id", release_id);
11671
+ need("expected_active_release_id", expected_active_release_id);
11672
+ need("expected_activation_id", expected_activation_id);
11673
+ need("mutation_id", mutation_id);
11674
+ return activateFrontendRelease(http, {
11675
+ projectRef: ref,
11676
+ deploymentId: id,
11677
+ releaseId: release_id,
11678
+ expectedActiveReleaseId: expected_active_release_id,
11679
+ expectedActivationId: expected_activation_id,
11680
+ mutationId: mutation_id
11681
+ });
11178
11682
  default:
11179
11683
  text = `❌ Unknown action`;
11180
11684
  }
@@ -11184,7 +11688,7 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
11184
11688
 
11185
11689
  // src/shared/tools/project-read-projection.ts
11186
11690
  var PROJECT_READ_RESPONSE_MAX_BYTES = 1048576;
11187
- var PROJECT_REF_PATTERN3 = /^[a-z0-9-]{1,20}$/;
11691
+ var PROJECT_REF_PATTERN4 = /^[a-z0-9-]{1,20}$/;
11188
11692
  var SAFE_IDENTIFIER_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
11189
11693
  var REGION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
11190
11694
  var STATUS_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
@@ -11248,7 +11752,7 @@ function matchingText(candidate, maxLength, pattern) {
11248
11752
  const candidateText = boundedText2(candidate, maxLength);
11249
11753
  return candidateText && pattern.test(candidateText) ? candidateText : null;
11250
11754
  }
11251
- function canonicalTimestamp(candidate) {
11755
+ function canonicalTimestamp2(candidate) {
11252
11756
  const timestamp = boundedText2(candidate, 64);
11253
11757
  if (!timestamp)
11254
11758
  return null;
@@ -11258,12 +11762,12 @@ function canonicalTimestamp(candidate) {
11258
11762
  function projectedSummary(project) {
11259
11763
  const summary = {
11260
11764
  id: matchingText(project.id, 128, SAFE_IDENTIFIER_PATTERN),
11261
- ref: matchingText(project.ref, 20, PROJECT_REF_PATTERN3),
11765
+ ref: matchingText(project.ref, 20, PROJECT_REF_PATTERN4),
11262
11766
  organization_id: matchingText(project.organization_id, 128, SAFE_IDENTIFIER_PATTERN),
11263
11767
  organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
11264
11768
  name: boundedText2(project.name, 100),
11265
11769
  region: matchingText(project.region, 64, REGION_PATTERN),
11266
- created_at: canonicalTimestamp(project.created_at),
11770
+ created_at: canonicalTimestamp2(project.created_at),
11267
11771
  status: matchingText(project.status, 64, STATUS_PATTERN)
11268
11772
  };
11269
11773
  return Object.values(summary).every((field) => field !== null) ? summary : null;
@@ -11382,7 +11886,7 @@ function projectGetRead(response, expectedRef) {
11382
11886
  // src/shared/tools/project-endpoint-read.ts
11383
11887
  var PROJECT_ENDPOINT_RESPONSE_MAX_BYTES = 256 * 1024;
11384
11888
  var PROJECT_ENDPOINT_LIST_RESPONSE_MAX_BYTES = 1024 * 1024;
11385
- var PROJECT_REF_PATTERN4 = /^[a-z0-9-]{1,20}$/;
11889
+ var PROJECT_REF_PATTERN5 = /^[a-z0-9-]{1,20}$/;
11386
11890
  var PROJECT_ENDPOINTS_SCHEMA = "supacloud.project-endpoints.v1";
11387
11891
  var PROJECT_ENDPOINT_SOURCES = new Set([
11388
11892
  "explicit_api_domain",
@@ -11452,7 +11956,7 @@ function projectEndpoint2(candidate) {
11452
11956
  }
11453
11957
  function projectEndpointProjection(candidate) {
11454
11958
  const projection = plainRecord2(candidate);
11455
- if (!projection || !hasOnlyKeys2(projection, ROOT_KEYS) || projection.schema !== PROJECT_ENDPOINTS_SCHEMA || typeof projection.project_ref !== "string" || !PROJECT_REF_PATTERN4.test(projection.project_ref))
11959
+ if (!projection || !hasOnlyKeys2(projection, ROOT_KEYS) || projection.schema !== PROJECT_ENDPOINTS_SCHEMA || typeof projection.project_ref !== "string" || !PROJECT_REF_PATTERN5.test(projection.project_ref))
11456
11960
  return null;
11457
11961
  const endpoints = plainRecord2(projection.endpoints);
11458
11962
  if (!endpoints || !hasOnlyKeys2(endpoints, ENDPOINTS_KEYS))
@@ -12531,7 +13035,7 @@ function registerBranchTools(server, http, options = {}) {
12531
13035
  // src/shared/tools/supabase-cli-tools.ts
12532
13036
  import { spawn } from "node:child_process";
12533
13037
  import { chmodSync, existsSync as existsSync5, mkdirSync, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
12534
- import { dirname, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
13038
+ import { dirname, isAbsolute, join as join3, resolve as resolve4 } from "node:path";
12535
13039
  var SENSITIVE_ENV_KEY = /(?:^|_)(?:PASSWORD|PASS|SECRET|TOKEN|KEY|CREDENTIALS?|AUTHORIZATION|AUTH|SESSION|COOKIE|BEARER|DB_URI|DB_URL|DSN|DATABASE_URL|DATABASE_URI|CONNECTION_STRING|CONNECTION_URI)(?:_|$)/i;
12536
13040
  var VALID_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
12537
13041
  var VALID_MIGRATION_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,100}$/;
@@ -12575,12 +13079,12 @@ function schemaArguments(schema) {
12575
13079
  function workdirArguments(workdir) {
12576
13080
  if (!workdir)
12577
13081
  throw new Error("A workdir is required");
12578
- return ["--workdir", resolve3(workdir)];
13082
+ return ["--workdir", resolve4(workdir)];
12579
13083
  }
12580
13084
  function resolveOutputPath(workdir, file, label) {
12581
13085
  if (!file || /[\r\n\0]/.test(file))
12582
13086
  throw new Error(`${label} file is required`);
12583
- return isAbsolute(file) ? resolve3(file) : resolve3(workdir, file);
13087
+ return isAbsolute(file) ? resolve4(file) : resolve4(workdir, file);
12584
13088
  }
12585
13089
  function databaseTargetArguments(databaseUrl) {
12586
13090
  return databaseUrl ? ["--db-url", requirePostgresUrl(databaseUrl)] : ["--local"];
@@ -12665,7 +13169,7 @@ function actionArguments(request, workdir) {
12665
13169
  function buildOfficialSupabaseArgs(request) {
12666
13170
  if (request.action === "version")
12667
13171
  return ["--version"];
12668
- const workdir = request.workdir ? resolve3(request.workdir) : undefined;
13172
+ const workdir = request.workdir ? resolve4(request.workdir) : undefined;
12669
13173
  if (!workdir)
12670
13174
  throw new Error("A workdir is required");
12671
13175
  return [...actionArguments(request, workdir), ...workdirArguments(workdir)];
@@ -12711,13 +13215,13 @@ function resolveOfficialSupabaseCommand(workdir, environment = process.env) {
12711
13215
  }
12712
13216
  return ["npx", "--yes", `supabase@${version}`];
12713
13217
  }
12714
- const localPackageEntry = join3(resolve3(workdir), "node_modules", "supabase", "dist", "supabase.js");
13218
+ const localPackageEntry = join3(resolve4(workdir), "node_modules", "supabase", "dist", "supabase.js");
12715
13219
  if (existsSync5(localPackageEntry))
12716
13220
  return [process.execPath, localPackageEntry];
12717
13221
  return ["supabase"];
12718
13222
  }
12719
13223
  function resolveExistingWorkdir(workdirInput, fallback) {
12720
- const workdir = resolve3(workdirInput || fallback);
13224
+ const workdir = resolve4(workdirInput || fallback);
12721
13225
  if (!existsSync5(workdir) || !statSync3(workdir).isDirectory()) {
12722
13226
  throw new Error(`Supabase workdir not found: ${workdir}`);
12723
13227
  }
@@ -12831,7 +13335,7 @@ async function executeMigrationPush(request, runtime) {
12831
13335
  if (!projectRef2)
12832
13336
  return missingProjectRefResult();
12833
13337
  const workdir = resolveExistingWorkdir(request.workdir, runtime.fallbackWorkdir);
12834
- const migrationDirectory = resolve3(workdir, request.dir || "supabase/migrations");
13338
+ const migrationDirectory = resolve4(workdir, request.dir || "supabase/migrations");
12835
13339
  const migrationResponse = await pushMigrations({
12836
13340
  action: "push_migrations",
12837
13341
  ref: projectRef2,
@@ -12915,9 +13419,9 @@ function registerSupabaseCliTools(server, options = {}) {
12915
13419
  // src/shared/tools/lite-cli-tools.ts
12916
13420
  import { spawn as spawn2 } from "node:child_process";
12917
13421
  import { existsSync as existsSync6, statSync as statSync4 } from "node:fs";
12918
- import { join as join4, resolve as resolve4 } from "node:path";
13422
+ import { join as join4, resolve as resolve5 } from "node:path";
12919
13423
  function requireWorkdir(workdir, fallback) {
12920
- const resolved = resolve4(workdir || fallback);
13424
+ const resolved = resolve5(workdir || fallback);
12921
13425
  if (!existsSync6(resolved) || !statSync4(resolved).isDirectory()) {
12922
13426
  throw new Error(`Lite workdir not found: ${resolved}`);
12923
13427
  }
@@ -13003,7 +13507,7 @@ function resolveLiteCommand(workdir, environment = process.env) {
13003
13507
  throw new Error("Invalid SUPACLOUD_LITE_CLI_BIN");
13004
13508
  return [explicitBinary];
13005
13509
  }
13006
- const localPackageEntry = join4(resolve4(workdir), "node_modules", "@supacloud", "lite", "dist", "launcher.cjs");
13510
+ const localPackageEntry = join4(resolve5(workdir), "node_modules", "@supacloud", "lite", "dist", "launcher.cjs");
13007
13511
  if (existsSync6(localPackageEntry))
13008
13512
  return [process.execPath, localPackageEntry];
13009
13513
  return ["supacloud-lite"];
@@ -13151,7 +13655,7 @@ import {
13151
13655
  rmSync as rmSync2
13152
13656
  } from "node:fs";
13153
13657
  import { homedir as homedir2 } from "node:os";
13154
- import { dirname as dirname2, join as join5, relative as relative2, resolve as resolve5, sep as sep2 } from "node:path";
13658
+ import { dirname as dirname2, join as join5, relative as relative2, resolve as resolve6, sep as sep2 } from "node:path";
13155
13659
  import { fileURLToPath } from "node:url";
13156
13660
  var SKILL_NAME = "supacloud-cli";
13157
13661
  function regularFiles(rootDirectory, currentDirectory = rootDirectory) {
@@ -13224,8 +13728,8 @@ function replaceSkill(sourceDirectory, targetRoot, destinationDirectory, backupD
13224
13728
  }
13225
13729
  }
13226
13730
  function skillSummary(request, action, files, backupDirectory) {
13227
- const sourceDirectory = resolve5(request.sourceDirectory);
13228
- const targetRoot = resolve5(request.targetRoot);
13731
+ const sourceDirectory = resolve6(request.sourceDirectory);
13732
+ const targetRoot = resolve6(request.targetRoot);
13229
13733
  return {
13230
13734
  name: SKILL_NAME,
13231
13735
  sourceDirectory,
@@ -13239,8 +13743,8 @@ function skillSummary(request, action, files, backupDirectory) {
13239
13743
  };
13240
13744
  }
13241
13745
  function installSkill(request) {
13242
- const sourceDirectory = resolve5(request.sourceDirectory);
13243
- const targetRoot = resolve5(request.targetRoot);
13746
+ const sourceDirectory = resolve6(request.sourceDirectory);
13747
+ const targetRoot = resolve6(request.targetRoot);
13244
13748
  const destinationDirectory = join5(targetRoot, SKILL_NAME);
13245
13749
  if (!existsSync7(join5(sourceDirectory, "SKILL.md"))) {
13246
13750
  throw new Error(`Bundled SupaCloud CLI skill not found: ${sourceDirectory}`);
@@ -13257,16 +13761,16 @@ function installSkill(request) {
13257
13761
  return installReplacementSkill(request, files);
13258
13762
  }
13259
13763
  function installNewSkill(request, files) {
13260
- const sourceDirectory = resolve5(request.sourceDirectory);
13261
- const targetRoot = resolve5(request.targetRoot);
13764
+ const sourceDirectory = resolve6(request.sourceDirectory);
13765
+ const targetRoot = resolve6(request.targetRoot);
13262
13766
  if (request.mode === "write") {
13263
13767
  createSkill(sourceDirectory, targetRoot, join5(targetRoot, SKILL_NAME));
13264
13768
  }
13265
13769
  return skillSummary(request, "create", files, null);
13266
13770
  }
13267
13771
  function installReplacementSkill(request, files) {
13268
- const sourceDirectory = resolve5(request.sourceDirectory);
13269
- const targetRoot = resolve5(request.targetRoot);
13772
+ const sourceDirectory = resolve6(request.sourceDirectory);
13773
+ const targetRoot = resolve6(request.targetRoot);
13270
13774
  const destinationDirectory = join5(targetRoot, SKILL_NAME);
13271
13775
  const backupDirectory = availableBackupDirectory(destinationDirectory, request.now);
13272
13776
  if (request.mode === "write") {
@@ -13276,13 +13780,13 @@ function installReplacementSkill(request, files) {
13276
13780
  }
13277
13781
  function resolveDefaultCodexSkillRoot(environment = process.env, homeDirectory = homedir2()) {
13278
13782
  const codexHome = environment.CODEX_HOME?.trim();
13279
- return join5(resolve5(codexHome || join5(homeDirectory, ".codex")), "skills");
13783
+ return join5(resolve6(codexHome || join5(homeDirectory, ".codex")), "skills");
13280
13784
  }
13281
13785
  function resolveBundledSkillDirectory(moduleUrl = import.meta.url) {
13282
13786
  const moduleDirectory = dirname2(fileURLToPath(moduleUrl));
13283
13787
  const candidates = [
13284
- resolve5(moduleDirectory, "../../../skills", SKILL_NAME),
13285
- resolve5(moduleDirectory, "../skills", SKILL_NAME)
13788
+ resolve6(moduleDirectory, "../../../skills", SKILL_NAME),
13789
+ resolve6(moduleDirectory, "../skills", SKILL_NAME)
13286
13790
  ];
13287
13791
  const skillDirectory = candidates.find((candidate) => existsSync7(join5(candidate, "SKILL.md")));
13288
13792
  if (!skillDirectory)
@@ -13322,7 +13826,7 @@ function registerAiTools(server) {
13322
13826
  // src/shared/tools/scheduled-function-tools.ts
13323
13827
  import { randomUUID } from "node:crypto";
13324
13828
  import { readFileSync as readFileSync6, statSync as statSync5 } from "node:fs";
13325
- import { resolve as resolve6 } from "node:path";
13829
+ import { resolve as resolve7 } from "node:path";
13326
13830
  import { isDeepStrictEqual } from "node:util";
13327
13831
  var HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$/;
13328
13832
  var ENVIRONMENT_NAME_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
@@ -13406,7 +13910,7 @@ function validScheduledFunctionCron(expression) {
13406
13910
  function readScheduleBodyFile(bodyPathInput) {
13407
13911
  if (!bodyPathInput.trim())
13408
13912
  throw new Error("'body_file' must be a path");
13409
- const bodyPath = resolve6(bodyPathInput);
13913
+ const bodyPath = resolve7(bodyPathInput);
13410
13914
  const bodyStat = statSync5(bodyPath);
13411
13915
  if (!bodyStat.isFile() || bodyStat.size > MAX_BODY_FILE_BYTES) {
13412
13916
  throw new Error("Scheduled Function body file must be a regular file no larger than 1 MiB");
@@ -13752,14 +14256,14 @@ var SCHEDULE_TOOL_SCHEMA = {
13752
14256
  };
13753
14257
 
13754
14258
  // src/shared/mutation-protocol.ts
13755
- var MUTATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
14259
+ var MUTATION_ID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
13756
14260
  var FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/;
13757
14261
  var OPERATION_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
13758
14262
  var RESOURCE_KEY_PATTERN = /^v1\/(?:[a-z0-9][a-z0-9._-]{0,63})\/([A-Za-z0-9_-]{2,171})$/;
13759
14263
  var RESOURCE_ID_CONTROL_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
13760
14264
  var FAILURE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
13761
14265
  var LEASE_OWNER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,254}$/;
13762
- var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
14266
+ var TIMESTAMP_PATTERN2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
13763
14267
  var MAX_STATUS_RESPONSE_BYTES = 196608;
13764
14268
  var MAX_RESOURCE_ID_BYTES = 128;
13765
14269
  var FATAL_UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
@@ -13792,7 +14296,7 @@ var MUTATION_KEYS = [
13792
14296
  var PRINCIPAL_KEYS = ["type", "id"];
13793
14297
  var LEASE_KEYS = ["owner", "expires_at", "fencing_epoch"];
13794
14298
  function isMutationId(candidate) {
13795
- return typeof candidate === "string" && MUTATION_ID_PATTERN.test(candidate);
14299
+ return typeof candidate === "string" && MUTATION_ID_PATTERN2.test(candidate);
13796
14300
  }
13797
14301
  function objectRecord4(candidate) {
13798
14302
  return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
@@ -13807,14 +14311,14 @@ function emptyProjection(candidate) {
13807
14311
  const record = objectRecord4(candidate);
13808
14312
  return record && Object.keys(record).length === 0 ? record : null;
13809
14313
  }
13810
- function canonicalTimestamp2(candidate) {
13811
- if (typeof candidate !== "string" || !TIMESTAMP_PATTERN.test(candidate))
14314
+ function canonicalTimestamp3(candidate) {
14315
+ if (typeof candidate !== "string" || !TIMESTAMP_PATTERN2.test(candidate))
13812
14316
  return false;
13813
14317
  const milliseconds = Date.parse(candidate);
13814
14318
  return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
13815
14319
  }
13816
14320
  function nullableTimestamp(candidate) {
13817
- return candidate === null || canonicalTimestamp2(candidate);
14321
+ return candidate === null || canonicalTimestamp3(candidate);
13818
14322
  }
13819
14323
  function safePrincipal(candidate) {
13820
14324
  const principal = exactRecord(candidate, PRINCIPAL_KEYS);
@@ -13897,7 +14401,7 @@ function validMutationTerminalFields(mutation) {
13897
14401
  return false;
13898
14402
  if (mutation.failure_code !== null && (typeof mutation.failure_code !== "string" || !FAILURE_CODE_PATTERN.test(mutation.failure_code)))
13899
14403
  return false;
13900
- return nullableTimestamp(mutation.completed_at) && canonicalTimestamp2(mutation.created_at) && canonicalTimestamp2(mutation.updated_at);
14404
+ return nullableTimestamp(mutation.completed_at) && canonicalTimestamp3(mutation.created_at) && canonicalTimestamp3(mutation.updated_at);
13901
14405
  }
13902
14406
  function safeMutationStatus(candidate) {
13903
14407
  const mutation = exactRecord(candidate, MUTATION_KEYS);
@@ -14005,7 +14509,7 @@ var RELEASE_CANARY_CLAIM_MAX_LENGTH = 2048;
14005
14509
  function isRecord3(value) {
14006
14510
  return value !== null && typeof value === "object" && !Array.isArray(value);
14007
14511
  }
14008
- function canonicalTimestamp3(value) {
14512
+ function canonicalTimestamp4(value) {
14009
14513
  if (typeof value !== "string")
14010
14514
  return false;
14011
14515
  const parsed = new Date(value);
@@ -14018,7 +14522,7 @@ function backupBelongsToProject(backupId, projectRef2) {
14018
14522
  return BACKUP_ID.test(backupId) && backupId.startsWith(`logical-full_${projectRef2}_`);
14019
14523
  }
14020
14524
  function verifiedBackup(value, projectRef2) {
14021
- if (!isRecord3(value) || typeof value.backup_id !== "string" || !backupBelongsToProject(value.backup_id, projectRef2) || value.project_ref !== projectRef2 || typeof value.database !== "string" || !SAFE_DATABASE.test(value.database) || value.kind !== "logical-full" || !canonicalTimestamp3(value.created_at) || !canonicalTimestamp3(value.completed_at) || new Date(value.completed_at).valueOf() < new Date(value.created_at).valueOf() || typeof value.bytes !== "number" || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || typeof value.sha256 !== "string" || !SHA256.test(value.sha256))
14525
+ if (!isRecord3(value) || typeof value.backup_id !== "string" || !backupBelongsToProject(value.backup_id, projectRef2) || value.project_ref !== projectRef2 || typeof value.database !== "string" || !SAFE_DATABASE.test(value.database) || value.kind !== "logical-full" || !canonicalTimestamp4(value.created_at) || !canonicalTimestamp4(value.completed_at) || new Date(value.completed_at).valueOf() < new Date(value.created_at).valueOf() || typeof value.bytes !== "number" || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || typeof value.sha256 !== "string" || !SHA256.test(value.sha256))
14022
14526
  return null;
14023
14527
  return {
14024
14528
  backup_id: value.backup_id,
@@ -14365,7 +14869,7 @@ function registerReleaseTools(server, http, options = {}) {
14365
14869
  // package.json
14366
14870
  var package_default = {
14367
14871
  name: "@supacloud/cli",
14368
- version: "0.33.0",
14872
+ version: "0.34.1",
14369
14873
  description: "Project-scoped CLI for SupaCloud users",
14370
14874
  type: "module",
14371
14875
  main: "./dist/index.js",
@@ -14742,6 +15246,11 @@ function createCliTools(context, confirmProduction) {
14742
15246
  if (branchHelpTool) {
14743
15247
  tools.branch = { schema: branchHelpTool.schema, callback: branchContextCallback };
14744
15248
  }
15249
+ const frontendContextCallback = tools.frontend.callback;
15250
+ const frontendHelpTool = captureTools((server) => registerFrontendTools(server, {})).frontend;
15251
+ if (frontendHelpTool) {
15252
+ tools.frontend = { schema: frontendHelpTool.schema, callback: frontendContextCallback };
15253
+ }
14745
15254
  };
14746
15255
  if (context.credentialScope !== "management" || !context.apiUrl || !context.apiToken) {
14747
15256
  registerContextAwareHelp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.33.0",
3
+ "version": "0.34.1",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",