@supacloud/cli 0.77.0 → 0.78.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.
package/README.md CHANGED
@@ -79,6 +79,94 @@ deployment. An unknown mutation outcome remains unknown; reconcile the same
79
79
  activation identity rather than blindly deploying again. These commands require
80
80
  Management API credentials except local plan/build and authoring commands.
81
81
 
82
+ ### Isolated Application Previews
83
+
84
+ ```sh
85
+ supacloud-cli app preview-plan --env test --id orders --environment_id test --release_id RELEASE_SHA256 --branch_ref preview-orders
86
+ supacloud-cli app preview --env test --id orders --environment_id test --release_id RELEASE_SHA256 --configuration_id CONFIG_UUID --ttl_seconds 3600 --wait --timeout_seconds 300
87
+ supacloud-cli app previews --env test --id orders --environment_id test
88
+ supacloud-cli app preview-status --env test --id orders --environment_id test --preview_id PREVIEW_UUID --wait --timeout_seconds 300
89
+ supacloud-cli app preview-cleanup --env test --id orders --environment_id test --preview_id PREVIEW_UUID
90
+ ```
91
+
92
+ The platform owns branch provisioning, queue/Storage namespaces, test Secrets,
93
+ configuration cloning, activation and runtime readiness. The CLI never performs
94
+ these operations through SSH or sequential client-side resource mutations.
95
+ Creation defaults to `schema_only`; copying source rows requires explicitly
96
+ selecting `--data_mode full_clone`. Creation requires an immutable source
97
+ configuration ID and verifies the branch release identity against the source
98
+ manifest. A `provisioning` receipt means accepted, not ready.
99
+
100
+ New previews use the platform's default 24-hour TTL. `--ttl_seconds` accepts 300
101
+ through 604800 seconds for creation or read-only planning. `expires_at` remains
102
+ in the public receipt; a plan has no deadline unless a TTL is requested. Receipt
103
+ expiry requires the matching platform cleanup contract (#1774), not the older
104
+ CLI-only lifecycle contract.
105
+
106
+ `--wait` on creation observes only the accepted Preview ID; it never repeats
107
+ creation. `preview-status --wait` continues observing that same ID. The observation
108
+ budget is 300 seconds by default and accepts 1 through 3600 seconds with
109
+ `--timeout_seconds`; creation's source verification and initial POST precede that
110
+ budget, while status includes its initial GET. Header and body share the remaining
111
+ request budget. Completed readiness ends waiting; a failed/cleaned receipt, failed
112
+ query or identity drift ends it with nonzero status. A wait timeout returns
113
+ `waiting.status: "timed_out"` with the last validated receipt and the exact
114
+ same-ID continuation in `reconciliation`. It does not mark provisioning failed,
115
+ retry a failed lifecycle GET, delete resources or promote an environment.
116
+
117
+ `preview-plan` is read-only. Preview list/status requests may resume persisted
118
+ provisioning on the server, so they are classified as writes, including in
119
+ read-only and production profiles. The platform may clean eligible expired
120
+ previews, but never interrupts a serving preview or empties nonempty Storage
121
+ automatically. Explicit cleanup remains available. Failed previews return
122
+ nonzero with the validated receipt and cleanup state. Unknown create outcomes
123
+ are never retried: list existing previews and reconcile their receipts first.
124
+ No production deployment, database promotion or data rollback is implied.
125
+ Runtime readiness is not authenticated business-flow acceptance.
126
+
127
+ ```gherkin
128
+ Feature: Platform-owned application previews
129
+ Scenario: Read-only planning
130
+ Given an explicit release and proposed branch ref
131
+ When preview-plan runs in read-only mode
132
+ Then only a GET plan request is sent
133
+ And no preview is created
134
+
135
+ Scenario: Accepted is not ready
136
+ Given an immutable source release and configuration
137
+ When preview creates an isolated environment
138
+ Then exactly one create request is sent
139
+ And a provisioning receipt is returned without claiming readiness
140
+
141
+ Scenario: TTL is validated before dispatch
142
+ Given a lifetime outside 300 through 604800 integer seconds
143
+ When preview or preview-plan is requested
144
+ Then no HTTP request is sent
145
+
146
+ Scenario: Wait preserves the selected preview
147
+ Given a valid provisioning receipt and an exhausted observation budget
148
+ When preview --wait returns
149
+ Then its exit status is nonzero and the accepted receipt is preserved
150
+ And reconciliation selects the same Preview ID without another creation
151
+
152
+ Scenario: Ready requires verified platform evidence
153
+ Given a provisioning preview selected by its scope, digest and Preview ID
154
+ When its complete readiness receipt is observed
155
+ Then waiting succeeds with that receipt
156
+ And foreign or malformed receipts never become success
157
+
158
+ Scenario: Safety guards precede effects
159
+ Given a read-only or unconfirmed production profile
160
+ When preview creation, status recovery or cleanup is requested
161
+ Then the CLI rejects the request before HTTP dispatch
162
+
163
+ Scenario: Unknown outcome preserves reconciliation
164
+ Given a create response is lost or belongs to another environment
165
+ When the CLI handles that response
166
+ Then it reports OUTCOME_UNKNOWN without retrying creation
167
+ And it retains the selected scope for receipt reconciliation
168
+ ```
169
+
82
170
  ### Framework source upgrades
83
171
 
84
172
  Breaking source-contract changes use the compiler's migration command, which is
package/dist/index.js CHANGED
@@ -179093,8 +179093,18 @@ function resolveSupaCloudContext(env = process.env, cwd = process.cwd(), selecti
179093
179093
  var ACTION_POLICY = {
179094
179094
  deploy: { write: ["deploy"] },
179095
179095
  applications: {
179096
- read: ["list_releases", "get_release", "get_runtime", "get_deployment_evidence", "get_configuration", "logs"],
179097
- write: ["upload_release", "put_configuration", "activate_release", "reconcile_activation", "retire_activation"]
179096
+ read: ["list_releases", "get_release", "get_runtime", "get_deployment_evidence", "get_configuration", "logs", "get_preview_plan"],
179097
+ write: [
179098
+ "upload_release",
179099
+ "put_configuration",
179100
+ "activate_release",
179101
+ "reconcile_activation",
179102
+ "retire_activation",
179103
+ "create_preview",
179104
+ "list_previews",
179105
+ "get_preview",
179106
+ "cleanup_preview"
179107
+ ]
179098
179108
  },
179099
179109
  project: {
179100
179110
  read: ["get", "endpoints", "health", "logs", "api_keys", "settings", "tasks", "task_detail", "task_stats", "dlq", "background_settings"],
@@ -179189,8 +179199,8 @@ var ACTION_POLICY = {
179189
179199
  ai: { local: ["show_skill", "install_skill"] },
179190
179200
  app: {
179191
179201
  local: ["init", "generate", "dev", "watch", "verify-plan", "compile", "check", "graph", "explain", "export-tools", "context", "doctor", "fix", "plan", "build"],
179192
- read: ["status", "logs"],
179193
- write: ["upload", "configure", "deploy", "rollback", "reconcile", "retire"]
179202
+ read: ["status", "logs", "preview-plan"],
179203
+ write: ["upload", "configure", "deploy", "rollback", "reconcile", "retire", "preview", "previews", "preview-status", "preview-cleanup"]
179194
179204
  },
179195
179205
  db: {
179196
179206
  local: ["context", "lint", "explain", "diff", "role_sql"],
@@ -179528,19 +179538,34 @@ class HttpTransport {
179528
179538
  }
179529
179539
  }
179530
179540
  async get(path, options = {}) {
179541
+ const timeoutMs = validatedPostTimeout(options);
179531
179542
  const maxResponseBytes = validatedGetResponseLimit(options);
179532
179543
  const maxJsonBytes = validatedJsonResponseLimit(options.maxJsonBytes);
179533
179544
  const responseTimeoutMs = validatedResponseTimeout(options.responseTimeoutMs);
179545
+ const totalTimeoutMs = validatedResponseTimeout(options.totalTimeoutMs);
179534
179546
  if (maxResponseBytes !== undefined && maxJsonBytes !== undefined) {
179535
179547
  throw new RangeError("HTTP response limit options are mutually exclusive");
179536
179548
  }
179549
+ if (totalTimeoutMs !== undefined && (options.retry !== false || maxJsonBytes === undefined)) {
179550
+ throw new RangeError("HTTP total timeout requires bounded JSON and retry: false");
179551
+ }
179552
+ const deadline = totalTimeoutMs === undefined ? undefined : performance.now() + totalTimeoutMs;
179537
179553
  try {
179538
- const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
179554
+ const request = options.retry === false ? fetchWithTimeout : fetchWithRetry;
179555
+ const res = await request(`${this.baseUrl}${path}`, {
179539
179556
  method: "GET",
179540
179557
  headers: this.headers()
179541
- }, DEFAULT_TIMEOUT, this.insecureTls);
179558
+ }, totalTimeoutMs === undefined ? timeoutMs : Math.min(timeoutMs, totalTimeoutMs), this.insecureTls);
179542
179559
  if (maxJsonBytes !== undefined) {
179543
- const data = await boundedResponseJson(res, maxJsonBytes, responseTimeoutMs);
179560
+ const remaining = deadline === undefined ? undefined : Math.floor(deadline - performance.now());
179561
+ if (remaining !== undefined && remaining <= 0) {
179562
+ res.body?.cancel().catch(() => {
179563
+ return;
179564
+ });
179565
+ return responseReadFailure(res.status);
179566
+ }
179567
+ const bodyTimeout = remaining === undefined ? responseTimeoutMs : Math.min(responseTimeoutMs ?? remaining, remaining);
179568
+ const data = await boundedResponseJson(res, maxJsonBytes, bodyTimeout);
179544
179569
  return data === null ? responseReadFailure(res.status) : { ok: res.ok, status: res.status, data };
179545
179570
  }
179546
179571
  const data = maxResponseBytes === undefined ? await res.json().catch(() => null) : await boundedResponseJson(res, maxResponseBytes);
@@ -187228,6 +187253,332 @@ var ApplicationActivationWriteSchema2 = ApplicationActivationWriteSchema;
187228
187253
  var ApplicationActivationResultSchema2 = ApplicationActivationResultSchema;
187229
187254
  var ApplicationActivationRetirementResultSchema2 = ApplicationActivationRetirementResultSchema;
187230
187255
 
187256
+ // src/shared/tools/application-preview-tools.ts
187257
+ var APPLICATION_PREVIEW_ACTIONS = [
187258
+ "get_preview_plan",
187259
+ "create_preview",
187260
+ "list_previews",
187261
+ "get_preview",
187262
+ "cleanup_preview"
187263
+ ];
187264
+ var previewIdSchema = String2({ pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" });
187265
+ var branchRefSchema = String2({ pattern: "^[a-z0-9-]{1,20}$" });
187266
+ var ttlSecondsSchema = Integer({ minimum: 300, maximum: 604800 });
187267
+ var waitSecondsSchema = Integer({ minimum: 1, maximum: 3600 });
187268
+ var timestampSchema = String2({
187269
+ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
187270
+ maxLength: 24
187271
+ });
187272
+ var APPLICATION_PREVIEW_FIELDS = {
187273
+ preview_id: optional(previewIdSchema, "[get_preview/cleanup_preview] Preview receipt ID"),
187274
+ branch_ref: optional(branchRefSchema, "[get_preview_plan] Proposed branch ref; create assigns its own"),
187275
+ branch_name: optional(String2({ minLength: 1, maxLength: 80 }), "[create_preview] Branch display name"),
187276
+ data_mode: optional(stringEnum(["schema_only", "full_clone"]), "[get_preview_plan/create_preview] Default schema_only; full_clone copies rows"),
187277
+ ttl_seconds: optional(ttlSecondsSchema, "[get_preview_plan/create_preview] Lifetime from 300 to 604800 seconds; creation defaults to the platform TTL"),
187278
+ wait: optional(Boolean2(), "[create_preview/get_preview] Wait for verified readiness of this preview only"),
187279
+ timeout_seconds: optional(waitSecondsSchema, "[create_preview/get_preview] Observation budget with wait, from 1 to 3600 seconds (default 300)")
187280
+ };
187281
+ var strict = { additionalProperties: false };
187282
+ var phase = stringEnum(["pending", "ready", "failed", "cleaned"]);
187283
+ var check5 = String2({ pattern: "^[a-z][a-z0-9_]{0,63}$" });
187284
+ var checks = _Array_(check5, { maxItems: 64, uniqueItems: true });
187285
+ var revision = Union([ApplicationConfigurationIdSchema2, Null()]);
187286
+ var receiptSchema = _Object_({
187287
+ schema: Literal("supacloud.application-preview.v1"),
187288
+ preview_id: previewIdSchema,
187289
+ project_ref: branchRefSchema,
187290
+ application_id: ApplicationIdSchema2,
187291
+ environment_id: ApplicationIdSchema2,
187292
+ release_id: ApplicationReleaseIdSchema2,
187293
+ expires_at: Union([timestampSchema, Null()]),
187294
+ status: stringEnum(["planned", "provisioning", "ready", "failed", "cleaned"]),
187295
+ resources: _Object_({
187296
+ build_artifact: _Object_({ status: phase, release_id: ApplicationReleaseIdSchema2 }, strict),
187297
+ database_branch: _Object_({ status: phase, branch_ref: branchRefSchema, data_mode: stringEnum(["schema_only", "full_clone"]) }, strict),
187298
+ queue_namespace: _Object_({ status: phase, namespace: String2({ minLength: 1, maxLength: 128 }) }, strict),
187299
+ storage_namespace: _Object_({ status: phase, namespace: branchRefSchema }, strict),
187300
+ test_secret: _Object_({ status: phase, name: String2({ pattern: "^[A-Z0-9_-]{1,128}$" }), value_issued: Literal(false) }, strict),
187301
+ configuration_revision: _Object_({ status: phase, configuration_id: revision }, strict),
187302
+ application_activation: _Object_({ status: phase, activation_id: Union([previewIdSchema, Null()]) }, strict),
187303
+ smoke_test: _Object_({ status: phase, checks, passed: checks, failed: checks }, strict)
187304
+ }, strict),
187305
+ cleanup: _Object_({
187306
+ required: Boolean2(),
187307
+ completed: Boolean2(),
187308
+ error: Union([String2({ pattern: "^[A-Z0-9_]{1,128}$" }), Null()])
187309
+ }, strict),
187310
+ branch_name: Optional(String2({ maxLength: 80 })),
187311
+ queue_name: Optional(String2({ maxLength: 128 })),
187312
+ test_secret_name: Optional(String2({ pattern: "^[A-Z0-9_]{1,128}$" })),
187313
+ source_configuration_id: Optional(revision),
187314
+ created_at: Optional(timestampSchema),
187315
+ updated_at: Optional(timestampSchema)
187316
+ }, strict);
187317
+ function required5(args, field) {
187318
+ const value = args[field];
187319
+ if (typeof value !== "string" || !value.trim())
187320
+ throw new Error(`'${field}' is required`);
187321
+ return value.trim();
187322
+ }
187323
+ var mandatoryChecks = [
187324
+ "release_artifact",
187325
+ "database_branch",
187326
+ "queue_namespace",
187327
+ "storage_namespace",
187328
+ "test_secret",
187329
+ "configuration_revision",
187330
+ "application_activation",
187331
+ "application_readiness",
187332
+ "tenant_runtime"
187333
+ ];
187334
+ var actionFields = {
187335
+ get_preview_plan: ["release_id", "branch_ref", "data_mode", "configuration_id", "ttl_seconds"],
187336
+ create_preview: ["release_id", "configuration_id", "data_mode", "branch_name", "ttl_seconds", "wait", "timeout_seconds"],
187337
+ list_previews: [],
187338
+ get_preview: ["preview_id", "wait", "timeout_seconds"],
187339
+ cleanup_preview: ["preview_id"]
187340
+ };
187341
+ function timestamp(value) {
187342
+ return Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
187343
+ }
187344
+ function receipt(value, identity) {
187345
+ if (!Check2(receiptSchema, value) || value.project_ref !== identity.ref || value.application_id !== identity.id || value.environment_id !== identity.environment || identity.previewId && value.preview_id !== identity.previewId || value.resources.build_artifact.release_id !== value.release_id || value.resources.storage_namespace.namespace !== value.resources.database_branch.branch_ref) {
187346
+ throw new Error("Invalid preview receipt");
187347
+ }
187348
+ if (value.expires_at !== null && !timestamp(value.expires_at) || value.created_at !== undefined && !timestamp(value.created_at) || value.updated_at !== undefined && !timestamp(value.updated_at) || value.expires_at !== null && value.created_at !== undefined && !Check2(ttlSecondsSchema, (Date.parse(value.expires_at) - Date.parse(value.created_at)) / 1000)) {
187349
+ throw new Error("Invalid preview expiry");
187350
+ }
187351
+ if (value.status === "ready") {
187352
+ if (value.resources.smoke_test.status !== "ready" || value.resources.smoke_test.failed.length > 0 || mandatoryChecks.some((check) => !value.resources.smoke_test.checks.includes(check) || !value.resources.smoke_test.passed.includes(check)) || value.resources.smoke_test.passed.some((check) => !value.resources.smoke_test.checks.includes(check)) || Object.values(value.resources).some((resource) => resource.status !== "ready") || !value.resources.configuration_revision.configuration_id || !value.resources.application_activation.activation_id) {
187353
+ throw new Error("Incomplete preview readiness");
187354
+ }
187355
+ }
187356
+ if (value.status === "cleaned" && (!value.cleanup.completed || value.cleanup.required || [
187357
+ value.resources.database_branch,
187358
+ value.resources.queue_namespace,
187359
+ value.resources.storage_namespace,
187360
+ value.resources.test_secret
187361
+ ].some((resource) => resource.status !== "cleaned"))) {
187362
+ throw new Error("Incomplete preview cleanup");
187363
+ }
187364
+ const {
187365
+ branch_name: _branchName,
187366
+ queue_name: _queueName,
187367
+ test_secret_name: _secretName,
187368
+ source_configuration_id: _sourceConfiguration,
187369
+ created_at: _createdAt,
187370
+ updated_at: _updatedAt,
187371
+ ...publicReceipt
187372
+ } = value;
187373
+ return publicReceipt;
187374
+ }
187375
+ async function sourceManifest(http, path, ref, id, releaseId) {
187376
+ const result = await http.get(`${path}/releases/${releaseId}`, { maxJsonBytes: 196608, responseTimeoutMs: 30000 });
187377
+ if (!result.ok)
187378
+ return { result };
187379
+ try {
187380
+ const data = result.data;
187381
+ if (!data || typeof data !== "object" || !("project_ref" in data) || data.project_ref !== ref || !("application_id" in data) || data.application_id !== id || !("release" in data))
187382
+ throw new Error;
187383
+ const release = parseApplicationReleaseRecord(data.release);
187384
+ if (release.project_ref !== ref || release.application_id !== id || release.release_id !== releaseId)
187385
+ throw new Error;
187386
+ return { result, manifest: release.manifest_sha256 };
187387
+ } catch {
187388
+ return { result };
187389
+ }
187390
+ }
187391
+ async function waitForPreview(http, path, operation, identity, initial, timeoutSeconds, startedAt = performance.now()) {
187392
+ const deadline = startedAt + timeoutSeconds * 1000;
187393
+ let current = initial;
187394
+ const state = () => ({
187395
+ ...identity,
187396
+ preview_id: initial.preview_id,
187397
+ preview: current,
187398
+ reconciliation: {
187399
+ action: "get_preview",
187400
+ ref: identity.project_ref,
187401
+ id: identity.application_id,
187402
+ environment_id: identity.environment_id,
187403
+ preview_id: initial.preview_id,
187404
+ wait: true,
187405
+ timeout_seconds: timeoutSeconds
187406
+ }
187407
+ });
187408
+ while (true) {
187409
+ if (current.status === "ready")
187410
+ return releaseControlSuccess(operation, {
187411
+ ...identity,
187412
+ preview: current,
187413
+ waiting: { status: "ready", timeout_seconds: timeoutSeconds }
187414
+ });
187415
+ if (current.status !== "provisioning")
187416
+ return releaseControlFailure(operation, "MUTATION_NOT_SUCCEEDED", null, {
187417
+ ...state(),
187418
+ waiting: { status: "stopped", timeout_seconds: timeoutSeconds }
187419
+ });
187420
+ const remaining = Math.floor(deadline - performance.now());
187421
+ if (remaining < 2)
187422
+ return releaseControlFailure(operation, "MUTATION_NOT_SUCCEEDED", null, {
187423
+ ...state(),
187424
+ waiting: { status: "timed_out", timeout_seconds: timeoutSeconds }
187425
+ });
187426
+ const requestBudget = Math.min(120000, remaining);
187427
+ const result = await http.get(`${path}/previews/${initial.preview_id}`, {
187428
+ retry: false,
187429
+ timeoutMs: requestBudget,
187430
+ responseTimeoutMs: requestBudget,
187431
+ totalTimeoutMs: requestBudget,
187432
+ maxJsonBytes: 1048576
187433
+ });
187434
+ if (!result.ok)
187435
+ return releaseControlFailure(operation, "HTTP_ERROR", result.transportError ? null : result.status, {
187436
+ ...state(),
187437
+ waiting: { status: "observation_failed", timeout_seconds: timeoutSeconds }
187438
+ });
187439
+ try {
187440
+ const next = receipt(result.data, {
187441
+ ref: identity.project_ref,
187442
+ id: identity.application_id,
187443
+ environment: identity.environment_id,
187444
+ previewId: initial.preview_id
187445
+ });
187446
+ if (next.status === "planned" || next.release_id !== initial.release_id || next.expires_at !== initial.expires_at || next.resources.database_branch.branch_ref !== initial.resources.database_branch.branch_ref || next.resources.database_branch.data_mode !== initial.resources.database_branch.data_mode || current.resources.application_activation.activation_id !== null && next.resources.application_activation.activation_id !== current.resources.application_activation.activation_id || current.resources.configuration_revision.configuration_id !== null && next.resources.configuration_revision.configuration_id !== current.resources.configuration_revision.configuration_id) {
187447
+ throw new Error("Preview identity changed during observation");
187448
+ }
187449
+ current = next;
187450
+ } catch {
187451
+ return releaseControlFailure(operation, "INVALID_RESPONSE", result.status, {
187452
+ ...state(),
187453
+ waiting: { status: "observation_failed", timeout_seconds: timeoutSeconds }
187454
+ });
187455
+ }
187456
+ if (current.status === "provisioning") {
187457
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, Math.min(1000, deadline - performance.now()))));
187458
+ }
187459
+ }
187460
+ }
187461
+ async function applicationPreviewAction(http, args, project) {
187462
+ const action = required5(args, "action"), ref = required5(args, "ref"), id = required5(args, "id");
187463
+ const environment = required5(args, "environment_id");
187464
+ if (!Check2(ApplicationIdSchema2, environment))
187465
+ throw new Error("Invalid environment ID");
187466
+ const fields = new Set(["action", "ref", "id", "environment_id", ...actionFields[action] ?? []]);
187467
+ if (Object.entries(args).some(([key, value]) => value !== undefined && !fields.has(key))) {
187468
+ throw new Error("Invalid option for preview action");
187469
+ }
187470
+ if (args["ttl_seconds"] !== undefined && !Check2(ttlSecondsSchema, args["ttl_seconds"])) {
187471
+ throw new Error("ttl_seconds must be an integer from 300 to 604800");
187472
+ }
187473
+ if (args["wait"] !== undefined && typeof args["wait"] !== "boolean")
187474
+ throw new Error("wait must be boolean");
187475
+ if (args["timeout_seconds"] !== undefined && (args["wait"] !== true || !Check2(waitSecondsSchema, args["timeout_seconds"]))) {
187476
+ throw new Error("timeout_seconds requires wait and must be an integer from 1 to 3600");
187477
+ }
187478
+ const timeoutSeconds = args["wait"] === true ? args["timeout_seconds"] ?? 300 : undefined;
187479
+ const startedAt = action === "get_preview" && timeoutSeconds !== undefined ? performance.now() : undefined;
187480
+ const operation = `applications.${action}`;
187481
+ const identity = { project_ref: ref, application_id: id, environment_id: environment };
187482
+ const applicationPath = `/v1/projects/${project}/applications/${encodeURIComponent(id)}`;
187483
+ const path = `${applicationPath}/environments/${encodeURIComponent(environment)}`;
187484
+ const previewId = action === "get_preview" || action === "cleanup_preview" ? required5(args, "preview_id") : undefined;
187485
+ const mutation = action === "create_preview" || action === "cleanup_preview";
187486
+ const requestBudget = startedAt === undefined ? 120000 : Math.min(120000, timeoutSeconds * 1000);
187487
+ const options = {
187488
+ timeoutMs: requestBudget,
187489
+ maxJsonBytes: 1048576,
187490
+ responseTimeoutMs: requestBudget,
187491
+ retry: false,
187492
+ ...startedAt === undefined ? {} : { totalTimeoutMs: requestBudget }
187493
+ };
187494
+ let sourceHash;
187495
+ let releaseId;
187496
+ let result;
187497
+ if (action === "get_preview_plan" || action === "create_preview") {
187498
+ releaseId = required5(args, "release_id");
187499
+ if (!Check2(ApplicationReleaseIdSchema2, releaseId))
187500
+ throw new Error("Invalid release ID");
187501
+ if (action === "get_preview_plan") {
187502
+ const query = new URLSearchParams({ release_id: releaseId, branch_ref: required5(args, "branch_ref") });
187503
+ if (typeof args["data_mode"] === "string")
187504
+ query.set("data_mode", args["data_mode"]);
187505
+ if (typeof args["configuration_id"] === "string")
187506
+ query.set("configuration_id", args["configuration_id"]);
187507
+ if (args["ttl_seconds"] !== undefined)
187508
+ query.set("ttl_seconds", String(args["ttl_seconds"]));
187509
+ result = await http.get(`${path}/preview-plan?${query}`, options);
187510
+ } else {
187511
+ const configurationId = required5(args, "configuration_id");
187512
+ if (!Check2(ApplicationConfigurationIdSchema2, configurationId))
187513
+ throw new Error("Invalid configuration ID");
187514
+ const source = await sourceManifest(http, applicationPath, ref, id, releaseId);
187515
+ if (!source.manifest)
187516
+ return releaseControlFailure(operation, source.result.ok ? "INVALID_RESPONSE" : "HTTP_ERROR", source.result.status, { ...identity, release_id: releaseId, configuration_id: configurationId });
187517
+ sourceHash = source.manifest;
187518
+ result = await http.post(`${path}/previews`, {
187519
+ release_id: releaseId,
187520
+ configuration_id: configurationId,
187521
+ data_mode: args["data_mode"] ?? "schema_only",
187522
+ ...args["branch_name"] === undefined ? {} : { branch_name: args["branch_name"] },
187523
+ ...args["ttl_seconds"] === undefined ? {} : { ttl_seconds: args["ttl_seconds"] }
187524
+ }, options);
187525
+ }
187526
+ } else if (action === "list_previews") {
187527
+ result = await http.get(`${path}/previews`, options);
187528
+ } else if (previewId) {
187529
+ if (!Check2(previewIdSchema, previewId))
187530
+ throw new Error("Invalid preview ID");
187531
+ result = action === "cleanup_preview" ? await http.deleteReleaseMutation(`${path}/previews/${previewId}`) : await http.get(`${path}/previews/${previewId}`, options);
187532
+ } else {
187533
+ throw new Error("Unknown preview action");
187534
+ }
187535
+ const state = {
187536
+ ...identity,
187537
+ ...previewId ? { preview_id: previewId } : {},
187538
+ ...releaseId ? { source_release_id: releaseId } : {},
187539
+ reconciliation: previewId ? { action: "get_preview", ref, id, environment_id: environment, preview_id: previewId } : { action: "list_previews", ref, id, environment_id: environment }
187540
+ };
187541
+ if (!result.ok)
187542
+ return mutation ? releaseControlMutationFailure(operation, result, state) : releaseControlFailure(operation, "HTTP_ERROR", result.status, state);
187543
+ try {
187544
+ if (action === "list_previews") {
187545
+ const inventorySchema = _Object_({
187546
+ ..._Object_({ project_ref: branchRefSchema, application_id: ApplicationIdSchema2, environment_id: ApplicationIdSchema2 }).properties,
187547
+ previews: _Array_(receiptSchema, { maxItems: 1000 })
187548
+ }, strict);
187549
+ if (!Check2(inventorySchema, result.data) || result.data.project_ref !== ref || result.data.application_id !== id || result.data.environment_id !== environment)
187550
+ throw new Error;
187551
+ const previews = result.data.previews.map((value) => receipt(value, { ref, id, environment }));
187552
+ if (new Set(previews.map((value) => value.preview_id)).size !== previews.length)
187553
+ throw new Error;
187554
+ return releaseControlSuccess(operation, { ...identity, previews });
187555
+ }
187556
+ const preview = receipt(result.data, {
187557
+ ref,
187558
+ id,
187559
+ environment,
187560
+ ...previewId === undefined ? {} : { previewId }
187561
+ });
187562
+ if (action === "get_preview_plan") {
187563
+ if (preview.status !== "planned" || preview.release_id !== releaseId || preview.resources.database_branch.branch_ref !== args["branch_ref"] || preview.resources.database_branch.data_mode !== (args["data_mode"] ?? "schema_only") || (args["ttl_seconds"] === undefined ? preview.expires_at !== null : preview.expires_at === null))
187564
+ throw new Error;
187565
+ } else {
187566
+ if (preview.status === "planned" || preview.resources.database_branch.branch_ref !== `pv${preview.preview_id.replaceAll("-", "").slice(0, 18)}`)
187567
+ throw new Error;
187568
+ if (action === "create_preview" && (preview.release_id !== applicationReleaseId(preview.resources.database_branch.branch_ref, id, sourceHash) || preview.resources.database_branch.data_mode !== (args["data_mode"] ?? "schema_only") || !Check2(receiptSchema, result.data) || result.data.source_configuration_id !== args["configuration_id"] || preview.expires_at === null || result.data.created_at === undefined || args["ttl_seconds"] !== undefined && Date.parse(preview.expires_at) - Date.parse(result.data.created_at) !== args["ttl_seconds"] * 1000))
187569
+ throw new Error;
187570
+ }
187571
+ if (preview.status === "failed" || action === "cleanup_preview" && preview.status !== "cleaned") {
187572
+ return releaseControlFailure(operation, "MUTATION_NOT_SUCCEEDED", result.status, { ...identity, preview });
187573
+ }
187574
+ if (timeoutSeconds !== undefined)
187575
+ return await waitForPreview(http, path, operation, identity, preview, timeoutSeconds, startedAt);
187576
+ return releaseControlSuccess(operation, { ...identity, preview });
187577
+ } catch {
187578
+ return releaseControlFailure(operation, mutation ? "OUTCOME_UNKNOWN" : "INVALID_RESPONSE", result.status, state);
187579
+ }
187580
+ }
187581
+
187231
187582
  // src/shared/tools/application-tools.ts
187232
187583
  var APPLICATION_TOOL_SCHEMA = {
187233
187584
  action: withDescription(stringEnum([
@@ -187241,6 +187592,7 @@ var APPLICATION_TOOL_SCHEMA = {
187241
187592
  "activate_release",
187242
187593
  "reconcile_activation",
187243
187594
  "retire_activation",
187595
+ ...APPLICATION_PREVIEW_ACTIONS,
187244
187596
  "logs"
187245
187597
  ]), "Action"),
187246
187598
  ref: withDescription(String2(), "Project ref"),
@@ -187258,7 +187610,8 @@ var APPLICATION_TOOL_SCHEMA = {
187258
187610
  service: optional(String2(), "[logs] Application target/service filter"),
187259
187611
  search: optional(String2(), "[logs] Full-text log filter"),
187260
187612
  start: optional(String2(), "[logs] ISO start timestamp"),
187261
- end: optional(String2(), "[logs] ISO end timestamp")
187613
+ end: optional(String2(), "[logs] ISO end timestamp"),
187614
+ ...APPLICATION_PREVIEW_FIELDS
187262
187615
  };
187263
187616
  var responseSchema = _Object_({
187264
187617
  project_ref: String2(),
@@ -187382,6 +187735,9 @@ function registerApplicationTools(server2, http) {
187382
187735
  throw new Error("Invalid application ID");
187383
187736
  const path = `/v1/projects/${project}/applications/${encodeURIComponent(id)}/releases`;
187384
187737
  const operation = `applications.${action}`;
187738
+ if (APPLICATION_PREVIEW_ACTIONS.some((value) => value === action)) {
187739
+ return applicationPreviewAction(http, args, project);
187740
+ }
187385
187741
  if (action === "activate_release" || action === "reconcile_activation" || action === "retire_activation") {
187386
187742
  return activationAction(http, args, project);
187387
187743
  }
@@ -188750,6 +189106,34 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
188750
189106
  }
188751
189107
 
188752
189108
  // src/shared/tools/branch-tools.ts
189109
+ var SHA256_PATTERN = /^[0-9a-f]{64}$/;
189110
+ var PROMOTION_PLAN_MAX_JSON_BYTES = 512 * 1024;
189111
+ var PROMOTION_BLOCK_MESSAGES = {
189112
+ parent_ahead: "Parent history is ahead; recreate or rebase the preview branch.",
189113
+ checksum_mismatch: "Parent and branch migration checksums differ.",
189114
+ stored_checksum_mismatch: "Stored checksum does not match migration SQL.",
189115
+ name_conflict: "Migration name already exists in the parent.",
189116
+ out_of_order_migration: "Migration precedes the latest parent migration.",
189117
+ empty_migration: "Migration has no executable statements.",
189118
+ non_transactional_sql: "Migration requires a separate maintenance path.",
189119
+ unsupported_sql: "Migration exceeds the project-scoped SQL policy."
189120
+ };
189121
+ var PROMOTION_FAILURE_CODES = new Set([
189122
+ "promotion_locked",
189123
+ "promotion_plan_changed",
189124
+ "promotion_blocked",
189125
+ "destructive_confirmation_required",
189126
+ "promotion_apply_failed",
189127
+ "promotion_readback_failed",
189128
+ "promotion_failed",
189129
+ "promotion_plan_failed",
189130
+ "promotion_plan_required",
189131
+ "branch_not_active"
189132
+ ]);
189133
+ var PROMOTION_WARNINGS = [
189134
+ "Only recorded migrations are promoted; branch data and untracked schema changes are not copied.",
189135
+ "Migration SQL can modify parent data and must be reviewed."
189136
+ ];
188753
189137
  function resolveProjectRef(ref, projectRef) {
188754
189138
  const resolved = typeof ref === "string" && ref.trim() ? ref.trim() : projectRef || "";
188755
189139
  if (!resolved)
@@ -188761,32 +189145,188 @@ function requireString(value, field) {
188761
189145
  throw new Error(`'${field}' is required`);
188762
189146
  return value.trim();
188763
189147
  }
189148
+ function isRecord4(value) {
189149
+ return !!value && typeof value === "object" && !Array.isArray(value);
189150
+ }
189151
+ function isSha256(value) {
189152
+ return typeof value === "string" && SHA256_PATTERN.test(value);
189153
+ }
189154
+ function parsePromotionEntry(candidate) {
189155
+ if (!isRecord4(candidate))
189156
+ return null;
189157
+ const { version, name, checksum, statement_count: statementCount, statements, destructive } = candidate;
189158
+ if (typeof version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(version))
189159
+ return null;
189160
+ if (name !== null && (typeof name !== "string" || name.length > 256 || /[\x00-\x1f\x7f]/.test(name)))
189161
+ return null;
189162
+ if (!isSha256(checksum))
189163
+ return null;
189164
+ if (typeof statementCount !== "number" || !Number.isSafeInteger(statementCount) || statementCount < 0)
189165
+ return null;
189166
+ if (typeof destructive !== "boolean")
189167
+ return null;
189168
+ if (statements !== undefined && (!Array.isArray(statements) || statements.length !== statementCount || statements.some((statement) => typeof statement !== "string"))) {
189169
+ return null;
189170
+ }
189171
+ return {
189172
+ version,
189173
+ name,
189174
+ checksum,
189175
+ statement_count: statementCount,
189176
+ destructive
189177
+ };
189178
+ }
189179
+ function parsePromotionBlock(candidate) {
189180
+ if (!isRecord4(candidate))
189181
+ return null;
189182
+ const { code, version, name, message } = candidate;
189183
+ if (typeof code !== "string" || !Object.hasOwn(PROMOTION_BLOCK_MESSAGES, code))
189184
+ return null;
189185
+ if (typeof version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(version))
189186
+ return null;
189187
+ if (name !== null && (typeof name !== "string" || name.length > 256 || /[\x00-\x1f\x7f]/.test(name)))
189188
+ return null;
189189
+ if (typeof message !== "string")
189190
+ return null;
189191
+ const blockCode = code;
189192
+ return {
189193
+ code: blockCode,
189194
+ version,
189195
+ name,
189196
+ message: PROMOTION_BLOCK_MESSAGES[blockCode]
189197
+ };
189198
+ }
189199
+ function uniqueVersions(entries) {
189200
+ const versions = entries.map((entry) => entry.version);
189201
+ return new Set(versions).size === versions.length;
189202
+ }
189203
+ function samePromotionEntry(left, right) {
189204
+ return left.version === right.version && left.checksum === right.checksum && left.name === right.name && left.statement_count === right.statement_count && left.destructive === right.destructive;
189205
+ }
189206
+ function parsePromotionPlan(candidate, expectedScope) {
189207
+ if (!isRecord4(candidate))
189208
+ return null;
189209
+ const {
189210
+ mode,
189211
+ parent_ref: parentRef,
189212
+ branch_ref: branchRef,
189213
+ safe_to_apply: safeToApply,
189214
+ plan_checksum: planChecksum,
189215
+ pending,
189216
+ applied,
189217
+ blocked,
189218
+ warnings,
189219
+ requires_destructive_confirmation: requiresDestructiveConfirmation,
189220
+ ignored_branch_data: ignoredBranchData
189221
+ } = candidate;
189222
+ if (mode !== "migrations" || typeof parentRef !== "string" || typeof branchRef !== "string" || typeof safeToApply !== "boolean" || !isSha256(planChecksum) || !Array.isArray(pending) || !Array.isArray(applied) || !Array.isArray(blocked) || !Array.isArray(warnings) || typeof requiresDestructiveConfirmation !== "boolean" || ignoredBranchData !== true) {
189223
+ return null;
189224
+ }
189225
+ if (parentRef !== expectedScope.parentRef || branchRef !== expectedScope.branchRef)
189226
+ return null;
189227
+ const pendingEntries = pending.map(parsePromotionEntry);
189228
+ const appliedEntries = applied.map(parsePromotionEntry);
189229
+ const blockingFindings = blocked.map(parsePromotionBlock);
189230
+ if (pendingEntries.some((entry) => entry === null) || appliedEntries.some((entry) => entry === null) || blockingFindings.some((entry) => entry === null) || warnings.some((warning) => typeof warning !== "string")) {
189231
+ return null;
189232
+ }
189233
+ const normalizedPending = pendingEntries.filter((entry) => entry !== null);
189234
+ const normalizedApplied = appliedEntries.filter((entry) => entry !== null);
189235
+ const normalizedBlocked = blockingFindings.filter((entry) => entry !== null);
189236
+ const hasDestructivePending = normalizedPending.some((entry) => entry.destructive);
189237
+ if (!uniqueVersions(normalizedPending) || !uniqueVersions(normalizedApplied) || !uniqueVersions([...normalizedPending, ...normalizedApplied]) || normalizedPending.some((entry) => entry.statement_count === 0) || safeToApply !== (normalizedBlocked.length === 0) || requiresDestructiveConfirmation !== hasDestructivePending) {
189238
+ return null;
189239
+ }
189240
+ return {
189241
+ mode,
189242
+ parent_ref: parentRef,
189243
+ branch_ref: branchRef,
189244
+ safe_to_apply: safeToApply,
189245
+ plan_checksum: planChecksum,
189246
+ pending: normalizedPending,
189247
+ applied: normalizedApplied,
189248
+ blocked: normalizedBlocked,
189249
+ warnings: [...PROMOTION_WARNINGS],
189250
+ requires_destructive_confirmation: requiresDestructiveConfirmation,
189251
+ ignored_branch_data: true
189252
+ };
189253
+ }
189254
+ function parsePromotionResult(candidate, expectedScope, reviewedPending) {
189255
+ if (!isRecord4(candidate) || candidate.promoted !== true || candidate.mode !== "migrations" || candidate.project_ref !== expectedScope.parentRef || candidate.branch_ref !== expectedScope.branchRef || candidate.branch_data_copied !== false || !Array.isArray(candidate.applied)) {
189256
+ return null;
189257
+ }
189258
+ const applied = candidate.applied.map(parsePromotionEntry);
189259
+ if (applied.some((entry) => entry === null))
189260
+ return null;
189261
+ const normalizedApplied = applied.filter((entry) => entry !== null);
189262
+ if (!uniqueVersions(normalizedApplied) || normalizedApplied.length !== reviewedPending.length)
189263
+ return null;
189264
+ const reviewedByVersion = new Map(reviewedPending.map((entry) => [entry.version, entry]));
189265
+ if (normalizedApplied.some((entry) => {
189266
+ const reviewed = reviewedByVersion.get(entry.version);
189267
+ return !reviewed || !samePromotionEntry(entry, reviewed);
189268
+ }))
189269
+ return null;
189270
+ const plan = parsePromotionPlan(candidate.plan, expectedScope);
189271
+ if (!plan || !plan.safe_to_apply || plan.pending.length > 0)
189272
+ return null;
189273
+ const readbackByVersion = new Map(plan.applied.map((entry) => [entry.version, entry]));
189274
+ if (normalizedApplied.some((entry) => {
189275
+ const readback = readbackByVersion.get(entry.version);
189276
+ return !readback || !samePromotionEntry(entry, readback);
189277
+ }))
189278
+ return null;
189279
+ return {
189280
+ promoted: true,
189281
+ mode: "migrations",
189282
+ project_ref: expectedScope.parentRef,
189283
+ branch_ref: expectedScope.branchRef,
189284
+ applied: normalizedApplied,
189285
+ plan,
189286
+ branch_data_copied: false
189287
+ };
189288
+ }
189289
+ function safePromotionCode(result) {
189290
+ if (!isRecord4(result.data) || typeof result.data.code !== "string")
189291
+ return;
189292
+ return PROMOTION_FAILURE_CODES.has(result.data.code) ? result.data.code : undefined;
189293
+ }
189294
+ function appliedVersions(result) {
189295
+ if (result.transportError || result.responseReadError || !isRecord4(result.data) || !Array.isArray(result.data.applied))
189296
+ return [];
189297
+ const entries = result.data.applied.map(parsePromotionEntry);
189298
+ if (entries.some((entry) => !entry))
189299
+ return [];
189300
+ const validated = entries.filter((entry) => entry !== null);
189301
+ return uniqueVersions(validated) ? validated.map((entry) => entry.version) : [];
189302
+ }
189303
+ function promotionFailure(response, json, unknownOutcome) {
189304
+ if (json)
189305
+ return response;
189306
+ const state = JSON.parse(response.content[0].text);
189307
+ if (!isRecord4(state) || !isRecord4(state.error))
189308
+ return response;
189309
+ const lines = [
189310
+ unknownOutcome ? "Promotion outcome is unknown; do not repeat the request automatically." : "Promotion request was not verified.",
189311
+ `Error: ${state.error.code}`,
189312
+ `Project: ${state.project_ref}; branch: ${state.branch_ref}`,
189313
+ ...typeof state.reason === "string" ? [`Reason: ${state.reason}`] : [],
189314
+ ...state.mutation_sent === false ? ["No promotion request was sent."] : [],
189315
+ ...typeof state.plan_checksum === "string" ? [`Reviewed checksum: ${state.plan_checksum}`] : [],
189316
+ ...Array.isArray(state.reported_applied_versions) && state.reported_applied_versions.length > 0 ? [`Server-reported applied versions: ${state.reported_applied_versions.join(", ")}`] : [],
189317
+ "Fetch a fresh promotion_plan and review ledger state before another promote."
189318
+ ];
189319
+ return { isError: true, content: [{ type: "text", text: lines.join(`
189320
+ `) }] };
189321
+ }
188764
189322
  function responseError(result) {
188765
- if (result.data && typeof result.data === "object") {
188766
- const data = result.data;
188767
- const message = data.error || data.message;
188768
- if (typeof message === "string") {
188769
- const details = [message];
188770
- if (Array.isArray(data.applied) && data.applied.length > 0) {
188771
- const versions = data.applied.map((entry) => entry && typeof entry === "object" ? entry.version : null).filter((version) => typeof version === "string");
188772
- details.push(`Applied before failure: ${versions.join(", ") || data.applied.length}`);
188773
- details.push("Fetch a fresh promotion_plan before retrying.");
188774
- }
188775
- if (data.replacement_committed === true) {
188776
- details.push(`Database replacement committed; recovery is required${typeof data.backup_database === "string" ? ` using backup ${data.backup_database}` : ""}.`);
188777
- }
188778
- return details.join(`
188779
- `);
188780
- }
189323
+ if (isRecord4(result.data)) {
189324
+ const message = result.data.error || result.data.message;
189325
+ if (typeof message === "string")
189326
+ return message;
188781
189327
  }
188782
189328
  return `Request failed with HTTP ${result.status}`;
188783
189329
  }
188784
- function isPromotionPlan(candidate) {
188785
- if (!candidate || typeof candidate !== "object")
188786
- return false;
188787
- const plan = candidate;
188788
- return plan.mode === "migrations" && typeof plan.safe_to_apply === "boolean" && typeof plan.plan_checksum === "string" && Array.isArray(plan.pending) && Array.isArray(plan.applied) && Array.isArray(plan.blocked) && Array.isArray(plan.warnings) && typeof plan.requires_destructive_confirmation === "boolean";
188789
- }
188790
189330
  function projectPath(ref) {
188791
189331
  return `/v1/projects/${encodeURIComponent(ref)}/branches`;
188792
189332
  }
@@ -188828,14 +189368,10 @@ function readOnlyResult() {
188828
189368
  content: [{ type: "text", text: "⚠️ Branch write blocked in read-only mode." }]
188829
189369
  };
188830
189370
  }
188831
- function formatPromotionResult(responseValue) {
188832
- if (!responseValue || typeof responseValue !== "object")
188833
- return "Migrations promoted.";
188834
- const response = responseValue;
188835
- const applied = Array.isArray(response.applied) ? response.applied : [];
188836
- const versions = applied.map((entry) => entry && typeof entry === "object" ? entry.version : null).filter((version) => typeof version === "string");
189371
+ function formatPromotionResult(response) {
189372
+ const versions = response.applied.map((entry) => entry.version);
188837
189373
  return [
188838
- `Migration promotion completed: ${applied.length} applied.`,
189374
+ `Migration promotion completed: ${response.applied.length} applied.`,
188839
189375
  ...versions.length > 0 ? [`Versions: ${versions.join(", ")}`] : [],
188840
189376
  "Branch data was not automatically copied to the parent project."
188841
189377
  ].join(`
@@ -188855,13 +189391,19 @@ function registerBranchTools(server2, http, options = {}) {
188855
189391
  name: optional(String2(), "[create] Branch name"),
188856
189392
  data_mode: optional(stringEnum(["schema_only", "full_clone"]), "[create] Preview data mode (default: schema_only)"),
188857
189393
  plan_checksum: optional(String2(), "[promote] Reviewed plan checksum from promotion_plan"),
188858
- confirm_destructive: optional(Boolean2(), "[promote] Confirm reviewed destructive migrations")
189394
+ confirm_destructive: optional(Boolean2(), "[promote] Confirm reviewed destructive migrations"),
189395
+ json: optional(Boolean2(), "[promotion_plan/promote] Return machine-readable JSON")
188859
189396
  }, async (args) => {
188860
189397
  const action = requireString(args.action, "action");
188861
189398
  const ref = resolveProjectRef(args.ref, options.projectRef);
188862
189399
  const writeAction = action === "create" || action === "delete" || action === "promote";
188863
- if (writeAction && options.readOnly)
188864
- return readOnlyResult();
189400
+ if (writeAction && options.readOnly) {
189401
+ return args.json && action === "promote" ? releaseControlFailure("branch.promote", "MUTATION_NOT_SUCCEEDED", null, {
189402
+ project_ref: ref,
189403
+ branch_ref: args.branch_ref,
189404
+ reason: "READ_ONLY"
189405
+ }) : readOnlyResult();
189406
+ }
188865
189407
  let result;
188866
189408
  if (action === "list") {
188867
189409
  result = await http.get(projectPath(ref));
@@ -188876,29 +189418,91 @@ function registerBranchTools(server2, http, options = {}) {
188876
189418
  result = await http.delete(branchPath(ref, branchRef));
188877
189419
  } else if (action === "promotion_plan") {
188878
189420
  const branchRef = requireString(args.branch_ref, "branch_ref");
188879
- result = await http.get(`${branchPath(ref, branchRef)}/promote/plan`);
188880
- if (result.ok && isPromotionPlan(result.data)) {
188881
- return { content: [{ type: "text", text: formatPromotionPlan(result.data) }] };
189421
+ const scope = { project_ref: ref, branch_ref: branchRef };
189422
+ result = await http.get(`${branchPath(ref, branchRef)}/promote/plan`, {
189423
+ maxJsonBytes: PROMOTION_PLAN_MAX_JSON_BYTES,
189424
+ responseTimeoutMs: 5000
189425
+ });
189426
+ if (!result.ok) {
189427
+ return promotionFailure(releaseControlFailure("branch.promotion_plan", "HTTP_ERROR", result.transportError ? null : result.status, scope), args.json === true, false);
188882
189428
  }
189429
+ const plan = parsePromotionPlan(result.data, { parentRef: ref, branchRef });
189430
+ if (result.status !== 200 || !plan) {
189431
+ return promotionFailure(releaseControlFailure("branch.promotion_plan", "INVALID_RESPONSE", result.status, scope), args.json === true, false);
189432
+ }
189433
+ return args.json ? releaseControlSuccess("branch.promotion_plan", { ...scope, plan }) : { content: [{ type: "text", text: formatPromotionPlan(plan) }] };
188883
189434
  } else if (action === "promote") {
188884
189435
  const branchRef = requireString(args.branch_ref, "branch_ref");
188885
189436
  const planChecksum = requireString(args.plan_checksum, "plan_checksum");
189437
+ if (!isSha256(planChecksum))
189438
+ throw new Error("'plan_checksum' must be a 64-character lowercase SHA-256 checksum");
189439
+ const state = {
189440
+ project_ref: ref,
189441
+ branch_ref: branchRef,
189442
+ plan_checksum: planChecksum,
189443
+ automatic_retry: false,
189444
+ reconciliation: { action: "promotion_plan", ref, branch_ref: branchRef }
189445
+ };
189446
+ const preflight = await http.get(`${branchPath(ref, branchRef)}/promote/plan`, {
189447
+ maxJsonBytes: PROMOTION_PLAN_MAX_JSON_BYTES,
189448
+ responseTimeoutMs: 5000
189449
+ });
189450
+ const beforeMutation = { ...state, mutation_sent: false };
189451
+ if (!preflight.ok) {
189452
+ return promotionFailure(releaseControlFailure("branch.promote", "HTTP_ERROR", preflight.transportError ? null : preflight.status, { ...beforeMutation, reason: "PREFLIGHT_FAILED" }), args.json === true, false);
189453
+ }
189454
+ const plan = parsePromotionPlan(preflight.data, { parentRef: ref, branchRef });
189455
+ if (preflight.status !== 200 || !plan) {
189456
+ return promotionFailure(releaseControlFailure("branch.promote", "INVALID_RESPONSE", preflight.status, { ...beforeMutation, reason: "PREFLIGHT_UNVERIFIED" }), args.json === true, false);
189457
+ }
189458
+ const preflightReason = plan.plan_checksum !== planChecksum ? "PLAN_CHANGED" : !plan.safe_to_apply ? "PLAN_BLOCKED" : plan.requires_destructive_confirmation && args.confirm_destructive !== true ? "DESTRUCTIVE_CONFIRMATION_REQUIRED" : null;
189459
+ if (preflightReason) {
189460
+ return promotionFailure(releaseControlFailure("branch.promote", "MUTATION_NOT_SUCCEEDED", null, { ...beforeMutation, reason: preflightReason, observed_plan_checksum: plan.plan_checksum }), args.json === true, false);
189461
+ }
189462
+ if (plan.pending.length === 0) {
189463
+ return args.json ? releaseControlSuccess("branch.promote", {
189464
+ ...beforeMutation,
189465
+ promoted: false,
189466
+ unchanged: true,
189467
+ mode: "migrations",
189468
+ applied: [],
189469
+ plan,
189470
+ branch_data_copied: false,
189471
+ reviewed_plan_checksum: planChecksum
189472
+ }) : { content: [{ type: "text", text: "Migration promotion unchanged: no pending migrations; no promotion request sent." }] };
189473
+ }
188886
189474
  result = await http.post(`${branchPath(ref, branchRef)}/promote`, {
188887
189475
  mode: "migrations",
188888
189476
  plan_checksum: planChecksum,
188889
189477
  confirm_destructive: args.confirm_destructive === true
189478
+ }, {
189479
+ maxJsonBytes: PROMOTION_PLAN_MAX_JSON_BYTES,
189480
+ responseTimeoutMs: 5000,
189481
+ timeoutMs: 30000
188890
189482
  });
189483
+ if (!result.ok) {
189484
+ return promotionFailure(releaseControlMutationFailure("branch.promote", result, {
189485
+ ...state,
189486
+ mutation_sent: true,
189487
+ reported_applied_versions: appliedVersions(result),
189488
+ ...safePromotionCode(result) ? { server_code: safePromotionCode(result) } : {}
189489
+ }), args.json === true, result.transportError === true || result.responseReadError === true || result.status === 408 || result.status >= 500);
189490
+ }
189491
+ const promoted = parsePromotionResult(result.data, { parentRef: ref, branchRef }, plan.pending);
189492
+ if (result.status !== 200 || !promoted) {
189493
+ return promotionFailure(releaseControlFailure("branch.promote", "OUTCOME_UNKNOWN", result.status, { ...state, mutation_sent: true }), args.json === true, true);
189494
+ }
189495
+ return args.json ? releaseControlSuccess("branch.promote", {
189496
+ ...promoted,
189497
+ unchanged: false,
189498
+ mutation_sent: true,
189499
+ reviewed_plan_checksum: planChecksum
189500
+ }) : { content: [{ type: "text", text: formatPromotionResult(promoted) }] };
188891
189501
  } else {
188892
189502
  throw new Error(`Unknown branch action: ${action}`);
188893
189503
  }
188894
189504
  if (!result.ok) {
188895
- return {
188896
- isError: true,
188897
- content: [{ type: "text", text: `❌ ${responseError(result)}` }]
188898
- };
188899
- }
188900
- if (action === "promote") {
188901
- return { content: [{ type: "text", text: formatPromotionResult(result.data) }] };
189505
+ return { isError: true, content: [{ type: "text", text: responseError(result) }] };
188902
189506
  }
188903
189507
  return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
188904
189508
  });
@@ -189433,7 +190037,7 @@ import { resolve as resolve11 } from "node:path";
189433
190037
  // src/shared/tools/lite-cli-command.ts
189434
190038
  import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync6 } from "node:fs";
189435
190039
  import { isAbsolute as isAbsolute3, join as join8, relative as relative3, resolve as resolve10 } from "node:path";
189436
- function isRecord4(value) {
190040
+ function isRecord5(value) {
189437
190041
  return value !== null && typeof value === "object" && !Array.isArray(value);
189438
190042
  }
189439
190043
  function resolveLiteCommand(workdir, environment = process.env) {
@@ -189448,7 +190052,7 @@ function resolveLiteCommand(workdir, environment = process.env) {
189448
190052
  if (!existsSync8(manifestPath))
189449
190053
  return ["supacloud-lite"];
189450
190054
  const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
189451
- if (!isRecord4(manifest) || manifest.name !== "@supacloud/lite" || !isRecord4(manifest.bin)) {
190055
+ if (!isRecord5(manifest) || manifest.name !== "@supacloud/lite" || !isRecord5(manifest.bin)) {
189452
190056
  throw new Error("Invalid installed @supacloud/lite package manifest");
189453
190057
  }
189454
190058
  const entry = manifest.bin["supacloud-lite"];
@@ -193380,18 +193984,30 @@ var REMOTE_APP_ACTIONS = {
193380
193984
  rollback: "activate_release",
193381
193985
  reconcile: "reconcile_activation",
193382
193986
  retire: "retire_activation",
193383
- logs: "logs"
193987
+ logs: "logs",
193988
+ "preview-plan": "get_preview_plan",
193989
+ preview: "create_preview",
193990
+ previews: "list_previews",
193991
+ "preview-status": "get_preview",
193992
+ "preview-cleanup": "cleanup_preview"
193384
193993
  };
193385
193994
  var REMOTE_APP_DESCRIPTIONS = {
193386
- ref: "[upload/configure/deploy/status/rollback/reconcile/retire] Project ref (defaults to context)",
193387
- id: "[upload/configure/deploy/status/rollback/reconcile/retire] Application ID",
193388
- environment_id: "[configure/deploy/status/rollback/reconcile/retire] Environment ID",
193389
- configuration_id: "[deploy/rollback] Required immutable configuration revision",
193995
+ ref: "[upload/configure/deploy/status/rollback/reconcile/retire/preview-plan/preview/previews/preview-status/preview-cleanup] Project ref (defaults to context)",
193996
+ id: "[upload/configure/deploy/status/rollback/reconcile/retire/preview-plan/preview/previews/preview-status/preview-cleanup] Application ID",
193997
+ environment_id: "[configure/deploy/status/rollback/reconcile/retire/preview-plan/preview/previews/preview-status/preview-cleanup] Environment ID",
193998
+ configuration_id: "[deploy/rollback/preview] Required immutable configuration revision",
193390
193999
  activation_id: "[deploy/rollback/reconcile/retire] Required explicit activation ID",
193391
194000
  expected_activation_id: "[deploy/rollback] Required current activation ID, or absent for first activation",
193392
194001
  configuration_path: "[configure] Configuration write JSON including revision and expected revision",
193393
194002
  manifest_path: "[upload] Local delivery.manifest.json",
193394
- release_id: "[deploy/rollback/reconcile] Required immutable application release ID"
194003
+ release_id: "[deploy/rollback/reconcile/preview-plan/preview] Required immutable application release ID",
194004
+ preview_id: "[preview-status/preview-cleanup] Preview receipt ID",
194005
+ branch_ref: "[preview-plan] Proposed branch ref; preview assigns its own",
194006
+ branch_name: "[preview] Branch display name",
194007
+ data_mode: "[preview-plan/preview] Default schema_only; full_clone copies rows",
194008
+ ttl_seconds: "[preview-plan/preview] Lifetime from 300 to 604800 seconds; creation defaults to the platform TTL",
194009
+ wait: "[preview/preview-status] Wait for verified readiness of the selected preview",
194010
+ timeout_seconds: "[preview/preview-status] Observation budget with --wait, from 1 to 3600 seconds (default 300)"
193395
194011
  };
193396
194012
  var { action: _remoteAction, ...remoteFields } = APPLICATION_TOOL_SCHEMA;
193397
194013
  var REMOTE_APP_SCHEMA = Partial(_Object_(remoteFields)).properties;
@@ -194396,7 +195012,12 @@ function registerAppTools(server2, options = {}) {
194396
195012
  "rollback",
194397
195013
  "reconcile",
194398
195014
  "retire",
194399
- "logs"
195015
+ "logs",
195016
+ "preview-plan",
195017
+ "preview",
195018
+ "previews",
195019
+ "preview-status",
195020
+ "preview-cleanup"
194400
195021
  ]), "App action; upload/configure only prepare, rollback activates an explicit old release without schema downgrade"),
194401
195022
  kind: optional(stringEnum(["module", "command", "query", "controller", "job", "contract", "resource"]), "[generate] Scaffold kind"),
194402
195023
  template: optional(stringEnum(["minimal", "http", "command", "edge"]), "[init] Minimal application by default; explicit http/command/edge recipes"),
@@ -195840,7 +196461,7 @@ var RELEASE_CANARY_DISABLE_RECEIPT_KEYS = new Set(["fixtureId", "state", "idempo
195840
196461
  var RELEASE_CANARY_DISABLE_RPC_PATH = "/rest/v1/rpc/fa_release_canary_fixture_disable";
195841
196462
  var RELEASE_CANARY_PENDING_RPC_PATH = "/rest/v1/rpc/fa_release_canary_fixture_pending";
195842
196463
  var RELEASE_CANARY_CLAIM_MAX_LENGTH = 2048;
195843
- function isRecord5(value) {
196464
+ function isRecord6(value) {
195844
196465
  return value !== null && typeof value === "object" && !Array.isArray(value);
195845
196466
  }
195846
196467
  function canonicalTimestamp4(value) {
@@ -195856,7 +196477,7 @@ function backupBelongsToProject(backupId, projectRef) {
195856
196477
  return BACKUP_ID.test(backupId) && backupId.startsWith(`logical-full_${projectRef}_`);
195857
196478
  }
195858
196479
  function verifiedBackup(value, projectRef) {
195859
- if (!isRecord5(value) || typeof value.backup_id !== "string" || !backupBelongsToProject(value.backup_id, projectRef) || value.project_ref !== projectRef || 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))
196480
+ if (!isRecord6(value) || typeof value.backup_id !== "string" || !backupBelongsToProject(value.backup_id, projectRef) || value.project_ref !== projectRef || 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))
195860
196481
  return null;
195861
196482
  return {
195862
196483
  backup_id: value.backup_id,
@@ -195870,7 +196491,7 @@ function verifiedBackup(value, projectRef) {
195870
196491
  };
195871
196492
  }
195872
196493
  function backupInventory(value, projectRef) {
195873
- if (!isRecord5(value) || !Array.isArray(value.backups))
196494
+ if (!isRecord6(value) || !Array.isArray(value.backups))
195874
196495
  return null;
195875
196496
  const backups = value.backups.map((backup) => verifiedBackup(backup, projectRef));
195876
196497
  if (backups.some((backup) => backup === null))
@@ -195949,7 +196570,7 @@ function readInventoryFailure(operation, read) {
195949
196570
  return null;
195950
196571
  }
195951
196572
  function postgrestStatus(value) {
195952
- if (!isRecord5(value) || value.component !== "postgrest" || !["running", "stopped"].includes(String(value.desired)) || !["running", "stopped", "starting", "error"].includes(String(value.actual)) || !["healthy", "unhealthy", "unknown"].includes(String(value.health)))
196573
+ if (!isRecord6(value) || value.component !== "postgrest" || !["running", "stopped"].includes(String(value.desired)) || !["running", "stopped", "starting", "error"].includes(String(value.actual)) || !["healthy", "unhealthy", "unknown"].includes(String(value.health)))
195953
196574
  return null;
195954
196575
  return {
195955
196576
  desired: value.desired,
@@ -195970,7 +196591,7 @@ function readPostgrestFailure(operation, read) {
195970
196591
  return read.response.status === 200 && read.status ? null : releaseControlFailure(operation, "INVALID_RESPONSE", read.response.status);
195971
196592
  }
195972
196593
  function isRestartReceipt(value) {
195973
- return isRecord5(value) && value.service === "postgrest" && value.action === "restart" && value.success === true;
196594
+ return isRecord6(value) && value.service === "postgrest" && value.action === "restart" && value.success === true;
195974
196595
  }
195975
196596
  function releaseCanaryStageInput(subject, requestId) {
195976
196597
  if (typeof subject !== "string" || !UUID.test(subject))
@@ -195980,7 +196601,7 @@ function releaseCanaryStageInput(subject, requestId) {
195980
196601
  return { p_subject: subject, p_request_id: requestId };
195981
196602
  }
195982
196603
  function releaseCanaryStageReceipt(value) {
195983
- if (!isRecord5(value) || Object.keys(value).some((key) => !RELEASE_CANARY_STAGE_RECEIPT_KEYS.has(key)) || Object.keys(value).length !== RELEASE_CANARY_STAGE_RECEIPT_KEYS.size || typeof value.fixtureId !== "string" || !UUID.test(value.fixtureId) || typeof value.tenantKey !== "string" || !RELEASE_CANARY_TENANT_KEY.test(value.tenantKey) || value.state !== "staged" || value.idempotent !== true)
196604
+ if (!isRecord6(value) || Object.keys(value).some((key) => !RELEASE_CANARY_STAGE_RECEIPT_KEYS.has(key)) || Object.keys(value).length !== RELEASE_CANARY_STAGE_RECEIPT_KEYS.size || typeof value.fixtureId !== "string" || !UUID.test(value.fixtureId) || typeof value.tenantKey !== "string" || !RELEASE_CANARY_TENANT_KEY.test(value.tenantKey) || value.state !== "staged" || value.idempotent !== true)
195984
196605
  return null;
195985
196606
  return {
195986
196607
  fixtureId: value.fixtureId,
@@ -196023,7 +196644,7 @@ function releaseCanaryDisableInput(fixtureId, disableRequestId, issuer, subject)
196023
196644
  };
196024
196645
  }
196025
196646
  function releaseCanaryDisableReceipt(receiptCandidate, fixtureId) {
196026
- if (!isRecord5(receiptCandidate) || Object.keys(receiptCandidate).some((key) => !RELEASE_CANARY_DISABLE_RECEIPT_KEYS.has(key)) || Object.keys(receiptCandidate).length !== RELEASE_CANARY_DISABLE_RECEIPT_KEYS.size || receiptCandidate.fixtureId !== fixtureId || receiptCandidate.state !== "disabled" || typeof receiptCandidate.idempotent !== "boolean")
196647
+ if (!isRecord6(receiptCandidate) || Object.keys(receiptCandidate).some((key) => !RELEASE_CANARY_DISABLE_RECEIPT_KEYS.has(key)) || Object.keys(receiptCandidate).length !== RELEASE_CANARY_DISABLE_RECEIPT_KEYS.size || receiptCandidate.fixtureId !== fixtureId || receiptCandidate.state !== "disabled" || typeof receiptCandidate.idempotent !== "boolean")
196027
196648
  return null;
196028
196649
  return { fixtureId, state: "disabled", idempotent: receiptCandidate.idempotent };
196029
196650
  }
@@ -196196,7 +196817,7 @@ function handleScopeRebind(args) {
196196
196817
  } catch {
196197
196818
  throw new Error(`Scope file '${filePath}' is not valid JSON`);
196198
196819
  }
196199
- if (!isRecord5(parsed) || typeof parsed.baseCommit !== "string" || !/^[0-9a-f]{40}$/.test(parsed.baseCommit)) {
196820
+ if (!isRecord6(parsed) || typeof parsed.baseCommit !== "string" || !/^[0-9a-f]{40}$/.test(parsed.baseCommit)) {
196200
196821
  throw new Error(`Scope file '${filePath}' has missing or invalid baseCommit`);
196201
196822
  }
196202
196823
  const previousBaseCommit = parsed.baseCommit;
@@ -196260,7 +196881,7 @@ function handleScopeInspect(args) {
196260
196881
  } catch {
196261
196882
  throw new Error(`Scope file '${args.file}' is not valid JSON`);
196262
196883
  }
196263
- if (!isRecord5(parsed) || typeof parsed.baseCommit !== "string" || !/^[0-9a-f]{40}$/.test(parsed.baseCommit)) {
196884
+ if (!isRecord6(parsed) || typeof parsed.baseCommit !== "string" || !/^[0-9a-f]{40}$/.test(parsed.baseCommit)) {
196264
196885
  throw new Error(`Scope file '${args.file}' has missing or invalid baseCommit`);
196265
196886
  }
196266
196887
  const scopeSha256 = genericScopedReleaseScopeSha256(parsed);
@@ -196422,7 +197043,7 @@ function registerReleaseTools(server2, http, options = {}) {
196422
197043
  if (!mutation.ok || mutation.status !== 200) {
196423
197044
  return mutationFailure("release.logical_backup.create", mutation);
196424
197045
  }
196425
- const responseBackup = isRecord5(mutation.data) ? verifiedBackup(mutation.data.backup, projectRef) : null;
197046
+ const responseBackup = isRecord6(mutation.data) ? verifiedBackup(mutation.data.backup, projectRef) : null;
196426
197047
  const afterFailure = readInventoryFailure("release.logical_backup.create", after);
196427
197048
  const addedBackup = after.inventory && newlyCreatedBackup(before.inventory, after.inventory);
196428
197049
  if (!responseBackup || afterFailure || !addedBackup || !equalBackup(responseBackup, addedBackup)) {
@@ -196447,7 +197068,7 @@ function registerReleaseTools(server2, http, options = {}) {
196447
197068
  if (!mutation.ok || mutation.status !== 200) {
196448
197069
  return mutationFailure("release.logical_backup.restore", mutation);
196449
197070
  }
196450
- const responseBackup = isRecord5(mutation.data) ? verifiedBackup(mutation.data.restored_backup, projectRef) : null;
197071
+ const responseBackup = isRecord6(mutation.data) ? verifiedBackup(mutation.data.restored_backup, projectRef) : null;
196451
197072
  const after = await readInventory(http, projectRef);
196452
197073
  const afterFailure = readInventoryFailure("release.logical_backup.restore", after);
196453
197074
  const restoredInventoryBackup = after.inventory?.find((backup) => backup.backup_id === request.backup_id);
@@ -197964,7 +198585,7 @@ function registerRemoteDevTools(server2, options = {}) {
197964
198585
  // package.json
197965
198586
  var package_default = {
197966
198587
  name: "@supacloud/cli",
197967
- version: "0.77.0",
198588
+ version: "0.78.0",
197968
198589
  description: "Project-scoped CLI for SupaCloud users",
197969
198590
  type: "module",
197970
198591
  main: "./dist/index.js",
@@ -198286,6 +198907,11 @@ EXAMPLES
198286
198907
  ${preferredCommand} app configure --ref abc123 --id orders --environment_id test --configuration_path configuration.json
198287
198908
  ${preferredCommand} app deploy --ref abc123 --id orders --environment_id test --release_id <sha256> --configuration_id <uuid> --activation_id <uuid> --expected_activation_id absent
198288
198909
  ${preferredCommand} app status --ref abc123 --id orders --environment_id test
198910
+ ${preferredCommand} app preview-plan --ref abc123 --id orders --environment_id test --release_id <sha256> --branch_ref preview-orders
198911
+ ${preferredCommand} app preview --ref abc123 --id orders --environment_id test --release_id <sha256> --configuration_id <uuid> --ttl_seconds 3600 --wait
198912
+ ${preferredCommand} app previews --ref abc123 --id orders --environment_id test
198913
+ ${preferredCommand} app preview-status --ref abc123 --id orders --environment_id test --preview_id <uuid> --wait --timeout_seconds 300
198914
+ ${preferredCommand} app preview-cleanup --ref abc123 --id orders --environment_id test --preview_id <uuid>
198289
198915
  ${preferredCommand} app logs --ref abc123 --id orders --environment_id test
198290
198916
  ${preferredCommand} app rollback --ref abc123 --id orders --environment_id test --release_id <old-sha256> --configuration_id <uuid> --activation_id <new-uuid> --expected_activation_id <current-uuid>
198291
198917
  ${preferredCommand} app check --root . --strict
@@ -9,6 +9,11 @@ declare const REMOTE_APP_ACTIONS: {
9
9
  readonly reconcile: "reconcile_activation";
10
10
  readonly retire: "retire_activation";
11
11
  readonly logs: "logs";
12
+ readonly "preview-plan": "get_preview_plan";
13
+ readonly preview: "create_preview";
14
+ readonly previews: "list_previews";
15
+ readonly "preview-status": "get_preview";
16
+ readonly "preview-cleanup": "cleanup_preview";
12
17
  };
13
18
  export interface AppToolOptions {
14
19
  getApplications?: () => ((args: Record<string, unknown>) => Promise<ToolResult>) | undefined;
@@ -29,6 +34,13 @@ export interface AppToolArguments {
29
34
  configuration_path?: string;
30
35
  manifest_path?: string;
31
36
  release_id?: string;
37
+ preview_id?: string;
38
+ branch_ref?: string;
39
+ branch_name?: string;
40
+ data_mode?: "schema_only" | "full_clone";
41
+ ttl_seconds?: number;
42
+ wait?: boolean;
43
+ timeout_seconds?: number;
32
44
  kind?: "module" | "command" | "query" | "controller" | "job" | "contract" | "resource";
33
45
  template?: "minimal" | "http" | "command" | "edge";
34
46
  name?: string;
@@ -0,0 +1,74 @@
1
+ import { Type, type Static } from "typebox";
2
+ import type { HttpTransport } from "../transports/http";
3
+ export declare const APPLICATION_PREVIEW_ACTIONS: readonly ["get_preview_plan", "create_preview", "list_previews", "get_preview", "cleanup_preview"];
4
+ export declare const APPLICATION_PREVIEW_FIELDS: {
5
+ preview_id: Type.TOptional<Type.TString>;
6
+ branch_ref: Type.TOptional<Type.TString>;
7
+ branch_name: Type.TOptional<Type.TString>;
8
+ data_mode: Type.TOptional<Type.TEnum<("schema_only" | "full_clone")[]>>;
9
+ ttl_seconds: Type.TOptional<Type.TInteger>;
10
+ wait: Type.TOptional<Type.TBoolean>;
11
+ timeout_seconds: Type.TOptional<Type.TInteger>;
12
+ };
13
+ declare const receiptSchema: Type.TObject<{
14
+ schema: Type.TLiteral<"supacloud.application-preview.v1">;
15
+ preview_id: Type.TString;
16
+ project_ref: Type.TString;
17
+ application_id: Type.TString;
18
+ environment_id: Type.TString;
19
+ release_id: Type.TString;
20
+ expires_at: Type.TUnion<[Type.TString, Type.TNull]>;
21
+ status: Type.TEnum<("failed" | "ready" | "planned" | "cleaned" | "provisioning")[]>;
22
+ resources: Type.TObject<{
23
+ build_artifact: Type.TObject<{
24
+ status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
25
+ release_id: Type.TString;
26
+ }>;
27
+ database_branch: Type.TObject<{
28
+ status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
29
+ branch_ref: Type.TString;
30
+ data_mode: Type.TEnum<("schema_only" | "full_clone")[]>;
31
+ }>;
32
+ queue_namespace: Type.TObject<{
33
+ status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
34
+ namespace: Type.TString;
35
+ }>;
36
+ storage_namespace: Type.TObject<{
37
+ status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
38
+ namespace: Type.TString;
39
+ }>;
40
+ test_secret: Type.TObject<{
41
+ status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
42
+ name: Type.TString;
43
+ value_issued: Type.TLiteral<false>;
44
+ }>;
45
+ configuration_revision: Type.TObject<{
46
+ status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
47
+ configuration_id: Type.TUnion<[Type.TString, Type.TNull]>;
48
+ }>;
49
+ application_activation: Type.TObject<{
50
+ status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
51
+ activation_id: Type.TUnion<[Type.TString, Type.TNull]>;
52
+ }>;
53
+ smoke_test: Type.TObject<{
54
+ status: Type.TEnum<("pending" | "failed" | "ready" | "cleaned")[]>;
55
+ checks: Type.TArray<Type.TString>;
56
+ passed: Type.TArray<Type.TString>;
57
+ failed: Type.TArray<Type.TString>;
58
+ }>;
59
+ }>;
60
+ cleanup: Type.TObject<{
61
+ required: Type.TBoolean;
62
+ completed: Type.TBoolean;
63
+ error: Type.TUnion<[Type.TString, Type.TNull]>;
64
+ }>;
65
+ branch_name: Type.TOptional<Type.TString>;
66
+ queue_name: Type.TOptional<Type.TString>;
67
+ test_secret_name: Type.TOptional<Type.TString>;
68
+ source_configuration_id: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
69
+ created_at: Type.TOptional<Type.TString>;
70
+ updated_at: Type.TOptional<Type.TString>;
71
+ }>;
72
+ export declare function applicationPreviewAction(http: HttpTransport, args: Record<string, unknown>, project: string): Promise<import("./release-control-response").ReleaseControlToolResponse>;
73
+ export type ApplicationPreviewReceipt = Static<typeof receiptSchema>;
74
+ export {};
@@ -2,7 +2,14 @@ import { Type } from "typebox";
2
2
  import type { HttpTransport } from "../transports/http";
3
3
  import { type ToolServer } from "../tool-server";
4
4
  export declare const APPLICATION_TOOL_SCHEMA: {
5
- action: Type.TEnum<("list_releases" | "get_release" | "upload_release" | "activate_release" | "logs" | "get_runtime" | "get_deployment_evidence" | "get_configuration" | "put_configuration" | "reconcile_activation" | "retire_activation")[]>;
5
+ preview_id: Type.TOptional<Type.TString>;
6
+ branch_ref: Type.TOptional<Type.TString>;
7
+ branch_name: Type.TOptional<Type.TString>;
8
+ data_mode: Type.TOptional<Type.TEnum<("schema_only" | "full_clone")[]>>;
9
+ ttl_seconds: Type.TOptional<Type.TInteger>;
10
+ wait: Type.TOptional<Type.TBoolean>;
11
+ timeout_seconds: Type.TOptional<Type.TInteger>;
12
+ action: Type.TEnum<("list_releases" | "get_release" | "upload_release" | "activate_release" | "logs" | "get_runtime" | "get_deployment_evidence" | "get_configuration" | "get_preview_plan" | "put_configuration" | "reconcile_activation" | "retire_activation" | "create_preview" | "list_previews" | "get_preview" | "cleanup_preview")[]>;
6
13
  ref: Type.TString;
7
14
  id: Type.TString;
8
15
  environment_id: Type.TOptional<Type.TString>;
@@ -1,6 +1,7 @@
1
1
  import type { HttpTransport } from "../transports/http";
2
2
  import { type ToolServer } from "../tool-server";
3
- export declare function registerBranchTools(server: ToolServer, http: HttpTransport, options?: {
3
+ export type BranchHttpTransport = Pick<HttpTransport, "get" | "post" | "delete">;
4
+ export declare function registerBranchTools(server: ToolServer, http: BranchHttpTransport, options?: {
4
5
  projectRef?: string;
5
6
  readOnly?: boolean;
6
7
  }): void;
@@ -18,6 +18,10 @@ export interface HttpResult<T = unknown> {
18
18
  responseReadError?: true;
19
19
  }
20
20
  export interface HttpGetOptions {
21
+ timeoutMs?: number;
22
+ retry?: boolean;
23
+ /** Complete header/body budget for a bounded JSON GET with retries disabled. */
24
+ totalTimeoutMs?: number;
21
25
  maxResponseBytes?: number;
22
26
  maxJsonBytes?: number;
23
27
  responseTimeoutMs?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.77.0",
3
+ "version": "0.78.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",