@supacloud/cli 0.21.2 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +260 -149
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -198,6 +198,13 @@ it never empties a bucket. Mutation receipts bind `project_ref`, `bucket_id`,
198
198
  name with another version, or a local version with another name, before either
199
199
  dry-run reporting or apply can continue. This keeps the preview consistent with
200
200
  the server conflict that would otherwise occur after deployment starts.
201
+ Mutation failures use the release-control receipt schema and never include the
202
+ server response body. `OUTCOME_UNKNOWN` requires a fresh migration inventory
203
+ read before deciding whether a retry is safe. A migration push failure also
204
+ reports the local files applied or skipped before the failed file, so operators
205
+ can reconcile a partially completed sequence without exposing SQL. Baseline
206
+ success requires a bounded migration-inventory readback that confirms every
207
+ requested version, name, baseline marker, and checksum.
201
208
 
202
209
  `edge_functions deploy --path` bundles local TypeScript and dependencies with
203
210
  Bun and runs a local syntax check before upload. The Management API validates and
package/dist/index.js CHANGED
@@ -6897,6 +6897,38 @@ function projectRefPathSegment(ref, operation) {
6897
6897
  return encodeURIComponent(ref);
6898
6898
  }
6899
6899
 
6900
+ // src/shared/tools/release-control-response.ts
6901
+ var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
6902
+ function releaseControlSuccess(operation, payload) {
6903
+ return releaseControlResponse({
6904
+ ...payload,
6905
+ schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
6906
+ ok: true,
6907
+ operation
6908
+ });
6909
+ }
6910
+ function releaseControlFailure(operation, code, httpStatus, safeState = {}) {
6911
+ return releaseControlErrorResponse({
6912
+ ...safeState,
6913
+ schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
6914
+ ok: false,
6915
+ operation,
6916
+ error: { code, http_status: httpStatus }
6917
+ });
6918
+ }
6919
+ function releaseControlMutationFailure(operation, response, safeState = {}) {
6920
+ const outcomeUnknown = response.transportError || response.responseReadError || response.status === 408 || response.status >= 500;
6921
+ return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status, safeState) : releaseControlFailure(operation, "HTTP_ERROR", response.status, safeState);
6922
+ }
6923
+ function releaseControlResponse(payload) {
6924
+ return {
6925
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
6926
+ };
6927
+ }
6928
+ function releaseControlErrorResponse(payload) {
6929
+ return { ...releaseControlResponse(payload), isError: true };
6930
+ }
6931
+
6900
6932
  // src/shared/tools/database-tools.ts
6901
6933
  var MAX_MIGRATION_VERSION = 9223372036854775807n;
6902
6934
  var FALLBACK_MIGRATION_VERSION_BASE = 8000000000000000000n;
@@ -7046,95 +7078,188 @@ function sortMigrationFiles(migrations) {
7046
7078
  }
7047
7079
  return sorted;
7048
7080
  }
7049
- function appliedMigrationKeys(data) {
7050
- const rows = Array.isArray(data) ? data : data?.rows || [];
7051
- const keys = new Set;
7052
- for (const row of rows) {
7053
- if (!row || typeof row !== "object")
7054
- continue;
7055
- const migration = row;
7056
- if (migration.version != null)
7057
- keys.add(String(migration.version));
7058
- if (migration.name != null)
7059
- keys.add(String(migration.name));
7060
- }
7061
- return keys;
7081
+ function normalizedMigrationStatement(statement) {
7082
+ return statement.replace(/\r\n?/g, `
7083
+ `).trim();
7062
7084
  }
7063
7085
  function migrationRows(data) {
7064
- const rows = Array.isArray(data) ? data : data?.rows || [];
7065
- return Array.isArray(rows) ? rows.filter((row) => Boolean(row && typeof row === "object")) : [];
7086
+ if (Array.isArray(data))
7087
+ return data;
7088
+ if (data && typeof data === "object" && Array.isArray(data.rows)) {
7089
+ return data.rows;
7090
+ }
7091
+ throw new Error("Invalid remote migration inventory");
7066
7092
  }
7067
- function migrationIdentityKey(version, name) {
7068
- return `${version}\x00${name}`;
7093
+ function migrationIdentity(row) {
7094
+ if (!row || typeof row !== "object" || Array.isArray(row)) {
7095
+ throw new Error("Invalid remote migration identity");
7096
+ }
7097
+ const migration = row;
7098
+ if (!isMigrationInventoryVersion(migration.version) || !isMigrationInventoryName(migration.name)) {
7099
+ throw new Error("Invalid remote migration identity");
7100
+ }
7101
+ return { version: migration.version, name: migration.name, statements: migration.statements };
7069
7102
  }
7070
7103
  function migrationIdentities(data) {
7071
- return migrationRows(data).flatMap((row) => row.version == null || row.name == null ? [] : [{ version: String(row.version), name: String(row.name), statements: row.statements }]);
7072
- }
7073
- function isLegacyNameBoundMigration(local, remote, remoteMigrations) {
7074
- return remoteMigrations.filter((candidate) => candidate.name === local.name).length === 1 && local.name === remote.name && local.version !== remote.version && Array.isArray(remote.statements) && remote.statements.length === 1 && typeof remote.statements[0] === "string" && remote.statements[0].trim() === local.sql.trim();
7104
+ const identities = migrationRows(data).map(migrationIdentity);
7105
+ const versions = new Set;
7106
+ const names = new Set;
7107
+ for (const identity of identities) {
7108
+ if (versions.has(identity.version) || identity.name !== null && names.has(identity.name)) {
7109
+ throw new Error("Invalid remote migration inventory");
7110
+ }
7111
+ versions.add(identity.version);
7112
+ if (identity.name !== null)
7113
+ names.add(identity.name);
7114
+ }
7115
+ return identities;
7075
7116
  }
7076
- function identityConflicts(local, remote, remoteMigrations) {
7077
- const reusesVersionOrName = local.version === remote.version || local.name === remote.name;
7078
- const exactlyMatches = local.version === remote.version && local.name === remote.name;
7079
- return reusesVersionOrName && !exactlyMatches && !isLegacyNameBoundMigration(local, remote, remoteMigrations);
7117
+ function singleRemoteStatement(remote) {
7118
+ return Array.isArray(remote.statements) && remote.statements.length === 1 && typeof remote.statements[0] === "string" ? remote.statements[0] : null;
7080
7119
  }
7081
- function migrationIdentityConflicts(data, migrationFiles) {
7120
+ function exactIdentityIsApplied(local, remote) {
7121
+ const statement = singleRemoteStatement(remote);
7122
+ if (statement === null)
7123
+ return false;
7124
+ if (statement === `baseline:${local.name}` || statement === `direct-apply:${local.name}`)
7125
+ return true;
7126
+ const rawChecksum = createHash("sha256").update(local.rawBytes).digest("hex");
7127
+ return statement === `sha256:${rawChecksum}` || statement.trim() === local.sql.trim();
7128
+ }
7129
+ function legacyIdentityIsApplied(local, remote) {
7130
+ const statement = singleRemoteStatement(remote);
7131
+ return remote.name === local.name && remote.version !== local.version && statement !== null && statement.trim() === local.sql.trim();
7132
+ }
7133
+ function migrationIdentityConflict(local) {
7134
+ return new Error(`Migration identity conflicts:
7135
+ - ${local.file} (${local.version}) conflicts with remote inventory`);
7136
+ }
7137
+ function migrationDisposition(local, remoteMigrations) {
7138
+ const sameVersion = remoteMigrations.filter((remote2) => remote2.version === local.version);
7139
+ const sameName = remoteMigrations.filter((remote2) => remote2.name === local.name);
7140
+ if (sameVersion.length > 1 || sameName.length > 1)
7141
+ throw migrationIdentityConflict(local);
7142
+ if (!sameVersion.length && !sameName.length)
7143
+ return "pending";
7144
+ if (sameVersion[0] && sameName[0] && sameVersion[0] !== sameName[0])
7145
+ throw migrationIdentityConflict(local);
7146
+ const remote = sameVersion[0] ?? sameName[0];
7147
+ const exactIdentity = remote.version === local.version && remote.name === local.name;
7148
+ if (exactIdentity && exactIdentityIsApplied(local, remote))
7149
+ return "applied";
7150
+ if (!sameVersion.length && legacyIdentityIsApplied(local, remote))
7151
+ return "applied";
7152
+ throw migrationIdentityConflict(local);
7153
+ }
7154
+ function migrationPushPlan(data, migrationFiles) {
7082
7155
  const remoteMigrations = migrationIdentities(data);
7083
- return migrationFiles.flatMap((localMigration) => remoteMigrations.filter((remoteMigration) => identityConflicts(localMigration, remoteMigration, remoteMigrations)).map((remoteMigration) => ({ file: localMigration.file, local: localMigration, remote: remoteMigration })));
7156
+ const plan = { alreadyApplied: [], pending: [] };
7157
+ for (const migration of migrationFiles) {
7158
+ const disposition = migrationDisposition(migration, remoteMigrations);
7159
+ plan[disposition === "applied" ? "alreadyApplied" : "pending"].push(migration);
7160
+ }
7161
+ return plan;
7084
7162
  }
7085
- function legacyNameBoundMigrationKeys(data, migrationFiles) {
7086
- const remoteMigrations = migrationIdentities(data);
7087
- return new Set(migrationFiles.flatMap((localMigration) => remoteMigrations.some((remoteMigration) => isLegacyNameBoundMigration(localMigration, remoteMigration, remoteMigrations)) ? [migrationIdentityKey(localMigration.version, localMigration.name)] : []));
7163
+ function recordPayload(payload) {
7164
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : null;
7088
7165
  }
7089
- function assertNoMigrationIdentityConflicts(data, migrationFiles) {
7090
- const conflicts = migrationIdentityConflicts(data, migrationFiles);
7091
- if (!conflicts.length)
7092
- return;
7093
- throw new Error([
7094
- "Migration identity conflicts:",
7095
- ...conflicts.map(({ file, local, remote }) => `- ${file} (${local.version}) conflicts with remote ${remote.name} (${remote.version})`)
7096
- ].join(`
7097
- `));
7166
+ function confirmedMigrationReceipt(payload, expected) {
7167
+ const receipt = recordPayload(payload);
7168
+ if (!receipt || !isMigrationInventoryVersion(receipt.version))
7169
+ return false;
7170
+ const responseStatements = [expected.sql.trim()];
7171
+ const checksumStatements = [normalizedMigrationStatement(expected.sql)];
7172
+ const name = expected.name.trim();
7173
+ if (receipt.name !== name || JSON.stringify(receipt.statements) !== JSON.stringify(responseStatements))
7174
+ return false;
7175
+ if (expected.version !== undefined && receipt.version !== expected.version)
7176
+ return false;
7177
+ return receipt.checksum === migrationInventoryChecksum({ version: receipt.version, name, statements: checksumStatements });
7098
7178
  }
7099
- function nameBoundMigrationMarkerKeys(data) {
7100
- const keys = new Set;
7101
- for (const row of migrationRows(data)) {
7102
- if (row.version == null || row.name == null || !Array.isArray(row.statements))
7103
- continue;
7104
- if (row.statements.length !== 1)
7105
- continue;
7106
- const name = String(row.name);
7107
- const marker = row.statements[0];
7108
- if (marker !== `baseline:${name}` && marker !== `direct-apply:${name}`)
7109
- continue;
7110
- keys.add(migrationIdentityKey(String(row.version), String(row.name)));
7111
- }
7112
- return keys;
7179
+ function isAlreadyAppliedMigrationResponse(response, migration) {
7180
+ if (response.ok || response.status !== 409 || response.transportError || response.responseReadError)
7181
+ return false;
7182
+ if (!response.data || typeof response.data !== "object" || Array.isArray(response.data))
7183
+ return false;
7184
+ const body = response.data;
7185
+ const name = migration.name.trim();
7186
+ const statements = [normalizedMigrationStatement(migration.sql)];
7187
+ return body.code === "409" && body.message === "Migration already applied" && body.version === migration.version && body.name === name && body.checksum === migrationInventoryChecksum({ version: migration.version, name, statements });
7188
+ }
7189
+ function expectedBaselineChecksum(migration) {
7190
+ return migrationInventoryChecksum({
7191
+ version: migration.version,
7192
+ name: migration.name,
7193
+ statements: [`baseline:${migration.name}`]
7194
+ });
7113
7195
  }
7114
- function historicalChecksumMarkerKeys(data, migrationFiles) {
7115
- const filesByKey = new Map(migrationFiles.map((migration) => [migrationIdentityKey(migration.version, migration.name), migration]));
7116
- const keys = new Set;
7117
- for (const row of migrationRows(data)) {
7118
- if (row.version == null || row.name == null || !Array.isArray(row.statements) || row.statements.length !== 1)
7119
- continue;
7120
- const marker = row.statements[0];
7121
- if (typeof marker !== "string" || !/^sha256:[0-9a-f]{64}$/.test(marker))
7122
- continue;
7123
- const key = migrationIdentityKey(String(row.version), String(row.name));
7124
- const migration = filesByKey.get(key);
7125
- if (!migration)
7126
- continue;
7127
- const checksum = createHash("sha256").update(migration.rawBytes).digest("hex");
7128
- if (marker === `sha256:${checksum}`)
7129
- keys.add(key);
7196
+ function confirmedBaselineReceipt(payload, migrations) {
7197
+ const receipt = recordPayload(payload);
7198
+ if (!receipt || typeof receipt.marked !== "number" || typeof receipt.already_applied !== "number")
7199
+ return null;
7200
+ if (!Number.isSafeInteger(receipt.marked) || !Number.isSafeInteger(receipt.already_applied))
7201
+ return null;
7202
+ if (receipt.marked < 0 || receipt.already_applied < 0)
7203
+ return null;
7204
+ if (receipt.marked + receipt.already_applied !== migrations.length)
7205
+ return null;
7206
+ if (!Array.isArray(receipt.migrations) || receipt.migrations.length !== receipt.marked)
7207
+ return null;
7208
+ const expected = new Map(migrations.map((migration) => [migration.version, migration]));
7209
+ const marked = [];
7210
+ for (const rawMigration of receipt.migrations) {
7211
+ const migration = recordPayload(rawMigration);
7212
+ if (!migration || typeof migration.version !== "string")
7213
+ return null;
7214
+ const local = expected.get(migration.version);
7215
+ if (!local || migration.name !== local.name || migration.checksum !== expectedBaselineChecksum(local))
7216
+ return null;
7217
+ expected.delete(migration.version);
7218
+ marked.push(local);
7130
7219
  }
7131
- return keys;
7220
+ return expected.size === receipt.already_applied ? { marked, concurrentlyAlreadyApplied: receipt.already_applied } : null;
7132
7221
  }
7133
- function isAlreadyAppliedMigrationResponse(response) {
7134
- if (response.status !== 409 || !response.data || typeof response.data !== "object")
7222
+ function baselineInventoryIsApplied(payload, migrations) {
7223
+ const inventory = migrationInventory(payload);
7224
+ if (!inventory)
7135
7225
  return false;
7136
- const body = response.data;
7137
- return body.code === "409" && body.message === "Migration already applied";
7226
+ const byVersion = new Map(inventory.map((migration) => [migration.version, migration]));
7227
+ return migrations.every((migration) => {
7228
+ const remote = byVersion.get(migration.version);
7229
+ return remote?.name === migration.name && remote.statements.length === 1 && remote.statements[0] === `baseline:${migration.name}` && remote.checksum === expectedBaselineChecksum(migration);
7230
+ });
7231
+ }
7232
+ function confirmedSqlBatchReceipt(payload, expectedCommands) {
7233
+ const receipt = recordPayload(payload);
7234
+ if (!receipt || receipt.command !== "BATCH" || !Array.isArray(receipt.statements))
7235
+ return false;
7236
+ if (receipt.statements.length !== expectedCommands.length)
7237
+ return false;
7238
+ return receipt.statements.every((rawStatement, index) => {
7239
+ const statement = recordPayload(rawStatement);
7240
+ if (!statement || statement.index !== index + 1 || statement.command !== expectedCommands[index])
7241
+ return false;
7242
+ if (typeof statement.rowCount !== "number" || !Number.isSafeInteger(statement.rowCount) || statement.rowCount < 0)
7243
+ return false;
7244
+ if (typeof statement.durationMs !== "number" || !Number.isFinite(statement.durationMs) || statement.durationMs < 0)
7245
+ return false;
7246
+ return true;
7247
+ });
7248
+ }
7249
+ function rlsStatementCommands(policyMode) {
7250
+ return [
7251
+ "CREATE",
7252
+ "ALTER",
7253
+ ...Array.from({ length: 5 }, () => "DROP"),
7254
+ ...policyMode === "owner" ? Array.from({ length: 4 }, () => "CREATE") : []
7255
+ ];
7256
+ }
7257
+ function migrationPushFailureState(applied, skipped, failedFile) {
7258
+ return {
7259
+ applied_before_failure: [...applied],
7260
+ skipped_before_failure: [...skipped],
7261
+ failed_file: failedFile
7262
+ };
7138
7263
  }
7139
7264
  function sqlReferencesVector(sql) {
7140
7265
  return /\bvector\s*\(\s*\d+\s*\)/i.test(sql) || /::\s*vector\b/i.test(sql) || /\bvector_(cosine|l2|ip)_ops\b/i.test(sql) || /<=>|<#>|<->/.test(sql);
@@ -7339,8 +7464,16 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7339
7464
  case "apply_migration": {
7340
7465
  if (!args.name || !args.sql)
7341
7466
  throw new Error("'name' and 'sql' required");
7342
- const r = await http.post(`/v1/projects/${ref}/database/migrations`, { name: args.name, sql: args.sql });
7343
- text = r.ok ? `✅ Migration '${args.name}' applied` : `❌ Failed (${r.status}): ${JSON.stringify(r.data)}`;
7467
+ const r = await http.postReleaseMutation(`/v1/projects/${ref}/database/migrations`, { name: args.name, sql: args.sql });
7468
+ if (!r.ok)
7469
+ return releaseControlMutationFailure("database.apply_migration", r);
7470
+ if (r.status !== 200) {
7471
+ return releaseControlFailure("database.apply_migration", "OUTCOME_UNKNOWN", r.status);
7472
+ }
7473
+ if (!confirmedMigrationReceipt(r.data, { name: args.name, sql: args.sql })) {
7474
+ return releaseControlFailure("database.apply_migration", "OUTCOME_UNKNOWN", r.status);
7475
+ }
7476
+ text = `✅ Migration '${args.name}' applied`;
7344
7477
  break;
7345
7478
  }
7346
7479
  case "push_migrations": {
@@ -7356,15 +7489,13 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7356
7489
  const migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
7357
7490
  const migrationsResult = await http.get(`/v1/projects/${ref}/database/migrations`);
7358
7491
  if (!migrationsResult.ok) {
7359
- text = `❌ Failed to load applied migrations (${migrationsResult.status}): ${JSON.stringify(migrationsResult.data)}`;
7360
- break;
7492
+ return releaseControlFailure("database.push_migrations", "HTTP_ERROR", migrationsResult.transportError ? null : migrationsResult.status);
7361
7493
  }
7362
- assertNoMigrationIdentityConflicts(migrationsResult.data, migrationFiles);
7494
+ const migrationPlan = migrationPushPlan(migrationsResult.data, migrationFiles);
7363
7495
  if (args.dry_run) {
7364
- const appliedKeys = appliedMigrationKeys(migrationsResult.data);
7365
- const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
7366
- const alreadyApplied = migrationFiles.filter(({ name, version }) => appliedKeys.has(name) || appliedKeys.has(String(version)));
7367
- const pendingWithSql = pending;
7496
+ const pending2 = migrationPlan.pending;
7497
+ const alreadyApplied = migrationPlan.alreadyApplied;
7498
+ const pendingWithSql = pending2;
7368
7499
  let vectorEnabled = null;
7369
7500
  if (pendingWithSql.some(({ sql }) => sqlReferencesVector(sql) || sqlCreatesVectorExtension(sql))) {
7370
7501
  const extResult = await execSql("SELECT extname AS name FROM pg_extension WHERE extname = 'vector';");
@@ -7376,7 +7507,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7376
7507
  `Total: ${migrationFiles.length}`,
7377
7508
  "",
7378
7509
  "Pending:",
7379
- ...pending.length ? pending.map(({ file, version }) => ` - ${file} (${version})`) : [" - none"],
7510
+ ...pending2.length ? pending2.map(({ file, version }) => ` - ${file} (${version})`) : [" - none"],
7380
7511
  "",
7381
7512
  "Already applied:",
7382
7513
  ...alreadyApplied.length ? alreadyApplied.map(({ file, version }) => ` - ${file} (${version})`) : [" - none"],
@@ -7385,32 +7516,25 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7385
7516
  `);
7386
7517
  break;
7387
7518
  }
7519
+ const pending = new Set(migrationPlan.pending.map(({ file }) => file));
7388
7520
  const applied = [];
7389
7521
  const skipped = [];
7390
- const nameBoundMarkerKeys = nameBoundMigrationMarkerKeys(migrationsResult.data);
7391
- const historicalChecksumKeys = historicalChecksumMarkerKeys(migrationsResult.data, migrationFiles);
7392
- const legacyNameBoundKeys = legacyNameBoundMigrationKeys(migrationsResult.data, migrationFiles);
7393
7522
  for (const { file, name, version, sql } of migrationFiles) {
7394
- const migrationKey = migrationIdentityKey(version, name);
7395
- if (nameBoundMarkerKeys.has(migrationKey) || historicalChecksumKeys.has(migrationKey) || legacyNameBoundKeys.has(migrationKey)) {
7523
+ if (!pending.has(file)) {
7396
7524
  skipped.push(file);
7397
7525
  continue;
7398
7526
  }
7399
- const r = await http.post(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
7400
- if (r.ok) {
7527
+ const r = await http.postReleaseMutation(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
7528
+ if (r.ok && r.status === 200 && confirmedMigrationReceipt(r.data, { name, version, sql })) {
7401
7529
  applied.push(file);
7402
- } else if (isAlreadyAppliedMigrationResponse(r)) {
7530
+ } else if (isAlreadyAppliedMigrationResponse(r, { name, version, sql })) {
7403
7531
  skipped.push(file);
7404
7532
  } else {
7405
- text = [
7406
- `❌ Failed to apply ${file} (${r.status})`,
7407
- JSON.stringify(r.data, null, 2),
7408
- "",
7409
- `Applied before failure: ${applied.length}`,
7410
- `Skipped before failure: ${skipped.length}`
7411
- ].join(`
7412
- `);
7413
- return { content: [{ type: "text", text }] };
7533
+ const safeState = migrationPushFailureState(applied, skipped, file);
7534
+ if (r.ok) {
7535
+ return releaseControlFailure("database.push_migrations", "OUTCOME_UNKNOWN", r.status, safeState);
7536
+ }
7537
+ return releaseControlMutationFailure("database.push_migrations", r, safeState);
7414
7538
  }
7415
7539
  }
7416
7540
  text = [
@@ -7436,12 +7560,11 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7436
7560
  const migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
7437
7561
  const r = await http.get(`/v1/projects/${ref}/database/migrations`);
7438
7562
  if (!r.ok) {
7439
- text = `❌ Failed to load applied migrations (${r.status}): ${JSON.stringify(r.data)}`;
7440
- break;
7563
+ return releaseControlFailure("database.baseline_migrations", "HTTP_ERROR", r.transportError ? null : r.status);
7441
7564
  }
7442
- const appliedKeys = appliedMigrationKeys(r.data);
7443
- const missing = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
7444
- const alreadyApplied = migrationFiles.filter(({ name, version }) => appliedKeys.has(name) || appliedKeys.has(String(version)));
7565
+ const migrationPlan = migrationPushPlan(r.data, migrationFiles);
7566
+ const missing = migrationPlan.pending;
7567
+ const alreadyApplied = migrationPlan.alreadyApplied;
7445
7568
  if (args.dry_run) {
7446
7569
  text = [
7447
7570
  `Migration baseline dry run for ${dir}`,
@@ -7464,16 +7587,28 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7464
7587
  `);
7465
7588
  break;
7466
7589
  }
7467
- const baselineResult = await http.post(`/v1/projects/${ref}/database/migrations/baseline`, { migrations: missing.map(({ name, version }) => ({ name, version })) });
7468
- text = baselineResult.ok ? [
7590
+ const baselineResult = await http.postReleaseMutation(`/v1/projects/${ref}/database/migrations/baseline`, { migrations: missing.map(({ name, version }) => ({ name, version })) });
7591
+ if (!baselineResult.ok) {
7592
+ return releaseControlMutationFailure("database.baseline_migrations", baselineResult);
7593
+ }
7594
+ if (baselineResult.status !== 200) {
7595
+ return releaseControlFailure("database.baseline_migrations", "OUTCOME_UNKNOWN", baselineResult.status);
7596
+ }
7597
+ const receipt = confirmedBaselineReceipt(baselineResult.data, missing);
7598
+ if (!receipt) {
7599
+ return releaseControlFailure("database.baseline_migrations", "OUTCOME_UNKNOWN", baselineResult.status);
7600
+ }
7601
+ const inventoryResult = await http.get(`/v1/projects/${ref}/database/migrations`, { maxResponseBytes: MAX_MIGRATION_INVENTORY_BYTES });
7602
+ if (!inventoryResult.ok || !baselineInventoryIsApplied(inventoryResult.data, missing)) {
7603
+ return releaseControlFailure("database.baseline_migrations", "OUTCOME_UNKNOWN", inventoryResult.transportError ? null : inventoryResult.status);
7604
+ }
7605
+ text = [
7469
7606
  `✅ Migration baseline completed for ${dir}`,
7470
- `Marked applied: ${missing.length}`,
7471
- `Already applied: ${alreadyApplied.length}`,
7472
- "",
7473
- "Marked files:",
7474
- ...missing.map(({ file, version }) => ` - ${file} (${version})`)
7607
+ `Marked applied: ${receipt.marked.length}`,
7608
+ `Already applied: ${alreadyApplied.length + receipt.concurrentlyAlreadyApplied}`,
7609
+ ...receipt.marked.length ? ["", "Marked files:", ...receipt.marked.map(({ file, version }) => ` - ${file} (${version})`)] : []
7475
7610
  ].join(`
7476
- `) : `❌ Failed (${baselineResult.status}): ${JSON.stringify(baselineResult.data)}`;
7611
+ `);
7477
7612
  break;
7478
7613
  }
7479
7614
  case "create_table_rls": {
@@ -7486,8 +7621,16 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7486
7621
  throw new Error("Invalid RLS policy mode");
7487
7622
  const policySql = buildRlsPolicySql(qualifiedTable, policyMode, args.owner_column);
7488
7623
  const sql = `BEGIN; CREATE TABLE IF NOT EXISTS ${qualifiedTable} (${columns}); ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY; ${policySql} COMMIT;`;
7489
- const r = await execSql(sql);
7490
- text = r.ok ? `✅ Table '${schema}.${args.table}' created with RLS (${policyMode === "owner" ? "auth.uid() owner policy" : "deny-all by default"})` : `❌ Failed (${r.status}): ${JSON.stringify(r.data)}`;
7624
+ const r = await http.postReleaseMutation(`/v1/projects/${ref}/database/sql`, { sql, mode: "migration" });
7625
+ if (!r.ok)
7626
+ return releaseControlMutationFailure("database.create_table_rls", r);
7627
+ if (r.status !== 200) {
7628
+ return releaseControlFailure("database.create_table_rls", "OUTCOME_UNKNOWN", r.status);
7629
+ }
7630
+ if (!confirmedSqlBatchReceipt(r.data, rlsStatementCommands(policyMode))) {
7631
+ return releaseControlFailure("database.create_table_rls", "OUTCOME_UNKNOWN", r.status);
7632
+ }
7633
+ text = `✅ Table '${schema}.${args.table}' created with RLS (${policyMode === "owner" ? "auth.uid() owner policy" : "deny-all by default"})`;
7491
7634
  break;
7492
7635
  }
7493
7636
  default:
@@ -7978,38 +8121,6 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
7978
8121
  });
7979
8122
  }
7980
8123
 
7981
- // src/shared/tools/release-control-response.ts
7982
- var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
7983
- function releaseControlSuccess(operation, payload) {
7984
- return releaseControlResponse({
7985
- ...payload,
7986
- schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7987
- ok: true,
7988
- operation
7989
- });
7990
- }
7991
- function releaseControlFailure(operation, code, httpStatus, safeState = {}) {
7992
- return releaseControlErrorResponse({
7993
- ...safeState,
7994
- schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7995
- ok: false,
7996
- operation,
7997
- error: { code, http_status: httpStatus }
7998
- });
7999
- }
8000
- function releaseControlMutationFailure(operation, response) {
8001
- const outcomeUnknown = response.transportError || response.responseReadError || response.status === 408 || response.status >= 500;
8002
- return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status) : releaseControlFailure(operation, "HTTP_ERROR", response.status);
8003
- }
8004
- function releaseControlResponse(payload) {
8005
- return {
8006
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
8007
- };
8008
- }
8009
- function releaseControlErrorResponse(payload) {
8010
- return { ...releaseControlResponse(payload), isError: true };
8011
- }
8012
-
8013
8124
  // src/shared/tools/storage-tools.ts
8014
8125
  var MAX_BUCKET_ID_LENGTH = 100;
8015
8126
  var MAX_MIME_TYPE_COUNT = 100;
@@ -12047,7 +12158,7 @@ var MUTATION_TOOL_SCHEMA = {
12047
12158
  // package.json
12048
12159
  var package_default = {
12049
12160
  name: "@supacloud/cli",
12050
- version: "0.21.2",
12161
+ version: "0.22.0",
12051
12162
  description: "Project-scoped CLI for SupaCloud users",
12052
12163
  type: "module",
12053
12164
  main: "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.21.2",
3
+ "version": "0.22.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",