@supacloud/cli 0.21.3 → 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 +198 -79
  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,6 +7078,10 @@ function sortMigrationFiles(migrations) {
7046
7078
  }
7047
7079
  return sorted;
7048
7080
  }
7081
+ function normalizedMigrationStatement(statement) {
7082
+ return statement.replace(/\r\n?/g, `
7083
+ `).trim();
7084
+ }
7049
7085
  function migrationRows(data) {
7050
7086
  if (Array.isArray(data))
7051
7087
  return data;
@@ -7078,15 +7114,6 @@ function migrationIdentities(data) {
7078
7114
  }
7079
7115
  return identities;
7080
7116
  }
7081
- function appliedMigrationKeys(data) {
7082
- const keys = new Set;
7083
- for (const migration of migrationIdentities(data)) {
7084
- keys.add(migration.version);
7085
- if (migration.name !== null)
7086
- keys.add(migration.name);
7087
- }
7088
- return keys;
7089
- }
7090
7117
  function singleRemoteStatement(remote) {
7091
7118
  return Array.isArray(remote.statements) && remote.statements.length === 1 && typeof remote.statements[0] === "string" ? remote.statements[0] : null;
7092
7119
  }
@@ -7133,11 +7160,106 @@ function migrationPushPlan(data, migrationFiles) {
7133
7160
  }
7134
7161
  return plan;
7135
7162
  }
7136
- function isAlreadyAppliedMigrationResponse(response) {
7137
- if (response.status !== 409 || !response.data || typeof response.data !== "object")
7163
+ function recordPayload(payload) {
7164
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : null;
7165
+ }
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 });
7178
+ }
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))
7138
7183
  return false;
7139
7184
  const body = response.data;
7140
- return body.code === "409" && body.message === "Migration already applied";
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
+ });
7195
+ }
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);
7219
+ }
7220
+ return expected.size === receipt.already_applied ? { marked, concurrentlyAlreadyApplied: receipt.already_applied } : null;
7221
+ }
7222
+ function baselineInventoryIsApplied(payload, migrations) {
7223
+ const inventory = migrationInventory(payload);
7224
+ if (!inventory)
7225
+ return false;
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
+ };
7141
7263
  }
7142
7264
  function sqlReferencesVector(sql) {
7143
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);
@@ -7342,8 +7464,16 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7342
7464
  case "apply_migration": {
7343
7465
  if (!args.name || !args.sql)
7344
7466
  throw new Error("'name' and 'sql' required");
7345
- const r = await http.post(`/v1/projects/${ref}/database/migrations`, { name: args.name, sql: args.sql });
7346
- 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`;
7347
7477
  break;
7348
7478
  }
7349
7479
  case "push_migrations": {
@@ -7359,14 +7489,13 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7359
7489
  const migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
7360
7490
  const migrationsResult = await http.get(`/v1/projects/${ref}/database/migrations`);
7361
7491
  if (!migrationsResult.ok) {
7362
- text = `❌ Failed to load applied migrations (${migrationsResult.status})`;
7363
- break;
7492
+ return releaseControlFailure("database.push_migrations", "HTTP_ERROR", migrationsResult.transportError ? null : migrationsResult.status);
7364
7493
  }
7365
7494
  const migrationPlan = migrationPushPlan(migrationsResult.data, migrationFiles);
7366
7495
  if (args.dry_run) {
7367
- const pending = migrationPlan.pending;
7496
+ const pending2 = migrationPlan.pending;
7368
7497
  const alreadyApplied = migrationPlan.alreadyApplied;
7369
- const pendingWithSql = pending;
7498
+ const pendingWithSql = pending2;
7370
7499
  let vectorEnabled = null;
7371
7500
  if (pendingWithSql.some(({ sql }) => sqlReferencesVector(sql) || sqlCreatesVectorExtension(sql))) {
7372
7501
  const extResult = await execSql("SELECT extname AS name FROM pg_extension WHERE extname = 'vector';");
@@ -7378,7 +7507,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7378
7507
  `Total: ${migrationFiles.length}`,
7379
7508
  "",
7380
7509
  "Pending:",
7381
- ...pending.length ? pending.map(({ file, version }) => ` - ${file} (${version})`) : [" - none"],
7510
+ ...pending2.length ? pending2.map(({ file, version }) => ` - ${file} (${version})`) : [" - none"],
7382
7511
  "",
7383
7512
  "Already applied:",
7384
7513
  ...alreadyApplied.length ? alreadyApplied.map(({ file, version }) => ` - ${file} (${version})`) : [" - none"],
@@ -7387,22 +7516,25 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7387
7516
  `);
7388
7517
  break;
7389
7518
  }
7519
+ const pending = new Set(migrationPlan.pending.map(({ file }) => file));
7390
7520
  const applied = [];
7391
- const skipped = migrationPlan.alreadyApplied.map(({ file }) => file);
7392
- for (const { file, name, version, sql } of migrationPlan.pending) {
7393
- const r = await http.post(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
7394
- if (r.ok) {
7521
+ const skipped = [];
7522
+ for (const { file, name, version, sql } of migrationFiles) {
7523
+ if (!pending.has(file)) {
7524
+ skipped.push(file);
7525
+ continue;
7526
+ }
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 })) {
7395
7529
  applied.push(file);
7396
- } else if (isAlreadyAppliedMigrationResponse(r)) {
7530
+ } else if (isAlreadyAppliedMigrationResponse(r, { name, version, sql })) {
7397
7531
  skipped.push(file);
7398
7532
  } else {
7399
- text = [
7400
- `❌ Failed to apply ${file} (${r.status})`,
7401
- `Applied before failure: ${applied.length}`,
7402
- `Skipped before failure: ${skipped.length}`
7403
- ].join(`
7404
- `);
7405
- 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);
7406
7538
  }
7407
7539
  }
7408
7540
  text = [
@@ -7428,12 +7560,11 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7428
7560
  const migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
7429
7561
  const r = await http.get(`/v1/projects/${ref}/database/migrations`);
7430
7562
  if (!r.ok) {
7431
- text = `❌ Failed to load applied migrations (${r.status}): ${JSON.stringify(r.data)}`;
7432
- break;
7563
+ return releaseControlFailure("database.baseline_migrations", "HTTP_ERROR", r.transportError ? null : r.status);
7433
7564
  }
7434
- const appliedKeys = appliedMigrationKeys(r.data);
7435
- const missing = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
7436
- 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;
7437
7568
  if (args.dry_run) {
7438
7569
  text = [
7439
7570
  `Migration baseline dry run for ${dir}`,
@@ -7456,16 +7587,28 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7456
7587
  `);
7457
7588
  break;
7458
7589
  }
7459
- const baselineResult = await http.post(`/v1/projects/${ref}/database/migrations/baseline`, { migrations: missing.map(({ name, version }) => ({ name, version })) });
7460
- 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 = [
7461
7606
  `✅ Migration baseline completed for ${dir}`,
7462
- `Marked applied: ${missing.length}`,
7463
- `Already applied: ${alreadyApplied.length}`,
7464
- "",
7465
- "Marked files:",
7466
- ...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})`)] : []
7467
7610
  ].join(`
7468
- `) : `❌ Failed (${baselineResult.status}): ${JSON.stringify(baselineResult.data)}`;
7611
+ `);
7469
7612
  break;
7470
7613
  }
7471
7614
  case "create_table_rls": {
@@ -7478,8 +7621,16 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7478
7621
  throw new Error("Invalid RLS policy mode");
7479
7622
  const policySql = buildRlsPolicySql(qualifiedTable, policyMode, args.owner_column);
7480
7623
  const sql = `BEGIN; CREATE TABLE IF NOT EXISTS ${qualifiedTable} (${columns}); ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY; ${policySql} COMMIT;`;
7481
- const r = await execSql(sql);
7482
- 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"})`;
7483
7634
  break;
7484
7635
  }
7485
7636
  default:
@@ -7970,38 +8121,6 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
7970
8121
  });
7971
8122
  }
7972
8123
 
7973
- // src/shared/tools/release-control-response.ts
7974
- var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
7975
- function releaseControlSuccess(operation, payload) {
7976
- return releaseControlResponse({
7977
- ...payload,
7978
- schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7979
- ok: true,
7980
- operation
7981
- });
7982
- }
7983
- function releaseControlFailure(operation, code, httpStatus, safeState = {}) {
7984
- return releaseControlErrorResponse({
7985
- ...safeState,
7986
- schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7987
- ok: false,
7988
- operation,
7989
- error: { code, http_status: httpStatus }
7990
- });
7991
- }
7992
- function releaseControlMutationFailure(operation, response) {
7993
- const outcomeUnknown = response.transportError || response.responseReadError || response.status === 408 || response.status >= 500;
7994
- return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status) : releaseControlFailure(operation, "HTTP_ERROR", response.status);
7995
- }
7996
- function releaseControlResponse(payload) {
7997
- return {
7998
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
7999
- };
8000
- }
8001
- function releaseControlErrorResponse(payload) {
8002
- return { ...releaseControlResponse(payload), isError: true };
8003
- }
8004
-
8005
8124
  // src/shared/tools/storage-tools.ts
8006
8125
  var MAX_BUCKET_ID_LENGTH = 100;
8007
8126
  var MAX_MIME_TYPE_COUNT = 100;
@@ -12039,7 +12158,7 @@ var MUTATION_TOOL_SCHEMA = {
12039
12158
  // package.json
12040
12159
  var package_default = {
12041
12160
  name: "@supacloud/cli",
12042
- version: "0.21.3",
12161
+ version: "0.22.0",
12043
12162
  description: "Project-scoped CLI for SupaCloud users",
12044
12163
  type: "module",
12045
12164
  main: "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.21.3",
3
+ "version": "0.22.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",