@supacloud/cli 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/dist/index.js +185 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,8 +65,20 @@ supacloud-cli supabase migration_new --name add_accounts
65
65
  supacloud-cli supabase db_diff --schema public --name add_accounts
66
66
  supacloud-cli supabase push --ref abc123 --dir supabase/migrations --dry_run
67
67
  supacloud-cli frontend list --ref abc123
68
+ supacloud-cli branch create --name feature-orders --data_mode schema_only
69
+ supacloud-cli branch promotion_plan --branch_ref preview123
70
+ supacloud-cli branch promote --branch_ref preview123 --plan_checksum <sha256>
68
71
  ```
69
72
 
73
+ Branch promotion is migration-first. `branch promotion_plan` prints pending
74
+ versions, names, statement counts, and checksums without echoing SQL into terminal
75
+ logs; review the migration files or the Web Console SQL view before approval.
76
+ `branch promote` requires the reviewed checksum, executes with the project-scoped
77
+ database role, and does not automatically copy branch data.
78
+ Use `--data_mode full_clone` only for an explicitly approved non-sensitive or
79
+ masked debugging dataset. Whole-database replacement is an administrator-only
80
+ break-glass API mode and is intentionally not exposed by this project CLI.
81
+
70
82
  ## Official Supabase CLI adapter
71
83
 
72
84
  The `supabase` command group is a thin, allowlisted adapter around the official
package/dist/index.js CHANGED
@@ -6131,7 +6131,8 @@ async function runCli(cliTools, args, options = {}) {
6131
6131
  const otherFields = Object.entries(shape).filter(([name]) => name !== "action");
6132
6132
  const relevantFields = otherFields.filter(([, field]) => {
6133
6133
  const description = getDescription(field);
6134
- return description.includes(`[${action}]`) || description.includes("[*]");
6134
+ const scopes = [...description.matchAll(/\[([^\]]+)\]/g)].flatMap((match) => match[1]?.split("/") || []);
6135
+ return scopes.includes(action) || scopes.includes("*");
6135
6136
  });
6136
6137
  const argLines = relevantFields.length ? relevantFields.map(([name, field]) => ` --${name} ${getDescription(field) || "(no description)"}`).join(`
6137
6138
  `) : " (no documented action-specific flags)";
@@ -6476,6 +6477,12 @@ function appliedMigrationKeys(data) {
6476
6477
  }
6477
6478
  return keys;
6478
6479
  }
6480
+ function isAlreadyAppliedMigrationResponse(response) {
6481
+ if (response.status !== 409 || !response.data || typeof response.data !== "object")
6482
+ return false;
6483
+ const body = response.data;
6484
+ return body.code === "409" && body.message === "Migration already applied";
6485
+ }
6479
6486
  function sqlReferencesVector(sql) {
6480
6487
  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);
6481
6488
  }
@@ -6733,7 +6740,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
6733
6740
  const r = await http.post(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
6734
6741
  if (r.ok) {
6735
6742
  applied.push(file);
6736
- } else if (r.status === 409) {
6743
+ } else if (isAlreadyAppliedMigrationResponse(r)) {
6737
6744
  skipped.push(file);
6738
6745
  } else {
6739
6746
  text = [
@@ -7771,7 +7778,7 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
7771
7778
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
7772
7779
  import { basename as basename3 } from "node:path";
7773
7780
  function registerFrontendTools(server, http) {
7774
- server.tool("frontend", `Frontend hosting (static sites & SSR). Supports: static, react, vue, svelte, sveltekit, nextjs, nuxt, astro.
7781
+ server.tool("frontend", `Frontend hosting (static sites & SSR). Supports: static, react, vue, svelte, sveltekit, sveltekit-static, nextjs, nuxt, astro.
7775
7782
  Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy, build_logs, add_domain, remove_domain, set_env, list_frameworks, list_records`, {
7776
7783
  action: withDescription(stringEnum([
7777
7784
  "list",
@@ -7792,18 +7799,19 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
7792
7799
  ref: optional(Type.String(), "Project ref"),
7793
7800
  id: optional(Type.String(), "Deployment ID"),
7794
7801
  name: optional(Type.String(), "[create] Deployment name"),
7795
- framework: optional(Type.String(), "[create] Framework (static|react|vue|svelte|sveltekit|nextjs|nuxt|astro)"),
7802
+ framework: optional(Type.String(), "[create] Framework (static|react|vue|svelte|sveltekit|sveltekit-static|nextjs|nuxt|astro)"),
7796
7803
  domain: optional(Type.String(), "[create/update/add_domain/remove_domain] Custom domain"),
7797
7804
  build_command: optional(Type.String(), "[create/update] Build command override"),
7798
7805
  output_dir: optional(Type.String(), "[create/update] Output directory override"),
7799
7806
  install_command: optional(Type.String(), "[create/update] Install command override"),
7800
7807
  node_version: optional(Type.String(), "[create/update] Node.js version"),
7808
+ health_check_path: optional(Type.String(), "[create/update] SSR readiness path (default: /)"),
7801
7809
  env_vars: optional(Type.Record(Type.String(), Type.String()), "[create/update/set_env] Environment variables"),
7802
7810
  git_url: optional(Type.String(), "[deploy_git] Git repository URL"),
7803
7811
  branch: optional(Type.String(), "[deploy_git] Branch (default: main)"),
7804
7812
  zip_path: optional(Type.String(), "[deploy_upload] Local zip file path")
7805
7813
  }, async (args) => {
7806
- const { action, ref, id, name, framework, domain, build_command, output_dir, install_command, node_version, env_vars, git_url, branch, zip_path } = args;
7814
+ const { action, ref, id, name, framework, domain, build_command, output_dir, install_command, node_version, health_check_path, env_vars, git_url, branch, zip_path } = args;
7807
7815
  const need = (f, v) => {
7808
7816
  if (!v)
7809
7817
  throw new Error(`'${f}' required for '${action}'`);
@@ -7832,6 +7840,7 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
7832
7840
  output_dir,
7833
7841
  install_command,
7834
7842
  node_version,
7843
+ health_check_path,
7835
7844
  env_vars
7836
7845
  }));
7837
7846
  break;
@@ -7845,6 +7854,7 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
7845
7854
  output_dir,
7846
7855
  install_command,
7847
7856
  node_version,
7857
+ health_check_path,
7848
7858
  env_vars
7849
7859
  }));
7850
7860
  break;
@@ -8741,6 +8751,161 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
8741
8751
  });
8742
8752
  }
8743
8753
 
8754
+ // src/shared/tools/branch-tools.ts
8755
+ function resolveProjectRef(ref, projectRef) {
8756
+ const resolved = typeof ref === "string" && ref.trim() ? ref.trim() : projectRef || "";
8757
+ if (!resolved)
8758
+ throw new Error("'ref' is required for this action");
8759
+ return resolved;
8760
+ }
8761
+ function requireString(value, field) {
8762
+ if (typeof value !== "string" || !value.trim())
8763
+ throw new Error(`'${field}' is required`);
8764
+ return value.trim();
8765
+ }
8766
+ function responseError(result) {
8767
+ if (result.data && typeof result.data === "object") {
8768
+ const data = result.data;
8769
+ const message = data.error || data.message;
8770
+ if (typeof message === "string") {
8771
+ const details = [message];
8772
+ if (Array.isArray(data.applied) && data.applied.length > 0) {
8773
+ const versions = data.applied.map((entry) => entry && typeof entry === "object" ? entry.version : null).filter((version) => typeof version === "string");
8774
+ details.push(`Applied before failure: ${versions.join(", ") || data.applied.length}`);
8775
+ details.push("Fetch a fresh promotion_plan before retrying.");
8776
+ }
8777
+ if (data.replacement_committed === true) {
8778
+ details.push(`Database replacement committed; recovery is required${typeof data.backup_database === "string" ? ` using backup ${data.backup_database}` : ""}.`);
8779
+ }
8780
+ return details.join(`
8781
+ `);
8782
+ }
8783
+ }
8784
+ return `Request failed with HTTP ${result.status}`;
8785
+ }
8786
+ function isPromotionPlan(candidate) {
8787
+ if (!candidate || typeof candidate !== "object")
8788
+ return false;
8789
+ const plan = candidate;
8790
+ return plan.mode === "migrations" && typeof plan.safe_to_apply === "boolean" && typeof plan.plan_checksum === "string" && Array.isArray(plan.pending) && Array.isArray(plan.applied) && Array.isArray(plan.blocked) && Array.isArray(plan.warnings) && typeof plan.requires_destructive_confirmation === "boolean";
8791
+ }
8792
+ function projectPath(ref) {
8793
+ return `/v1/projects/${encodeURIComponent(ref)}/branches`;
8794
+ }
8795
+ function branchPath(ref, branchRef) {
8796
+ return `${projectPath(ref)}/${encodeURIComponent(branchRef)}`;
8797
+ }
8798
+ function formatPromotionPlan(plan) {
8799
+ const lines = [
8800
+ `Migration promotion plan: ${plan.safe_to_apply ? "READY" : "BLOCKED"}`,
8801
+ `Plan checksum: ${plan.plan_checksum}`,
8802
+ `Pending: ${plan.pending.length}`,
8803
+ `Already applied: ${plan.applied.length}`,
8804
+ `Blocked: ${plan.blocked.length}`,
8805
+ "Branch data will not be automatically copied to the parent project."
8806
+ ];
8807
+ if (plan.pending.length > 0) {
8808
+ lines.push("", "Pending migrations:");
8809
+ for (const migration of plan.pending) {
8810
+ lines.push(` - ${migration.version} ${migration.name || "(unnamed)"}` + ` checksum=${migration.checksum.slice(0, 12)}` + `${migration.destructive ? " destructive" : ""}`);
8811
+ }
8812
+ }
8813
+ if (plan.blocked.length > 0) {
8814
+ lines.push("", "Blocking findings:");
8815
+ for (const blocked of plan.blocked)
8816
+ lines.push(` - [${blocked.code}] ${blocked.message}`);
8817
+ }
8818
+ if (plan.warnings.length > 0) {
8819
+ lines.push("", "Warnings:", ...plan.warnings.map((warning) => ` - ${warning}`));
8820
+ }
8821
+ if (plan.requires_destructive_confirmation) {
8822
+ lines.push("", "Re-run promote with --confirm_destructive true after reviewing destructive SQL.");
8823
+ }
8824
+ return lines.join(`
8825
+ `);
8826
+ }
8827
+ function readOnlyResult() {
8828
+ return {
8829
+ isError: true,
8830
+ content: [{ type: "text", text: "⚠️ Branch write blocked in read-only mode." }]
8831
+ };
8832
+ }
8833
+ function formatPromotionResult(responseValue) {
8834
+ if (!responseValue || typeof responseValue !== "object")
8835
+ return "Migrations promoted.";
8836
+ const response = responseValue;
8837
+ const applied = Array.isArray(response.applied) ? response.applied : [];
8838
+ const versions = applied.map((entry) => entry && typeof entry === "object" ? entry.version : null).filter((version) => typeof version === "string");
8839
+ return [
8840
+ `Migration promotion completed: ${applied.length} applied.`,
8841
+ ...versions.length > 0 ? [`Versions: ${versions.join(", ")}`] : [],
8842
+ "Branch data was not automatically copied to the parent project."
8843
+ ].join(`
8844
+ `);
8845
+ }
8846
+ function registerBranchTools(server, http, options = {}) {
8847
+ server.tool("branch", "Preview branch lifecycle and safe migration promotion. Whole-database replacement is intentionally not exposed by this project CLI.", {
8848
+ action: withDescription(stringEnum([
8849
+ "list",
8850
+ "create",
8851
+ "delete",
8852
+ "promotion_plan",
8853
+ "promote"
8854
+ ]), "Action to perform"),
8855
+ ref: optional(Type.String(), "[*] Optional parent project override"),
8856
+ branch_ref: optional(Type.String(), "[delete/promotion_plan/promote] Preview branch ref"),
8857
+ name: optional(Type.String(), "[create] Branch name"),
8858
+ data_mode: optional(stringEnum(["schema_only", "full_clone"]), "[create] Preview data mode (default: schema_only)"),
8859
+ plan_checksum: optional(Type.String(), "[promote] Reviewed plan checksum from promotion_plan"),
8860
+ confirm_destructive: optional(Type.Boolean(), "[promote] Confirm reviewed destructive migrations")
8861
+ }, async (args) => {
8862
+ const action = requireString(args.action, "action");
8863
+ const ref = resolveProjectRef(args.ref, options.projectRef);
8864
+ const writeAction = action === "create" || action === "delete" || action === "promote";
8865
+ if (writeAction && options.readOnly)
8866
+ return readOnlyResult();
8867
+ let result;
8868
+ if (action === "list") {
8869
+ result = await http.get(projectPath(ref));
8870
+ } else if (action === "create") {
8871
+ const name = requireString(args.name, "name");
8872
+ result = await http.post(projectPath(ref), {
8873
+ name,
8874
+ data_mode: args.data_mode === "full_clone" ? "full_clone" : "schema_only"
8875
+ });
8876
+ } else if (action === "delete") {
8877
+ const branchRef = requireString(args.branch_ref, "branch_ref");
8878
+ result = await http.delete(branchPath(ref, branchRef));
8879
+ } else if (action === "promotion_plan") {
8880
+ const branchRef = requireString(args.branch_ref, "branch_ref");
8881
+ result = await http.get(`${branchPath(ref, branchRef)}/promote/plan`);
8882
+ if (result.ok && isPromotionPlan(result.data)) {
8883
+ return { content: [{ type: "text", text: formatPromotionPlan(result.data) }] };
8884
+ }
8885
+ } else if (action === "promote") {
8886
+ const branchRef = requireString(args.branch_ref, "branch_ref");
8887
+ const planChecksum = requireString(args.plan_checksum, "plan_checksum");
8888
+ result = await http.post(`${branchPath(ref, branchRef)}/promote`, {
8889
+ mode: "migrations",
8890
+ plan_checksum: planChecksum,
8891
+ confirm_destructive: args.confirm_destructive === true
8892
+ });
8893
+ } else {
8894
+ throw new Error(`Unknown branch action: ${action}`);
8895
+ }
8896
+ if (!result.ok) {
8897
+ return {
8898
+ isError: true,
8899
+ content: [{ type: "text", text: `❌ ${responseError(result)}` }]
8900
+ };
8901
+ }
8902
+ if (action === "promote") {
8903
+ return { content: [{ type: "text", text: formatPromotionResult(result.data) }] };
8904
+ }
8905
+ return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
8906
+ });
8907
+ }
8908
+
8744
8909
  // src/shared/tools/supabase-cli-tools.ts
8745
8910
  import { spawn } from "node:child_process";
8746
8911
  import { chmodSync, existsSync as existsSync5, mkdirSync, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
@@ -9461,6 +9626,9 @@ EXAMPLES
9461
9626
  ${preferredCommand} supabase db_diff --schema public --name add_accounts
9462
9627
  ${preferredCommand} supabase push --ref abc123 --dir supabase/migrations --dry_run
9463
9628
  ${preferredCommand} supabase db_dump --db_url "postgresql://..." --file backups/schema.sql
9629
+ ${preferredCommand} branch create --name feature-auth --data_mode schema_only
9630
+ ${preferredCommand} branch promotion_plan --branch_ref preview123
9631
+ ${preferredCommand} branch promote --branch_ref preview123 --plan_checksum <sha256>
9464
9632
  ${preferredCommand} ai show_skill
9465
9633
  ${preferredCommand} ai install_skill --dry_run
9466
9634
  ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello
@@ -9517,7 +9685,7 @@ function createCliTools() {
9517
9685
  ]
9518
9686
  })
9519
9687
  };
9520
- for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "diagnostics", "gateway"]) {
9688
+ for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "diagnostics", "gateway", "branch"]) {
9521
9689
  tools[name] = {
9522
9690
  schema: { action: genericActionSchema },
9523
9691
  callback: async () => ({
@@ -9531,6 +9699,13 @@ function createCliTools() {
9531
9699
  })
9532
9700
  };
9533
9701
  }
9702
+ const branchContextCallback = tools.branch.callback;
9703
+ const branchHelpTool = captureTools((server) => registerBranchTools(server, {}, {
9704
+ readOnly: true
9705
+ })).branch;
9706
+ if (branchHelpTool) {
9707
+ tools.branch = { schema: branchHelpTool.schema, callback: branchContextCallback };
9708
+ }
9534
9709
  };
9535
9710
  if (!context.apiUrl || !context.apiToken) {
9536
9711
  registerContextAwareHelp();
@@ -9586,6 +9761,10 @@ function createCliTools() {
9586
9761
  assign(captureTools((server) => registerGatewayTools(server, http, {
9587
9762
  projectRef: context.projectRef || undefined
9588
9763
  })));
9764
+ assign(captureTools((server) => registerBranchTools(server, http, {
9765
+ projectRef: context.projectRef || undefined,
9766
+ readOnly: context.readOnly
9767
+ })));
9589
9768
  assign(captureTools((server) => registerQueueTools(server, http, {
9590
9769
  projectRef: context.projectRef || undefined
9591
9770
  })));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",