@supacloud/cli 0.88.0 → 0.90.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.
package/README.md CHANGED
@@ -284,9 +284,13 @@ supacloud --env production deploy --confirm-production production-ref
284
284
  Test and production use the same workflow. Production safety is enforced by the
285
285
  CLI confirmation gate; immutable releases, configuration revisions, backups,
286
286
  readiness, CAS activation, rollback artifacts and uncertain-result
287
- reconciliation belong to SupaCloud. The application layer only supplies its
288
- build output and configuration file; it does not implement backup, activation
289
- or downgrade orchestration.
287
+ reconciliation belong to SupaCloud. `SUPACLOUD_APPLICATION_CONFIGURATION_PATH`
288
+ and `SUPACLOUD_APPLICATION_CONFIGURATION_ID` are optional pins; when neither
289
+ is supplied, SupaCloud reuses the current configuration head for the selected
290
+ environment. A first deployment still requires a configuration revision to
291
+ exist on the platform. The application layer only supplies its build output and
292
+ optional configuration file; it does not implement backup, activation or
293
+ downgrade orchestration.
290
294
 
291
295
  CLI requests (including `status`) default to `SUPACLOUD_TLS_VERIFY=false`, skipping HTTPS certificate verification so that
292
296
  air-gapped installations using private or self-signed certificates work
@@ -629,44 +633,25 @@ query parameters and accepts only its authoritative JSON boolean `false`
629
633
  read-back. The fixture binding comes from the validated disable receipt before
630
634
  reporting success. Management endpoint projection is checked before and after.
631
635
 
632
- ### Release scope rebinding and inspection
636
+ ### Internal release scope controls
633
637
 
634
- Release scopes bind deployment manifests (functions, migrations, web assets) to
635
- an exact 40-character base commit SHA. When upstream `main` advances with
636
- unrelated fixes, `release scope_rebind` rebinds release manifests to the updated
637
- base commit without manual JSON editing or risk of dirtying non-manifest fields.
638
+ Scope files and manual release-controller operations are internal compatibility
639
+ surfaces. They are not part of the normal customer deployment path. Use the
640
+ project command instead:
638
641
 
639
- ```bash
640
- # Rebind a single scope manifest to latest git main (or origin/main)
641
- supacloud release scope_rebind --file supacloud/fa/release-scopes/20260907-intake-p1-production.json
642
-
643
- # Rebind multiple files (e.g. production and staging together)
644
- supacloud release scope_rebind --files "20260907-intake-p1-production.json,20260907-intake-p1-staging.json"
645
-
646
- # Rebind all scopes matching a task ID or filename prefix in standard directories
647
- supacloud release scope_rebind --task 20260907-intake-p1
648
-
649
- # Specify an explicit base commit SHA or git ref
650
- supacloud release scope_rebind --task 20260907-intake-p1 --base_commit origin/main
651
-
652
- # Preview changes without modifying files
653
- supacloud release scope_rebind --task 20260907-intake-p1 --dry_run
654
-
655
- # Inspect scope status, SHA-256 hash, and alignment with git HEAD / origin/main
656
- supacloud release scope_inspect --file 20260907-intake-p1-production.json
657
-
658
- # Scaffold a new canonical release scope
659
- supacloud release scope_create \
660
- --file supacloud/fa/release-scopes/20260907-intake-p1.json \
661
- --task FA-INTAKE-P1 \
662
- --ref abc123 \
663
- --functions "fa-api,fa-worker" \
664
- --migrations "20260907120000_init"
642
+ ```sh
643
+ supacloud --env test deploy
644
+ supacloud --env production deploy --confirm-production PROJECT_REF
665
645
  ```
666
646
 
667
- Scope actions run locally without requiring Management API credentials. Files
668
- are formatted canonically with 2 spaces and sorted keys, and the authoritative
669
- `scope_sha256` checksum is recalculated automatically.
647
+ FA deployment policy and the boundary between platform-owned operations and
648
+ FA-owned business features are documented in
649
+ [`docs/fa-deployment-capability-boundary.md`](../../docs/fa-deployment-capability-boundary.md).
650
+
651
+ Scope creation, rebinding, inspection and hash calculation are intentionally
652
+ undocumented here. They remain available only for internal compatibility,
653
+ audit and historical receipt migration. Customers do not need a scope file
654
+ before running the one-command deployment flow above.
670
655
 
671
656
  The legacy `.env` fallback is unclassified and therefore does not enable the
672
657
  production confirmation gate. Production automation must select a `prod` or
package/dist/index.js CHANGED
@@ -8713,6 +8713,8 @@ function decodedSchema(inputSchema, outputSchema, decode, options) {
8713
8713
  return result;
8714
8714
  }
8715
8715
  function schemaEnumValues(schema) {
8716
+ if (!schema)
8717
+ return [];
8716
8718
  const view = schema;
8717
8719
  if (Array.isArray(view.enum)) {
8718
8720
  return view.enum.filter((value) => typeof value === "string" || typeof value === "number").map((value) => String(value));
@@ -8818,7 +8820,7 @@ async function runCli(cliTools, args, options = {}) {
8818
8820
  `);
8819
8821
  const getSchemaShape = (schema) => schemaProperties(schema);
8820
8822
  const getEnumOptions = (field) => schemaEnumValues(field);
8821
- const getDescription = (field) => schemaDescription(field);
8823
+ const getDescription = (field) => field ? schemaDescription(field) : "";
8822
8824
  const formatToolHelp = (toolName, tool) => {
8823
8825
  const shape = getSchemaShape(tool.schema);
8824
8826
  const actionField = shape.action;
@@ -9167,7 +9169,7 @@ var ACTION_POLICY = {
9167
9169
  deploy: { write: ["deploy"] },
9168
9170
  applications: {
9169
9171
  read: ["list_releases", "get_release", "get_runtime", "get_deployment_evidence", "get_configuration", "get_release_transfer_plan", "get_promotion_plan", "get_promotion_status", "get_rollback_snapshot", "get_history", "get_deploy_plan", "logs", "get_preview_plan", "list_previews", "get_preview"],
9170
- write: ["upload_release", "put_configuration", "release_application", "transfer_release", "promote_application", "reconcile_promotion", "create_preview", "reconcile_preview", "cleanup_preview", "activate_release", "deploy_release", "rollback_release", "reconcile_activation", "retire_activation"]
9172
+ write: ["upload_release", "put_configuration", "deploy_application", "transfer_release", "promote_application", "reconcile_promotion", "create_preview", "reconcile_preview", "cleanup_preview", "activate_release", "deploy_release", "rollback_release", "reconcile_activation", "retire_activation"]
9171
9173
  },
9172
9174
  project: {
9173
9175
  read: ["get", "endpoints", "health", "logs", "api_keys", "settings", "tasks", "task_detail", "task_stats", "dlq", "background_settings"],
@@ -10493,34 +10495,67 @@ function parseApplicationDeployPlan(value) {
10493
10495
  throw new Error("Invalid application deploy plan action");
10494
10496
  return value;
10495
10497
  }
10496
- // node_modules/@supacloud/delivery/src/application-release-operation.ts
10498
+ // node_modules/@supacloud/delivery/src/application-deploy-operation.ts
10497
10499
  var closed3 = { additionalProperties: false };
10500
+ var sha256 = ApplicationReleaseIdSchema;
10498
10501
  var nullableActivation = Union([ApplicationActivationIdSchema, Null()]);
10499
- var ApplicationReleaseRequestSchema = _Object_({
10500
- release_id: ApplicationReleaseIdSchema,
10502
+ var ApplicationDeployWebRequestSchema = _Object_({
10503
+ deployment_id: String2({ pattern: "^[A-Za-z0-9_-]{1,128}$" }),
10504
+ release_id: sha256,
10505
+ expected_active_release_id: String2({ minLength: 1 }),
10506
+ expected_activation_id: String2({ minLength: 1 }),
10507
+ mutation_id: ApplicationActivationIdSchema
10508
+ }, closed3);
10509
+ var ApplicationDeployRequestSchema = _Object_({
10510
+ mutation_id: ApplicationActivationIdSchema,
10511
+ release_id: sha256,
10501
10512
  configuration_id: Optional(ApplicationConfigurationIdSchema),
10502
10513
  configuration_write: Optional(ApplicationConfigurationWriteSchema),
10503
- activation_id: Optional(ApplicationActivationIdSchema),
10504
- expected_activation_id: Optional(nullableActivation),
10505
- expected_plan_sha256: Optional(ApplicationReleaseIdSchema)
10514
+ expected_plan_sha256: Optional(sha256),
10515
+ approved_migration_digest: Optional(sha256),
10516
+ web: Optional(ApplicationDeployWebRequestSchema)
10506
10517
  }, closed3);
10507
- var ApplicationReleaseResultSchema = _Object_({
10508
- schema: Literal("supacloud.application-release.v1"),
10518
+ var ApplicationBackendReceiptSchema = _Object_({
10519
+ schema: Literal("supacloud.application-backend-receipt.v1"),
10509
10520
  project_ref: String2({ pattern: "^[a-z0-9-]{1,20}$" }),
10510
10521
  application_id: ApplicationIdSchema,
10511
10522
  environment_id: ApplicationIdSchema,
10512
- release_id: ApplicationReleaseIdSchema,
10523
+ release_id: sha256,
10524
+ manifest_sha256: sha256,
10525
+ mutation_id: ApplicationActivationIdSchema,
10526
+ plan_sha256: sha256,
10527
+ backup_id: Union([String2(), Null()]),
10528
+ before_ledger_digest: sha256,
10529
+ after_ledger_digest: sha256,
10530
+ applied_versions: _Array_(String2({ pattern: "^\\d{1,20}$" }), { uniqueItems: true }),
10531
+ schema_reloaded: Boolean2(),
10532
+ postgrest_healthy: Boolean2()
10533
+ }, closed3);
10534
+ var ApplicationDeployResultSchema = _Object_({
10535
+ schema: Literal("supacloud.application-deploy.v1"),
10536
+ project_ref: String2({ pattern: "^[a-z0-9-]{1,20}$" }),
10537
+ application_id: ApplicationIdSchema,
10538
+ environment_id: ApplicationIdSchema,
10539
+ mutation_id: ApplicationActivationIdSchema,
10540
+ release_id: sha256,
10513
10541
  configuration_id: ApplicationConfigurationIdSchema,
10514
10542
  activation_id: ApplicationActivationIdSchema,
10515
10543
  expected_activation_id: nullableActivation,
10516
- plan_sha256: ApplicationReleaseIdSchema,
10544
+ plan_sha256: sha256,
10517
10545
  action: Union([Literal("activate"), Literal("no-op")]),
10518
10546
  replayed: Boolean2(),
10519
- execution_performed: Boolean2()
10547
+ execution_performed: Boolean2(),
10548
+ backend_receipt: ApplicationBackendReceiptSchema,
10549
+ web: Optional(_Object_({
10550
+ deployment_id: String2({ pattern: "^[A-Za-z0-9_-]{1,128}$" }),
10551
+ release_id: sha256,
10552
+ activation_id: ApplicationActivationIdSchema,
10553
+ replayed: Boolean2()
10554
+ }, closed3))
10520
10555
  }, closed3);
10521
- function parseApplicationReleaseResult(value) {
10522
- if (!Check2(ApplicationReleaseResultSchema, value)) {
10523
- throw new Error("Invalid application release result");
10556
+ function parseApplicationDeployResult(value) {
10557
+ if (!Check2(ApplicationDeployResultSchema, value)) {
10558
+ throw new Error("Invalid application deploy result");
10524
10559
  }
10525
10560
  return structuredClone(value);
10526
10561
  }
@@ -10721,7 +10756,7 @@ function parseApplicationReleaseTransferResult(candidate) {
10721
10756
  }
10722
10757
  // node_modules/@supacloud/delivery/src/deployment-evidence.ts
10723
10758
  var closed5 = { additionalProperties: false };
10724
- var sha256 = String2({ pattern: "^[a-f0-9]{64}$" });
10759
+ var sha2562 = String2({ pattern: "^[a-f0-9]{64}$" });
10725
10760
  var isoDate = String2({ minLength: 20, maxLength: 40 });
10726
10761
  var status = Union([
10727
10762
  Literal("confirmed"),
@@ -10741,7 +10776,7 @@ var DatabaseProviderSchema = _Object_({
10741
10776
  topology: Union([Literal("single-node"), Literal("distributed")]),
10742
10777
  migration: _Object_({
10743
10778
  status,
10744
- inventory_sha256: Union([sha256, Null()]),
10779
+ inventory_sha256: Union([sha2562, Null()]),
10745
10780
  compatibility: Union([
10746
10781
  Literal("verified"),
10747
10782
  Literal("not-proven"),
@@ -10789,9 +10824,9 @@ var DeploymentEvidenceSchema = _Object_({
10789
10824
  }, closed5),
10790
10825
  source: _Object_({
10791
10826
  commit_sha: Union([String2({ pattern: "^[0-9a-f]{7,64}$" }), Null()]),
10792
- manifest_sha256: sha256,
10827
+ manifest_sha256: sha2562,
10793
10828
  contract_schema: Union([String2({ minLength: 1, maxLength: 128 }), Null()]),
10794
- environment_binding_version: Union([sha256, Null()])
10829
+ environment_binding_version: Union([sha2562, Null()])
10795
10830
  }, closed5),
10796
10831
  database: DatabaseProviderSchema,
10797
10832
  components: _Array_(DeploymentComponentEvidenceSchema, { minItems: 1, maxItems: 32 }),
@@ -10847,9 +10882,9 @@ function parseDeploymentEvidence(value) {
10847
10882
  // node_modules/@supacloud/delivery/src/application-promotion.ts
10848
10883
  var closed6 = { additionalProperties: false };
10849
10884
  var projectRef2 = String2({ pattern: "^[a-z0-9-]{1,20}$" });
10850
- var sha2562 = ApplicationReleaseIdSchema;
10885
+ var sha2563 = ApplicationReleaseIdSchema;
10851
10886
  var nullableActivation2 = Union([ApplicationActivationIdSchema, Null()]);
10852
- var nullableDigest = Union([sha2562, Null()]);
10887
+ var nullableDigest = Union([sha2563, Null()]);
10853
10888
  var observation = {
10854
10889
  activation_id: nullableActivation2,
10855
10890
  receipt_confirmed: Boolean2(),
@@ -10875,24 +10910,24 @@ var ApplicationPromotionPlanSchema = _Object_({
10875
10910
  project_ref: projectRef2,
10876
10911
  application_id: ApplicationIdSchema,
10877
10912
  environment_id: ApplicationIdSchema,
10878
- manifest_sha256: sha2562,
10879
- plan_sha256: sha2562,
10913
+ manifest_sha256: sha2563,
10914
+ plan_sha256: sha2563,
10880
10915
  source: _Object_({
10881
10916
  project_ref: projectRef2,
10882
10917
  environment_id: ApplicationIdSchema,
10883
- release_id: sha2562,
10918
+ release_id: sha2563,
10884
10919
  ...observation
10885
10920
  }, closed6),
10886
10921
  target: _Object_({
10887
- candidate_release_id: sha2562,
10888
- current_release_id: Union([sha2562, Null()]),
10922
+ candidate_release_id: sha2563,
10923
+ current_release_id: Union([sha2563, Null()]),
10889
10924
  artifact_action: Union([Literal("materialize"), Literal("reuse")]),
10890
10925
  configuration_id: Union([ApplicationConfigurationIdSchema, Null()]),
10891
10926
  configuration: Union([ApplicationConfigurationViewSchema, Null()]),
10892
10927
  ...observation
10893
10928
  }, closed6),
10894
10929
  migrations: _Object_({
10895
- ledger_digest: sha2562,
10930
+ ledger_digest: sha2563,
10896
10931
  ledger_compatible: Boolean2(),
10897
10932
  project_migrations_applied: Boolean2(),
10898
10933
  pending_versions: _Array_(String2({ pattern: "^\\d{1,20}$" }), { maxItems: 4096, uniqueItems: true }),
@@ -16857,7 +16892,8 @@ var ApplicationActivationRetirementResultSchema2 = ApplicationActivationRetireme
16857
16892
  var ApplicationRollbackSnapshotSchema2 = ApplicationRollbackSnapshotSchema;
16858
16893
  var ApplicationActivationHistoryCursorSchema2 = ApplicationActivationHistoryCursorSchema;
16859
16894
  var ApplicationPromotionPlanSchema2 = ApplicationPromotionPlanSchema;
16860
- var ApplicationReleaseRequestSchema2 = ApplicationReleaseRequestSchema;
16895
+ var ApplicationDeployRequestSchema2 = ApplicationDeployRequestSchema;
16896
+ var ApplicationDeployWebRequestSchema2 = ApplicationDeployWebRequestSchema;
16861
16897
 
16862
16898
  // src/shared/mutation-protocol.ts
16863
16899
  import { createHash as createHash6 } from "node:crypto";
@@ -17645,7 +17681,7 @@ var APPLICATION_TOOL_SCHEMA = {
17645
17681
  "put_configuration",
17646
17682
  "get_deploy_plan",
17647
17683
  "deploy_release",
17648
- "release_application",
17684
+ "deploy_application",
17649
17685
  "get_release_transfer_plan",
17650
17686
  "transfer_release",
17651
17687
  "get_promotion_plan",
@@ -17673,14 +17709,15 @@ var APPLICATION_TOOL_SCHEMA = {
17673
17709
  activation_id: optional(ApplicationActivationIdSchema2, "[deploy_release/activate_release/reconcile_activation/retire_activation/rollback_release/get_history] Stable activation ID; generated for automatic deployment/rollback if omitted"),
17674
17710
  expected_activation_id: optional(Union([ApplicationActivationIdSchema2, Null(), Literal("absent")]), "[deploy_release/activate_release/rollback_release] Current activation ID, or absent for first activation; selected from verified plan for automatic deployment"),
17675
17711
  configuration_path: optional(String2(), "[put_configuration] Local configuration write JSON including revision and expected revision"),
17676
- configuration_write: optional(Unknown(), "[release_application] Parsed immutable configuration write"),
17712
+ configuration_write: optional(Unknown(), "[deploy_application] Parsed immutable configuration write"),
17677
17713
  manifest_path: optional(String2(), "[upload_release] Local delivery.manifest.json"),
17714
+ web: optional(ApplicationDeployWebRequestSchema2, "[deploy_application] Immutable staged Web release binding"),
17678
17715
  release_id: optional(ApplicationReleaseIdSchema2, "[get_release/get_deploy_plan/deploy_release/activate_release/reconcile_activation/rollback_release] Immutable application release ID; platform selects previous for default rollback"),
17679
17716
  source_ref: optional(String2({ pattern: "^[A-Za-z0-9_-]{1,20}$" }), "[get_release_transfer_plan/transfer_release] Source project ref"),
17680
17717
  source_release_id: optional(ApplicationReleaseIdSchema2, "[get_release_transfer_plan/transfer_release] Source immutable release ID"),
17681
17718
  source_environment_id: optional(ApplicationIdSchema2, "[get_promotion_plan] Source environment"),
17682
- mutation_id: optional(ApplicationActivationIdSchema2, "[promote_application/get_promotion_status/reconcile_promotion] Stable promotion mutation ID"),
17683
- approved_migration_digest: optional(ApplicationReleaseIdSchema2, "[promote_application] Exact reviewed migration execution-plan digest"),
17719
+ mutation_id: optional(ApplicationActivationIdSchema2, "[deploy_application/promote_application/get_promotion_status/reconcile_promotion] Stable mutation ID"),
17720
+ approved_migration_digest: optional(ApplicationReleaseIdSchema2, "[deploy_application/promote_application] Exact reviewed migration execution-plan digest"),
17684
17721
  plan: optional(ApplicationPromotionPlanSchema2, "[promote_application] Immutable plan returned by get_promotion_plan"),
17685
17722
  cursor: optional(Union([ApplicationReleaseIdSchema2, ApplicationActivationHistoryCursorSchema2]), "[list_releases/get_history] Returned page cursor"),
17686
17723
  limit: optional(Integer({ minimum: 1, maximum: 100 }), "[list_releases/get_history] Page size, default 50/20"),
@@ -18033,57 +18070,50 @@ function registerApplicationTools(server, http) {
18033
18070
  const observed = await readDeployPlan(http, args, project);
18034
18071
  return observed.failure ?? releaseControlSuccess(operation, { ...observed.identity, plan: observed.plan });
18035
18072
  }
18036
- if (action === "release_application") {
18073
+ if (action === "deploy_application") {
18037
18074
  const environmentId = text(args, "environment_id");
18038
18075
  const releaseId = text(args, "release_id");
18039
- if (!Check2(ApplicationIdSchema2, environmentId) || !Check2(ApplicationReleaseIdSchema2, releaseId)) {
18040
- throw new Error("Invalid application release identity");
18076
+ const mutationId = text(args, "mutation_id");
18077
+ if (!Check2(ApplicationIdSchema2, environmentId) || !Check2(ApplicationReleaseIdSchema2, releaseId) || !Check2(ApplicationActivationIdSchema2, mutationId)) {
18078
+ throw new Error("Invalid application deploy identity");
18041
18079
  }
18042
18080
  const configurationId = args["configuration_id"];
18043
- const activationId = args["activation_id"];
18044
- const expectedActivationId = args["expected_activation_id"];
18045
18081
  if (configurationId !== undefined && !Check2(ApplicationConfigurationIdSchema2, configurationId)) {
18046
18082
  throw new Error("Invalid application configuration ID");
18047
18083
  }
18048
- if (activationId !== undefined && !Check2(ApplicationActivationIdSchema2, activationId)) {
18049
- throw new Error("Invalid application activation ID");
18050
- }
18051
- if (expectedActivationId !== undefined && expectedActivationId !== null && expectedActivationId !== "absent" && !Check2(ApplicationActivationIdSchema2, expectedActivationId)) {
18052
- throw new Error("Invalid expected activation ID");
18053
- }
18054
18084
  const configurationWrite = args["configuration_write"];
18055
18085
  const parsedConfigurationWrite = configurationWrite === undefined ? undefined : parseApplicationConfigurationWrite(configurationWrite);
18056
18086
  const requestedConfigurationId = configurationId ?? parsedConfigurationWrite?.configuration_id;
18057
18087
  const body = {
18088
+ mutation_id: mutationId,
18058
18089
  release_id: releaseId,
18059
18090
  ...configurationId === undefined ? {} : { configuration_id: configurationId },
18060
18091
  ...configurationWrite === undefined ? {} : { configuration_write: configurationWrite },
18061
- ...activationId === undefined ? {} : { activation_id: activationId },
18062
- ...expectedActivationId === undefined ? {} : {
18063
- expected_activation_id: expectedActivationId === "absent" ? null : expectedActivationId
18064
- }
18092
+ ...typeof args["approved_migration_digest"] === "string" ? { approved_migration_digest: args["approved_migration_digest"] } : {},
18093
+ ...args["web"] === undefined ? {} : { web: args["web"] }
18065
18094
  };
18066
- if (!Check2(ApplicationReleaseRequestSchema2, body)) {
18067
- throw new Error("Invalid application release request");
18095
+ if (!Check2(ApplicationDeployRequestSchema2, body)) {
18096
+ throw new Error("Invalid application deploy request");
18068
18097
  }
18069
- const result = await http.post(`/v1/projects/${project}/applications/${encodeURIComponent(id)}/environments/${encodeURIComponent(environmentId)}/release`, body, { timeoutMs: 120000, maxJsonBytes: 65536, responseTimeoutMs: 30000 });
18098
+ const result = await http.post(`/v1/projects/${project}/applications/${encodeURIComponent(id)}/environments/${encodeURIComponent(environmentId)}/deploy`, body, { timeoutMs: 120000, maxJsonBytes: 131072, responseTimeoutMs: 30000 });
18070
18099
  const identity = {
18071
18100
  project_ref: ref,
18072
18101
  application_id: id,
18073
18102
  environment_id: environmentId,
18074
18103
  release_id: releaseId,
18075
- ...configurationId === undefined ? {} : { configuration_id: configurationId },
18076
- ...activationId === undefined ? {} : { activation_id: activationId }
18104
+ mutation_id: mutationId,
18105
+ ...requestedConfigurationId === undefined ? {} : { configuration_id: requestedConfigurationId }
18077
18106
  };
18078
18107
  if (!result.ok)
18079
18108
  return releaseControlMutationFailure(operation, result, identity);
18080
18109
  try {
18081
18110
  if (result.status !== 200)
18082
18111
  throw new Error;
18083
- const release = parseApplicationReleaseResult(result.data);
18084
- if (release.project_ref !== ref || release.application_id !== id || release.environment_id !== environmentId || release.release_id !== releaseId || requestedConfigurationId !== undefined && release.configuration_id !== requestedConfigurationId || activationId !== undefined && release.activation_id !== activationId)
18112
+ const deploy = parseApplicationDeployResult(result.data);
18113
+ if (deploy.project_ref !== ref || deploy.application_id !== id || deploy.environment_id !== environmentId || deploy.release_id !== releaseId || deploy.mutation_id !== mutationId || requestedConfigurationId !== undefined && deploy.configuration_id !== requestedConfigurationId) {
18085
18114
  throw new Error;
18086
- return releaseControlSuccess(operation, release);
18115
+ }
18116
+ return releaseControlSuccess(operation, deploy);
18087
18117
  } catch {
18088
18118
  return releaseControlFailure(operation, "OUTCOME_UNKNOWN", result.status, identity);
18089
18119
  }
@@ -24538,6 +24568,7 @@ var REMOTE_APP_DESCRIPTIONS = {
24538
24568
  source_environment_id: "[promote-plan/diff/promote] Source environment ID",
24539
24569
  mutation_id: "[promote-status/promote-reconcile] Stable promotion mutation ID",
24540
24570
  approved_migration_digest: "[promote] Exact reviewed migration execution-plan digest",
24571
+ web: "[release] Immutable staged Web release binding prepared by the deploy profile",
24541
24572
  branch_ref: "[preview-plan] Preview branch identity",
24542
24573
  branch_name: "[preview/preview-create] Human-readable preview branch name",
24543
24574
  data_mode: "[preview-plan/preview/preview-create] schema_only or full_clone",
@@ -25506,9 +25537,6 @@ async function runAppRelease(request, options, delegate) {
25506
25537
  if (request.configuration_path !== undefined && request.configuration_id !== undefined) {
25507
25538
  throw new Error("app release accepts either --configuration_path or --configuration_id, not both");
25508
25539
  }
25509
- if (request.configuration_path === undefined && request.configuration_id === undefined) {
25510
- throw new Error("app release requires --configuration_path or --configuration_id");
25511
- }
25512
25540
  if (request.activation_id !== undefined || request.expected_activation_id !== undefined) {
25513
25541
  throw new Error("app release generates one activation identity; use app deploy for an exact retry");
25514
25542
  }
@@ -25562,25 +25590,36 @@ async function runAppRelease(request, options, delegate) {
25562
25590
  configurationWrite = parseApplicationConfigurationWrite(await Bun.file(configurationPath).json());
25563
25591
  configurationId = configurationWrite.configuration_id;
25564
25592
  } catch {
25565
- return releasePhaseFailure("configure", releaseControlFailure("applications.release_application", "INVALID_RESPONSE", null, { project_ref: ref, application_id: id, environment_id: environmentId }), completed);
25593
+ return releasePhaseFailure("configure", releaseControlFailure("applications.deploy_application", "INVALID_RESPONSE", null, { project_ref: ref, application_id: id, environment_id: environmentId }), completed);
25566
25594
  }
25567
25595
  completed.configure = { delegated: true, configuration_id: configurationId };
25568
25596
  } else {
25569
- completed.configure = { delegated: true, configuration_id: configurationId };
25597
+ completed.configure = {
25598
+ delegated: false,
25599
+ resolved_by: "platform",
25600
+ ...configurationId === undefined ? {} : { configuration_id: configurationId }
25601
+ };
25570
25602
  }
25571
25603
  const deployArgs = {
25572
- action: "release_application",
25604
+ action: "deploy_application",
25573
25605
  ref,
25574
25606
  id,
25575
25607
  environment_id: environmentId,
25576
25608
  release_id: releaseId,
25577
- configuration_id: configurationId,
25578
- activation_id: activationId,
25579
- ...configurationWrite === undefined ? {} : { configuration_write: configurationWrite }
25609
+ mutation_id: activationId,
25610
+ ...configurationId === undefined ? {} : { configuration_id: configurationId },
25611
+ ...configurationWrite === undefined ? {} : { configuration_write: configurationWrite },
25612
+ ...request.web === undefined ? {} : { web: request.web }
25580
25613
  };
25581
25614
  validateToolArguments(APPLICATION_TOOL_SCHEMA, deployArgs);
25582
25615
  const deployed = await delegate(deployArgs);
25583
25616
  const deployReceipt = resultObject(deployed);
25617
+ const resolvedConfigurationId = typeof deployReceipt?.configuration_id === "string" ? deployReceipt.configuration_id : undefined;
25618
+ if (resolvedConfigurationId !== undefined) {
25619
+ configurationId = resolvedConfigurationId;
25620
+ const configure = completed.configure;
25621
+ completed.configure = configure && typeof configure === "object" && !Array.isArray(configure) ? { ...configure, configuration_id: resolvedConfigurationId } : { delegated: false, resolved_by: "platform", configuration_id: resolvedConfigurationId };
25622
+ }
25584
25623
  if (deployed.isError || !deployReceipt?.ok) {
25585
25624
  return releasePhaseFailure("deploy", deployed, completed);
25586
25625
  }
@@ -25593,13 +25632,14 @@ async function runAppRelease(request, options, delegate) {
25593
25632
  application_id: id,
25594
25633
  environment_id: environmentId,
25595
25634
  release_id: releaseId,
25596
- configuration_id: configurationId,
25635
+ ...configurationId === undefined ? {} : { configuration_id: configurationId },
25597
25636
  manifest_path: manifestPath,
25598
25637
  build: buildReceipt ? completed.build : { skipped: true, manifest_path: manifestPath },
25599
25638
  phases: completed,
25600
25639
  no_op: deployReceipt.action === "no-op" || deployReceipt.no_op === true,
25601
25640
  execution_performed: deployReceipt.execution_performed === true,
25602
- ...typeof deployReceipt.activation_id === "string" ? { activation_id: deployReceipt.activation_id } : {}
25641
+ ...typeof deployReceipt.activation_id === "string" ? { activation_id: deployReceipt.activation_id } : {},
25642
+ ...deployReceipt.web === undefined ? {} : { web: deployReceipt.web }
25603
25643
  };
25604
25644
  return request.format === "json" || request.json === true ? textResult(JSON.stringify(result, null, 2)) : textResult([
25605
25645
  `${ref}/${id}/${environmentId}: ${result.no_op ? "no-op" : "released"}`,
@@ -27205,6 +27245,7 @@ import { createHash as createHash8 } from "node:crypto";
27205
27245
  import { existsSync as existsSync13, mkdirSync as mkdirSync4, readdirSync as readdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
27206
27246
  import { dirname as dirname10, join as join16, relative as relative9, resolve as resolve21 } from "node:path";
27207
27247
  var SAFE_PROJECT_REF = /^[A-Za-z0-9_-]{1,64}$/;
27248
+ var SAFE_ENVIRONMENT_ID = /^[A-Za-z0-9_-]{1,64}$/;
27208
27249
  var BACKUP_ID = /^logical-full_[A-Za-z0-9_-]{1,64}_[a-f0-9]{32}$/;
27209
27250
  var SHA256 = /^[a-f0-9]{64}$/;
27210
27251
  var SAFE_DATABASE = /^[^\u0000-\u001f\u007f]{1,128}$/;
@@ -27445,6 +27486,27 @@ function genericScopedReleaseScopeJson(scope) {
27445
27486
  function genericScopedReleaseScopeSha256(scope) {
27446
27487
  return createHash8("sha256").update(genericScopedReleaseScopeJson(scope)).digest("hex");
27447
27488
  }
27489
+ function scopeEnvironmentId(scope, action) {
27490
+ const environmentId = isRecord6(scope) ? scope.targetEnvironmentId : undefined;
27491
+ if (typeof environmentId !== "string" || !SAFE_ENVIRONMENT_ID.test(environmentId)) {
27492
+ throw new Error(`Scope for ${action} must be regenerated with a concrete targetEnvironmentId`);
27493
+ }
27494
+ return environmentId;
27495
+ }
27496
+ function requestedScopeEnvironment(args, action) {
27497
+ const environmentId = args.environment_id;
27498
+ if (typeof environmentId !== "string" || !SAFE_ENVIRONMENT_ID.test(environmentId)) {
27499
+ throw new Error(`'environment_id' is required for ${action} and must identify one deployment environment`);
27500
+ }
27501
+ return environmentId;
27502
+ }
27503
+ function assertScopeEnvironment(scope, requestedEnvironmentId, action) {
27504
+ const targetEnvironmentId = scopeEnvironmentId(scope, action);
27505
+ if (targetEnvironmentId !== requestedEnvironmentId) {
27506
+ throw new Error(`Scope target environment '${targetEnvironmentId}' does not match requested environment '${requestedEnvironmentId}'`);
27507
+ }
27508
+ return targetEnvironmentId;
27509
+ }
27448
27510
  function runGitRevParse(cwd, args) {
27449
27511
  const res = Bun.spawnSync(["git", ...args], { cwd });
27450
27512
  if (res.exitCode !== 0 || !res.stdout)
@@ -27571,10 +27633,10 @@ function collectScopeFiles(cwd, file, files, dir, task) {
27571
27633
  function handleScopeRebind(args) {
27572
27634
  const cwd = typeof args.cwd === "string" && args.cwd.trim() ? resolve21(args.cwd.trim()) : process.cwd();
27573
27635
  const targetBaseCommit = resolveGitBaseCommit(cwd, args.base_commit);
27636
+ const requestedEnvironmentId = requestedScopeEnvironment(args, "scope_rebind");
27574
27637
  const scopeFiles = collectScopeFiles(cwd, args.file, args.files, args.dir, args.task);
27575
27638
  const dryRun = args.dry_run === true;
27576
- const scopes = [];
27577
- for (const filePath of scopeFiles) {
27639
+ const parsedScopes = scopeFiles.map((filePath) => {
27578
27640
  let content;
27579
27641
  try {
27580
27642
  content = readFileSync10(filePath, "utf8");
@@ -27587,15 +27649,20 @@ function handleScopeRebind(args) {
27587
27649
  } catch {
27588
27650
  throw new Error(`Scope file '${filePath}' is not valid JSON`);
27589
27651
  }
27590
- if (!isRecord6(parsed) || typeof parsed.baseCommit !== "string" || !/^[0-9a-f]{40}$/.test(parsed.baseCommit)) {
27652
+ const targetEnvironmentId = assertScopeEnvironment(parsed, requestedEnvironmentId, "scope_rebind");
27653
+ const previousBaseCommit = parsed.baseCommit;
27654
+ if (typeof previousBaseCommit !== "string" || !/^[0-9a-f]{40}$/.test(previousBaseCommit)) {
27591
27655
  throw new Error(`Scope file '${filePath}' has missing or invalid baseCommit`);
27592
27656
  }
27657
+ return { filePath, parsed, targetEnvironmentId };
27658
+ });
27659
+ const scopes = [];
27660
+ for (const { filePath, parsed, targetEnvironmentId } of parsedScopes) {
27593
27661
  const previousBaseCommit = parsed.baseCommit;
27594
27662
  const updated = previousBaseCommit !== targetBaseCommit;
27595
27663
  if (updated) {
27596
27664
  parsed.baseCommit = targetBaseCommit;
27597
27665
  }
27598
- const canonicalJson = genericScopedReleaseScopeJson(parsed);
27599
27666
  const scopeSha256 = genericScopedReleaseScopeSha256(parsed);
27600
27667
  if (updated && !dryRun) {
27601
27668
  const formatted = JSON.stringify(canonicalValue(parsed), null, 2) + `
@@ -27608,6 +27675,7 @@ function handleScopeRebind(args) {
27608
27675
  target_project_ref: typeof parsed.targetProjectRef === "string" ? parsed.targetProjectRef : "",
27609
27676
  previous_base_commit: previousBaseCommit,
27610
27677
  new_base_commit: targetBaseCommit,
27678
+ target_environment_id: targetEnvironmentId,
27611
27679
  scope_sha256: scopeSha256,
27612
27680
  updated
27613
27681
  });
@@ -27622,6 +27690,7 @@ function handleScopeRebind(args) {
27622
27690
  }
27623
27691
  function handleScopeInspect(args) {
27624
27692
  const cwd = typeof args.cwd === "string" && args.cwd.trim() ? resolve21(args.cwd.trim()) : process.cwd();
27693
+ const requestedEnvironmentId = requestedScopeEnvironment(args, "scope_inspect");
27625
27694
  if (typeof args.file !== "string" || !args.file.trim()) {
27626
27695
  throw new Error("'file' is required for scope_inspect");
27627
27696
  }
@@ -27651,7 +27720,11 @@ function handleScopeInspect(args) {
27651
27720
  } catch {
27652
27721
  throw new Error(`Scope file '${args.file}' is not valid JSON`);
27653
27722
  }
27654
- if (!isRecord6(parsed) || typeof parsed.baseCommit !== "string" || !/^[0-9a-f]{40}$/.test(parsed.baseCommit)) {
27723
+ if (!isRecord6(parsed)) {
27724
+ throw new Error(`Scope file '${args.file}' must be an object and must be regenerated`);
27725
+ }
27726
+ const targetEnvironmentId = assertScopeEnvironment(parsed, requestedEnvironmentId, "scope_inspect");
27727
+ if (typeof parsed.baseCommit !== "string" || !/^[0-9a-f]{40}$/.test(parsed.baseCommit)) {
27655
27728
  throw new Error(`Scope file '${args.file}' has missing or invalid baseCommit`);
27656
27729
  }
27657
27730
  const scopeSha256 = genericScopedReleaseScopeSha256(parsed);
@@ -27661,6 +27734,7 @@ function handleScopeInspect(args) {
27661
27734
  scope: {
27662
27735
  task_id: parsed.taskId,
27663
27736
  target_project_ref: parsed.targetProjectRef,
27737
+ target_environment_id: targetEnvironmentId,
27664
27738
  base_commit: parsed.baseCommit,
27665
27739
  functions: parsed.functions,
27666
27740
  excluded_functions: parsed.excludedFunctions,
@@ -27698,6 +27772,7 @@ function handleScopeCreate(args, fallbackRef) {
27698
27772
  if (typeof args.task !== "string" || !args.task.trim()) {
27699
27773
  throw new Error("'task' is required for scope_create");
27700
27774
  }
27775
+ const targetEnvironmentId = requestedScopeEnvironment(args, "scope_create");
27701
27776
  const projectRef = typeof args.ref === "string" && args.ref.trim() || fallbackRef;
27702
27777
  if (!projectRef) {
27703
27778
  throw new Error("'ref' is required for scope_create");
@@ -27714,6 +27789,7 @@ function handleScopeCreate(args, fallbackRef) {
27714
27789
  functions: parseCommaList(args.functions),
27715
27790
  migrations: parseCommaList(args.migrations),
27716
27791
  targetProjectRef: projectRef,
27792
+ targetEnvironmentId,
27717
27793
  taskId: String(args.task).trim(),
27718
27794
  web: Boolean(args.web)
27719
27795
  };
@@ -27726,6 +27802,7 @@ function handleScopeCreate(args, fallbackRef) {
27726
27802
  file: relative9(cwd, filePath),
27727
27803
  task_id: scope.taskId,
27728
27804
  target_project_ref: projectRef,
27805
+ target_environment_id: targetEnvironmentId,
27729
27806
  base_commit: targetBaseCommit,
27730
27807
  scope_sha256: scopeSha256,
27731
27808
  scope
@@ -27752,6 +27829,7 @@ function registerReleaseTools(server, http, options = {}) {
27752
27829
  dir: optional(String2(), "[scope_rebind] Directory containing release scope JSON files (defaults to supacloud/fa/release-scopes, release-scopes, or scopes)"),
27753
27830
  task: optional(String2(), "[scope_rebind/scope_create] Task ID or prefix filter for scope files"),
27754
27831
  base_commit: optional(String2(), "[scope_rebind/scope_create] Target 40-character base commit SHA or git ref (defaults to origin/main -> main -> HEAD^ -> HEAD)"),
27832
+ environment_id: optional(String2(), "[scope_inspect/scope_rebind/scope_create] Exact target deployment environment"),
27755
27833
  functions: optional(String2(), "[scope_create] Comma-separated list of functions to deploy"),
27756
27834
  excluded_functions: optional(String2(), "[scope_create] Comma-separated list of functions to exclude"),
27757
27835
  migrations: optional(String2(), "[scope_create] Comma-separated list of migrations to apply"),
@@ -27948,7 +28026,8 @@ function registerReleaseTools(server, http, options = {}) {
27948
28026
  }
27949
28027
 
27950
28028
  // src/shared/tools/deploy-tools.ts
27951
- import { access, lstat as lstat5, mkdtemp as mkdtemp2, opendir, rm as rm3 } from "node:fs/promises";
28029
+ import { access, lstat as lstat5, mkdtemp as mkdtemp2, opendir, readFile as readFile6, rm as rm3, writeFile as writeFile6 } from "node:fs/promises";
28030
+ import { spawn as spawn2 } from "node:child_process";
27952
28031
  import { createHash as createHash9 } from "node:crypto";
27953
28032
  import { tmpdir as tmpdir3 } from "node:os";
27954
28033
  import { basename as basename8, dirname as dirname11, join as join17, relative as relative10, resolve as resolve22, sep as sep5 } from "node:path";
@@ -27998,7 +28077,7 @@ function frontendDeployment(candidate) {
27998
28077
  }
27999
28078
  async function readJson(path) {
28000
28079
  try {
28001
- return await Bun.file(path).json();
28080
+ return JSON.parse(await readFile6(path, "utf8"));
28002
28081
  } catch (error) {
28003
28082
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
28004
28083
  return null;
@@ -28153,6 +28232,33 @@ function selectDeployment(deployments, requestedId, projectName) {
28153
28232
  const ids = deployments.map((deployment) => deployment.id).sort().join(", ");
28154
28233
  throw new Error(deployments.length === 0 ? "No frontend deployments exist for this project" : `Multiple frontend deployments found (${ids}). Set frontend.id in supacloud.json or pass --id`);
28155
28234
  }
28235
+ function selectFrontendTarget(configRoot, config) {
28236
+ if (config.targets === undefined && config.frontend === undefined)
28237
+ return null;
28238
+ const targets = config.targets ? Object.entries(config.targets).filter(([, target]) => target.type === "frontend") : [["frontend", {
28239
+ type: "frontend",
28240
+ root: config.frontend?.root,
28241
+ id: config.frontend?.id,
28242
+ buildCommand: config.frontend?.buildCommand,
28243
+ outputDirectory: config.frontend?.outputDirectory,
28244
+ buildEnv: config.frontend?.buildEnv
28245
+ }]];
28246
+ if (targets.length === 0)
28247
+ return null;
28248
+ if (config.defaultTarget) {
28249
+ const selected = targets.find(([name]) => name === config.defaultTarget);
28250
+ if (selected)
28251
+ return { name: selected[0], config: selected[1], root: targetRoot(configRoot, selected[1], selected[0]) };
28252
+ }
28253
+ const namedWeb = targets.find(([name]) => name === "web");
28254
+ if (namedWeb)
28255
+ return { name: namedWeb[0], config: namedWeb[1], root: targetRoot(configRoot, namedWeb[1], namedWeb[0]) };
28256
+ if (targets.length === 1) {
28257
+ const selected = targets[0];
28258
+ return { name: selected[0], config: selected[1], root: targetRoot(configRoot, selected[1], selected[0]) };
28259
+ }
28260
+ throw new Error(`Multiple frontend deploy targets found (${targets.map(([name]) => name).sort().join(", ")}). Set defaultTarget to the Web target`);
28261
+ }
28156
28262
  function targetRoot(configRoot, target, name) {
28157
28263
  const root = resolve22(configRoot, target.root || (name === "frontend" ? "." : name));
28158
28264
  const prefix = `${resolve22(configRoot)}${sep5}`;
@@ -28217,9 +28323,9 @@ function resolveInsideProject(projectDirectory, candidate, allowRoot = false) {
28217
28323
  return resolved;
28218
28324
  }
28219
28325
  function createBuildEnvironment(names, environmentVariables) {
28220
- const selectedNames = [...new Set(names || [])].sort();
28221
- const selectedNameSet = new Set(selectedNames);
28222
28326
  const values = environmentVariables ?? process.env;
28327
+ const selectedNames = [...new Set(names ?? Object.keys(values).filter(isPublicBuildEnv))].sort();
28328
+ const selectedNameSet = new Set(selectedNames);
28223
28329
  const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !isProtectedBuildEnv(name) && (!isPublicBuildEnv(name) || selectedNameSet.has(name)) && (!SENSITIVE_BUILD_ENV.test(name) || isPublicBuildEnv(name))));
28224
28330
  const missing = selectedNames.filter((name) => !values[name]?.trim());
28225
28331
  for (const name of selectedNames) {
@@ -28235,15 +28341,21 @@ function assertBuildEnvironment(build, skipBuild) {
28235
28341
  }
28236
28342
  }
28237
28343
  async function runBuild(command, cwd, environment, json) {
28238
- const child = Bun.spawn(["sh", "-c", command], {
28239
- cwd,
28240
- env: environment,
28241
- stdio: ["inherit", json ? 2 : "inherit", "inherit"]
28344
+ await new Promise((resolve, reject) => {
28345
+ const child = spawn2("sh", ["-c", command], {
28346
+ cwd,
28347
+ env: environment,
28348
+ stdio: ["inherit", json ? 2 : "inherit", "inherit"]
28349
+ });
28350
+ child.once("error", reject);
28351
+ child.once("close", (exitCode) => {
28352
+ if (exitCode === 0) {
28353
+ resolve();
28354
+ } else {
28355
+ reject(new Error(`Build command failed (exit ${exitCode ?? "unknown"}): ${command}`));
28356
+ }
28357
+ });
28242
28358
  });
28243
- const exitCode = await child.exited;
28244
- if (exitCode !== 0) {
28245
- throw new Error(`Build command failed (exit ${exitCode ?? "unknown"}): ${command}`);
28246
- }
28247
28359
  }
28248
28360
  async function collectArchiveFiles(root) {
28249
28361
  const rootStat = await lstat5(root).catch(() => null);
@@ -28276,7 +28388,7 @@ async function collectArchiveFiles(root) {
28276
28388
  const files = new Map;
28277
28389
  const digests = [];
28278
28390
  for (const path of paths) {
28279
- const data = new Uint8Array(await Bun.file(path).arrayBuffer());
28391
+ const data = new Uint8Array(await readFile6(path));
28280
28392
  bytes += data.byteLength;
28281
28393
  if (bytes > MAX_SOURCE_BYTES)
28282
28394
  throw new Error(`Frontend output exceeds ${MAX_SOURCE_BYTES} bytes`);
@@ -28300,7 +28412,7 @@ async function createFrontendArchive(outputDirectory) {
28300
28412
  const archivePath = join17(temporaryDirectory, `${basename8(outputDirectory)}.tar.zst`);
28301
28413
  try {
28302
28414
  const archiveBytes = await createFrontendTarZstd(collected.files);
28303
- await Bun.write(archivePath, archiveBytes);
28415
+ await writeFile6(archivePath, archiveBytes);
28304
28416
  const sha256 = createHash9("sha256").update(archiveBytes).digest("hex");
28305
28417
  return {
28306
28418
  archivePath,
@@ -28325,6 +28437,71 @@ function payload(response) {
28325
28437
  throw new Error("Deployment operation returned an invalid response");
28326
28438
  return parsed;
28327
28439
  }
28440
+ async function stageApplicationWebRelease(http, projectRef, configRoot, skipBuild, json, environmentVariables) {
28441
+ const target = selectFrontendTarget(configRoot, await readDeployConfig(configRoot));
28442
+ if (!target)
28443
+ return null;
28444
+ const projectDirectory = target.root;
28445
+ const targetState = await lstat5(projectDirectory).catch(() => null);
28446
+ if (!targetState?.isDirectory() || targetState.isSymbolicLink()) {
28447
+ throw new Error(`Deploy target '${target.name}' root is not a regular directory: ${projectDirectory}`);
28448
+ }
28449
+ const requestedId = target.config.id;
28450
+ const listResponse = await http.get(`/v1/projects/${encodeURIComponent(projectRef)}/frontend/deployments`);
28451
+ if (!listResponse.ok)
28452
+ throw new Error(`Failed to list frontend deployments (HTTP ${listResponse.status})`);
28453
+ const selected = selectDeployment(deploymentList(listResponse.data), requestedId, await packageName(projectDirectory));
28454
+ const configuredBuildCommand = String(target.config.buildCommand || selected.buildCommand || "").trim();
28455
+ const buildCommand = configuredBuildCommand || (skipBuild ? "" : await defaultBuildCommand(projectDirectory, configRoot));
28456
+ const configuredOutput = String(target.config.outputDirectory || "").trim();
28457
+ const remoteOutput = selected.outputDirectory && selected.outputDirectory !== "." ? selected.outputDirectory : "";
28458
+ const outputDirectory = resolveInsideProject(projectDirectory, configuredOutput || remoteOutput || defaultOutputDirectory(selected.framework));
28459
+ const build = createBuildEnvironment(target.config.buildEnv, environmentVariables);
28460
+ const report = phaseReporter(json);
28461
+ report("web", `${target.name} (${selected.id})`);
28462
+ assertBuildEnvironment(build, skipBuild);
28463
+ if (!skipBuild) {
28464
+ report("build", buildCommand);
28465
+ await runBuild(buildCommand, projectDirectory, build.env, json);
28466
+ } else {
28467
+ report("build", "skipped");
28468
+ }
28469
+ report("package", relative10(projectDirectory, outputDirectory) || ".");
28470
+ const archive = await createFrontendArchive(outputDirectory);
28471
+ try {
28472
+ const authority = payload(await readFrontendReleaseAuthority(http, projectRef, selected.id));
28473
+ const activeReleaseId = typeof authority.active_release_id === "string" ? authority.active_release_id : null;
28474
+ const activeActivationId = typeof authority.active_activation_id === "string" ? authority.active_activation_id : null;
28475
+ const current = Array.isArray(authority.releases) ? record4(authority.releases[0]) : null;
28476
+ if (archive.sha256 === activeReleaseId && archive.treeSha256 !== current?.tree_sha256) {
28477
+ throw new Error("Active Web release tree does not match the local archive");
28478
+ }
28479
+ if (archive.treeSha256 === current?.tree_sha256) {
28480
+ report("done", "Web already current");
28481
+ return {};
28482
+ }
28483
+ report("upload", `${archive.fileCount} files`);
28484
+ const uploaded = payload(await uploadFrontendRelease(http, projectRef, selected.id, archive.archivePath));
28485
+ const release = record4(uploaded.release);
28486
+ const releaseId = requiredString(release?.release_id, "release.release_id");
28487
+ if (releaseId !== archive.sha256)
28488
+ throw new Error("Uploaded Web release identity does not match the local archive");
28489
+ if (release?.tree_sha256 !== archive.treeSha256)
28490
+ throw new Error("Uploaded Web release tree does not match the local build");
28491
+ report("done", releaseId.slice(0, 12));
28492
+ return {
28493
+ request: {
28494
+ deployment_id: selected.id,
28495
+ release_id: releaseId,
28496
+ expected_active_release_id: activeReleaseId ?? "absent",
28497
+ expected_activation_id: activeActivationId ?? "absent",
28498
+ mutation_id: crypto.randomUUID()
28499
+ }
28500
+ };
28501
+ } finally {
28502
+ await archive.cleanup();
28503
+ }
28504
+ }
28328
28505
  function phaseReporter(json) {
28329
28506
  const started = performance.now();
28330
28507
  return (phase, detail) => {
@@ -28373,12 +28550,10 @@ function registerDeployTools(server, http, options = {}) {
28373
28550
  }
28374
28551
  const configurationPath = options.application.configurationPath?.trim();
28375
28552
  const configurationId = options.application.configurationId?.trim();
28376
- if (!configurationPath && !configurationId) {
28377
- throw new Error("Application profile requires SUPACLOUD_APPLICATION_CONFIGURATION_PATH or SUPACLOUD_APPLICATION_CONFIGURATION_ID");
28378
- }
28379
28553
  if (configurationPath && configurationId) {
28380
28554
  throw new Error("Application profile must set only one of SUPACLOUD_APPLICATION_CONFIGURATION_PATH or SUPACLOUD_APPLICATION_CONFIGURATION_ID");
28381
28555
  }
28556
+ const stagedWeb = await stageApplicationWebRelease(http, projectRef, configRoot, args.skip_build === true, args.json === true, options.environmentVariables);
28382
28557
  const result = await options.applicationRelease({
28383
28558
  action: "release",
28384
28559
  ref: projectRef,
@@ -28389,6 +28564,7 @@ function registerDeployTools(server, http, options = {}) {
28389
28564
  ...configurationId ? { configuration_id: configurationId } : {},
28390
28565
  ...options.application.manifestPath ? { manifest_path: options.application.manifestPath } : {},
28391
28566
  ...args.skip_build === true ? { skip_build: true } : {},
28567
+ ...stagedWeb?.request === undefined ? {} : { web: stagedWeb.request },
28392
28568
  ...args.json === true ? { format: "json", json: true } : {}
28393
28569
  });
28394
28570
  return result;
@@ -28625,7 +28801,7 @@ function registerDeployTools(server, http, options = {}) {
28625
28801
 
28626
28802
  // src/shared/tools/remote-dev-tools.ts
28627
28803
  import { existsSync as existsSync14 } from "node:fs";
28628
- import { readFile as readFile6 } from "node:fs/promises";
28804
+ import { readFile as readFile7 } from "node:fs/promises";
28629
28805
  import { readdir as readdir7 } from "node:fs/promises";
28630
28806
  import { createHash as createHash10 } from "node:crypto";
28631
28807
  import { join as join18, resolve as resolve23 } from "node:path";
@@ -28703,7 +28879,7 @@ async function readProjectConfig(root) {
28703
28879
  if (!existsSync14(file))
28704
28880
  return {};
28705
28881
  try {
28706
- const parsed = JSON.parse(await readFile6(file, "utf8"));
28882
+ const parsed = JSON.parse(await readFile7(file, "utf8"));
28707
28883
  return parsed && typeof parsed === "object" ? parsed : {};
28708
28884
  } catch (error) {
28709
28885
  throw new Error(`Invalid supacloud.json: ${error instanceof Error ? error.message : String(error)}`);
@@ -28729,7 +28905,7 @@ async function sourceFingerprint(root) {
28729
28905
  const hash = createHash10("sha256");
28730
28906
  for (const file of files) {
28731
28907
  hash.update(file.slice(root.length).replace(/\\/g, "/"));
28732
- hash.update(await readFile6(file));
28908
+ hash.update(await readFile7(file));
28733
28909
  }
28734
28910
  return hash.digest("hex");
28735
28911
  }
@@ -28895,7 +29071,7 @@ function registerRemoteDevTools(server, options = {}) {
28895
29071
 
28896
29072
  // src/shared/tools/developer-experience-tools.ts
28897
29073
  import { existsSync as existsSync15 } from "node:fs";
28898
- import { mkdir as mkdir7, readdir as readdir8, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
29074
+ import { mkdir as mkdir7, readdir as readdir8, readFile as readFile8, writeFile as writeFile7 } from "node:fs/promises";
28899
29075
  import { dirname as dirname12, join as join19, resolve as resolve24 } from "node:path";
28900
29076
  var initToolSchema = {
28901
29077
  root: optional(String2(), "Project directory to initialize (default: current directory)"),
@@ -28950,7 +29126,7 @@ async function readPackageManifest(root) {
28950
29126
  if (!existsSync15(path))
28951
29127
  return {};
28952
29128
  try {
28953
- const parsed = JSON.parse(await readFile7(path, "utf8"));
29129
+ const parsed = JSON.parse(await readFile8(path, "utf8"));
28954
29130
  const value = record5(parsed);
28955
29131
  if (!value)
28956
29132
  throw new Error("package.json must contain an object");
@@ -28968,7 +29144,7 @@ async function readPackageManifest(root) {
28968
29144
  }
28969
29145
  async function writeNewFile(path, content) {
28970
29146
  try {
28971
- await writeFile6(path, content, { encoding: "utf8", flag: "wx", mode: 384 });
29147
+ await writeFile7(path, content, { encoding: "utf8", flag: "wx", mode: 384 });
28972
29148
  return true;
28973
29149
  } catch (error) {
28974
29150
  if (error instanceof Error && "code" in error && error.code === "EEXIST")
@@ -28978,7 +29154,7 @@ async function writeNewFile(path, content) {
28978
29154
  }
28979
29155
  async function ensureGitignore(root) {
28980
29156
  const path = join19(root, ".gitignore");
28981
- const existing = existsSync15(path) ? await readFile7(path, "utf8") : "";
29157
+ const existing = existsSync15(path) ? await readFile8(path, "utf8") : "";
28982
29158
  const required = [".env.supacloud.*", "!.env.supacloud.*.example"];
28983
29159
  const missing = required.filter((entry) => !existing.split(/\r?\n/).includes(entry));
28984
29160
  if (missing.length === 0)
@@ -28986,7 +29162,7 @@ async function ensureGitignore(root) {
28986
29162
  const prefix = existing.length > 0 && !existing.endsWith(`
28987
29163
  `) ? `${existing}
28988
29164
  ` : existing;
28989
- await writeFile6(path, `${prefix}${missing.join(`
29165
+ await writeFile7(path, `${prefix}${missing.join(`
28990
29166
  `)}
28991
29167
  `, "utf8");
28992
29168
  return true;
@@ -29009,7 +29185,7 @@ async function initializeProject(request, options) {
29009
29185
  };
29010
29186
  const created = [];
29011
29187
  try {
29012
- await writeFile6(configPath, `${JSON.stringify(config, null, 2)}
29188
+ await writeFile7(configPath, `${JSON.stringify(config, null, 2)}
29013
29189
  `, {
29014
29190
  encoding: "utf8",
29015
29191
  flag: "wx",
@@ -29205,7 +29381,7 @@ function registerDeveloperExperienceTools(server, options = {}) {
29205
29381
  // package.json
29206
29382
  var package_default = {
29207
29383
  name: "@supacloud/cli",
29208
- version: "0.88.0",
29384
+ version: "0.90.1",
29209
29385
  description: "Project-scoped CLI for SupaCloud users",
29210
29386
  type: "module",
29211
29387
  main: "./dist/index.js",
@@ -29258,13 +29434,13 @@ var package_default = {
29258
29434
  },
29259
29435
  dependencies: {
29260
29436
  "@supacloud/compiler": "^0.44.1",
29261
- "@supacloud/delivery": "0.14.1",
29437
+ "@supacloud/delivery": "0.15.0",
29262
29438
  "@supacloud/db": "0.16.1",
29263
29439
  typebox: "1.3.36"
29264
29440
  },
29265
29441
  overrides: {
29266
29442
  "@supacloud/contracts": "0.8.1",
29267
- "@supacloud/delivery": "0.14.1",
29443
+ "@supacloud/delivery": "0.15.0",
29268
29444
  "@graphql-tools/utils": "^12.0.3"
29269
29445
  },
29270
29446
  devDependencies: {
@@ -29498,8 +29674,6 @@ EXAMPLES
29498
29674
  ${preferredCommand} release postgrest_restart --ref abc123
29499
29675
  ${preferredCommand} release release_canary_fixture_stage_replay --ref abc123 --subject <uuid> --request_id <uuid>
29500
29676
  ${preferredCommand} release release_canary_fixture_disable_replay --ref abc123 --fixture_id <uuid> --disable_request_id <uuid> --issuer <issuer-url> --subject <uuid>
29501
- ${preferredCommand} release scope_rebind --file supacloud/fa/release-scopes/20260907-intake-p1-production.json
29502
- ${preferredCommand} release scope_inspect --file supacloud/fa/release-scopes/20260907-intake-p1-production.json
29503
29677
  ${preferredCommand} queue stats --queue emails
29504
29678
  ${preferredCommand} queue dlq --queue emails --limit 20
29505
29679
  ${preferredCommand} frontend list --ref abc123
@@ -8,6 +8,6 @@ export declare function stringEnum<const Values extends readonly [string, ...str
8
8
  export declare function withDescription<T extends TSchema>(schema: T, description: string): T;
9
9
  export declare function optional<T extends TSchema>(schema: T, description?: string): "~optional" extends keyof T ? T : Type.TOptional<T>;
10
10
  export declare function decodedSchema<TInput extends TSchema, TOutput extends TSchema>(inputSchema: TInput, outputSchema: TOutput, decode: (input: StaticDecode<TInput>) => unknown, options?: TSchemaOptions): Type.TCodec<TInput, import("typebox").StaticType<[], "Decode", {}, {}, TOutput>>;
11
- export declare function schemaEnumValues(schema: TSchema): string[];
11
+ export declare function schemaEnumValues(schema: TSchema | undefined): string[];
12
12
  export declare function schemaDescription(schema: TSchema): string;
13
13
  export declare function schemaProperties(schema: TSchema | ToolSchema): ToolSchema;
@@ -1,3 +1,4 @@
1
+ import type { ApplicationDeployRequest } from "@supacloud/delivery";
1
2
  import { type ToolServer } from "../tool-server";
2
3
  import { type AppManifest } from "./app-tool-export";
3
4
  declare const REMOTE_APP_ACTIONS: {
@@ -53,6 +54,7 @@ export interface AppToolArguments {
53
54
  source_environment_id?: string;
54
55
  mutation_id?: string;
55
56
  approved_migration_digest?: string;
57
+ web?: ApplicationDeployRequest["web"];
56
58
  branch_ref?: string;
57
59
  branch_name?: string;
58
60
  data_mode?: "schema_only" | "full_clone";
@@ -152,7 +152,8 @@ export declare const ApplicationPromotionPlanSchema: import("typebox").TObject<{
152
152
  execution_performed: import("typebox").TLiteral<false>;
153
153
  data_recovery: import("typebox").TLiteral<"separate-required">;
154
154
  }>;
155
- export declare const ApplicationReleaseRequestSchema: import("typebox").TObject<{
155
+ export declare const ApplicationDeployRequestSchema: import("typebox").TObject<{
156
+ mutation_id: import("typebox").TString;
156
157
  release_id: import("typebox").TString;
157
158
  configuration_id: import("typebox").TOptional<import("typebox").TString>;
158
159
  configuration_write: import("typebox").TOptional<import("typebox").TObject<{
@@ -168,15 +169,22 @@ export declare const ApplicationReleaseRequestSchema: import("typebox").TObject<
168
169
  }>>;
169
170
  }>;
170
171
  }>>;
171
- activation_id: import("typebox").TOptional<import("typebox").TString>;
172
- expected_activation_id: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TString, import("typebox").TNull]>>;
173
172
  expected_plan_sha256: import("typebox").TOptional<import("typebox").TString>;
173
+ approved_migration_digest: import("typebox").TOptional<import("typebox").TString>;
174
+ web: import("typebox").TOptional<import("typebox").TObject<{
175
+ deployment_id: import("typebox").TString;
176
+ release_id: import("typebox").TString;
177
+ expected_active_release_id: import("typebox").TString;
178
+ expected_activation_id: import("typebox").TString;
179
+ mutation_id: import("typebox").TString;
180
+ }>>;
174
181
  }>;
175
- export declare const ApplicationReleaseResultSchema: import("typebox").TObject<{
176
- schema: import("typebox").TLiteral<"supacloud.application-release.v1">;
182
+ export declare const ApplicationDeployResultSchema: import("typebox").TObject<{
183
+ schema: import("typebox").TLiteral<"supacloud.application-deploy.v1">;
177
184
  project_ref: import("typebox").TString;
178
185
  application_id: import("typebox").TString;
179
186
  environment_id: import("typebox").TString;
187
+ mutation_id: import("typebox").TString;
180
188
  release_id: import("typebox").TString;
181
189
  configuration_id: import("typebox").TString;
182
190
  activation_id: import("typebox").TString;
@@ -185,4 +193,33 @@ export declare const ApplicationReleaseResultSchema: import("typebox").TObject<{
185
193
  action: import("typebox").TUnion<[import("typebox").TLiteral<"activate">, import("typebox").TLiteral<"no-op">]>;
186
194
  replayed: import("typebox").TBoolean;
187
195
  execution_performed: import("typebox").TBoolean;
196
+ backend_receipt: import("typebox").TObject<{
197
+ schema: import("typebox").TLiteral<"supacloud.application-backend-receipt.v1">;
198
+ project_ref: import("typebox").TString;
199
+ application_id: import("typebox").TString;
200
+ environment_id: import("typebox").TString;
201
+ release_id: import("typebox").TString;
202
+ manifest_sha256: import("typebox").TString;
203
+ mutation_id: import("typebox").TString;
204
+ plan_sha256: import("typebox").TString;
205
+ backup_id: import("typebox").TUnion<[import("typebox").TString, import("typebox").TNull]>;
206
+ before_ledger_digest: import("typebox").TString;
207
+ after_ledger_digest: import("typebox").TString;
208
+ applied_versions: import("typebox").TArray<import("typebox").TString>;
209
+ schema_reloaded: import("typebox").TBoolean;
210
+ postgrest_healthy: import("typebox").TBoolean;
211
+ }>;
212
+ web: import("typebox").TOptional<import("typebox").TObject<{
213
+ deployment_id: import("typebox").TString;
214
+ release_id: import("typebox").TString;
215
+ activation_id: import("typebox").TString;
216
+ replayed: import("typebox").TBoolean;
217
+ }>>;
218
+ }>;
219
+ export declare const ApplicationDeployWebRequestSchema: import("typebox").TObject<{
220
+ deployment_id: import("typebox").TString;
221
+ release_id: import("typebox").TString;
222
+ expected_active_release_id: import("typebox").TString;
223
+ expected_activation_id: import("typebox").TString;
224
+ mutation_id: import("typebox").TString;
188
225
  }>;
@@ -9,7 +9,7 @@ export declare const APPLICATION_TOOL_SCHEMA: {
9
9
  ttl_seconds: Type.TOptional<Type.TInteger>;
10
10
  wait: Type.TOptional<Type.TBoolean>;
11
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_release_transfer_plan" | "get_promotion_plan" | "get_promotion_status" | "get_rollback_snapshot" | "get_history" | "get_deploy_plan" | "get_preview_plan" | "list_previews" | "get_preview" | "put_configuration" | "release_application" | "transfer_release" | "promote_application" | "reconcile_promotion" | "create_preview" | "reconcile_preview" | "cleanup_preview" | "deploy_release" | "rollback_release" | "reconcile_activation" | "retire_activation")[]>;
12
+ action: Type.TEnum<("list_releases" | "get_release" | "upload_release" | "activate_release" | "logs" | "get_runtime" | "get_deployment_evidence" | "get_configuration" | "get_release_transfer_plan" | "get_promotion_plan" | "get_promotion_status" | "get_rollback_snapshot" | "get_history" | "get_deploy_plan" | "get_preview_plan" | "list_previews" | "get_preview" | "put_configuration" | "deploy_application" | "transfer_release" | "promote_application" | "reconcile_promotion" | "create_preview" | "reconcile_preview" | "cleanup_preview" | "deploy_release" | "rollback_release" | "reconcile_activation" | "retire_activation")[]>;
13
13
  ref: Type.TString;
14
14
  id: Type.TString;
15
15
  environment_id: Type.TOptional<Type.TString>;
@@ -19,6 +19,13 @@ export declare const APPLICATION_TOOL_SCHEMA: {
19
19
  configuration_path: Type.TOptional<Type.TString>;
20
20
  configuration_write: Type.TOptional<Type.TUnknown>;
21
21
  manifest_path: Type.TOptional<Type.TString>;
22
+ web: Type.TOptional<Type.TObject<{
23
+ deployment_id: Type.TString;
24
+ release_id: Type.TString;
25
+ expected_active_release_id: Type.TString;
26
+ expected_activation_id: Type.TString;
27
+ mutation_id: Type.TString;
28
+ }>>;
22
29
  release_id: Type.TOptional<Type.TString>;
23
30
  source_ref: Type.TOptional<Type.TString>;
24
31
  source_release_id: Type.TOptional<Type.TString>;
@@ -97,6 +97,8 @@ function decodedSchema(inputSchema, outputSchema, decode, options) {
97
97
  return result;
98
98
  }
99
99
  function schemaEnumValues(schema) {
100
+ if (!schema)
101
+ return [];
100
102
  const view = schema;
101
103
  if (Array.isArray(view.enum)) {
102
104
  return view.enum.filter((value) => typeof value === "string" || typeof value === "number").map((value) => String(value));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.88.0",
3
+ "version": "0.90.1",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,13 +53,13 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@supacloud/compiler": "^0.44.1",
56
- "@supacloud/delivery": "0.14.1",
56
+ "@supacloud/delivery": "0.15.0",
57
57
  "@supacloud/db": "0.16.1",
58
58
  "typebox": "1.3.36"
59
59
  },
60
60
  "overrides": {
61
61
  "@supacloud/contracts": "0.8.1",
62
- "@supacloud/delivery": "0.14.1",
62
+ "@supacloud/delivery": "0.15.0",
63
63
  "@graphql-tools/utils": "^12.0.3"
64
64
  },
65
65
  "devDependencies": {
@@ -23,9 +23,9 @@ Load this reference when selecting a command surface or when a user asks an AI t
23
23
  | Inspect or back up a remote database | `supabase db_pull`, `migration_list`, `db_dump`, `gen_types` | Requires explicit PostgreSQL DSN; redact it |
24
24
  | Replay the release-canary fixture stage receipt | `release release_canary_fixture_stage_replay` | Dual-bind Management project context and that project's service-role application origin; accepts only the exact subject/request UUID pair and a strict idempotent receipt |
25
25
  | Disable a staged release-canary fixture | `release release_canary_fixture_disable_replay` | Uses the fixed disable RPC once, accepts idempotent false/true receipts, then queries the existing pending RPC with issuer/subject and requires authoritative JSON `false`; fixture binding comes from the strict disable receipt |
26
- | Rebind release scope manifest to upstream base commit | `release scope_rebind` | Local action; updates baseCommit in scope JSON files canonically to target git commit, supports `--dry_run`, `--task`, and recalculates `scope_sha256` |
27
- | Inspect release scope manifest and git alignment | `release scope_inspect` | Local action; validates scope structure, returns canonical hash, and checks alignment with HEAD / origin/main |
28
- | Scaffold a new canonical release scope manifest | `release scope_create` | Local action; outputs canonical JSON scope with sorted functions, migrations, and baseCommit |
26
+ | Rebind release scope manifest to upstream base commit | `release scope_rebind` | Local maintenance action; requires the exact `--environment_id`, updates only scopes bound to that environment, supports `--dry_run`/`--task`, and recalculates `scope_sha256` |
27
+ | Inspect release scope manifest and git alignment | `release scope_inspect` | Local maintenance action; requires the exact `--environment_id`, rejects legacy or cross-environment scopes, returns canonical hash, and checks alignment with HEAD / origin/main |
28
+ | Scaffold a new canonical release scope manifest | `release scope_create` | Local maintenance action; requires `--environment_id` and outputs a current-main-bound scope with sorted functions, migrations, and environment identity |
29
29
  | Preview/apply migrations remotely | `supabase push` | Always dry-run first; production needs explicit approval |
30
30
  | Mark proven-equivalent historical migrations as applied | `database baseline_migrations` | Dry-run, schema-equivalence proof, backup, explicit approval |
31
31
  | Inspect auth users or generate a controlled login link | `auth list_users`, `auth get_user`, `auth generate_link` | User reads are bounded; generation supports only `magiclink`, `recovery`, and `invite`, requires production confirmation, and returns only a validated action URL |
@@ -51,7 +51,7 @@ until a project-scoped context is resolved.
51
51
  - `secrets`: project secret management; never print values after write.
52
52
  - `queue`, `task_events`, `diagnostics`: asynchronous workload operations and bounded diagnostics.
53
53
  - `gateway`: project route/config/rebuild operations.
54
- - `release`: local release scope rebinding/inspection (`scope_rebind`, `scope_inspect`, `scope_create`), verified backup/PostgREST lifecycle controls, plus production-confirmed, non-retried, strict release-canary fixture stage and disable replay actions.
54
+ - `release`: local release scope maintenance (`scope_rebind`, `scope_inspect`, `scope_create`), verified backup/PostgREST lifecycle controls, plus production-confirmed, non-retried, strict release-canary fixture stage and disable replay actions. Scope maintenance is not a prerequisite for the normal `supacloud --env <name> deploy` flow.
55
55
  - `ai`: inspect or install this packaged Skill.
56
56
 
57
57
  ## Safe inspection pattern