@supacloud/cli 0.11.0 → 0.12.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 +12 -0
  2. package/dist/index.js +178 -5
  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)";
@@ -7771,7 +7772,7 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
7771
7772
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
7772
7773
  import { basename as basename3 } from "node:path";
7773
7774
  function registerFrontendTools(server, http) {
7774
- server.tool("frontend", `Frontend hosting (static sites & SSR). Supports: static, react, vue, svelte, sveltekit, nextjs, nuxt, astro.
7775
+ server.tool("frontend", `Frontend hosting (static sites & SSR). Supports: static, react, vue, svelte, sveltekit, sveltekit-static, nextjs, nuxt, astro.
7775
7776
  Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy, build_logs, add_domain, remove_domain, set_env, list_frameworks, list_records`, {
7776
7777
  action: withDescription(stringEnum([
7777
7778
  "list",
@@ -7792,18 +7793,19 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
7792
7793
  ref: optional(Type.String(), "Project ref"),
7793
7794
  id: optional(Type.String(), "Deployment ID"),
7794
7795
  name: optional(Type.String(), "[create] Deployment name"),
7795
- framework: optional(Type.String(), "[create] Framework (static|react|vue|svelte|sveltekit|nextjs|nuxt|astro)"),
7796
+ framework: optional(Type.String(), "[create] Framework (static|react|vue|svelte|sveltekit|sveltekit-static|nextjs|nuxt|astro)"),
7796
7797
  domain: optional(Type.String(), "[create/update/add_domain/remove_domain] Custom domain"),
7797
7798
  build_command: optional(Type.String(), "[create/update] Build command override"),
7798
7799
  output_dir: optional(Type.String(), "[create/update] Output directory override"),
7799
7800
  install_command: optional(Type.String(), "[create/update] Install command override"),
7800
7801
  node_version: optional(Type.String(), "[create/update] Node.js version"),
7802
+ health_check_path: optional(Type.String(), "[create/update] SSR readiness path (default: /)"),
7801
7803
  env_vars: optional(Type.Record(Type.String(), Type.String()), "[create/update/set_env] Environment variables"),
7802
7804
  git_url: optional(Type.String(), "[deploy_git] Git repository URL"),
7803
7805
  branch: optional(Type.String(), "[deploy_git] Branch (default: main)"),
7804
7806
  zip_path: optional(Type.String(), "[deploy_upload] Local zip file path")
7805
7807
  }, 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;
7808
+ 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
7809
  const need = (f, v) => {
7808
7810
  if (!v)
7809
7811
  throw new Error(`'${f}' required for '${action}'`);
@@ -7832,6 +7834,7 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
7832
7834
  output_dir,
7833
7835
  install_command,
7834
7836
  node_version,
7837
+ health_check_path,
7835
7838
  env_vars
7836
7839
  }));
7837
7840
  break;
@@ -7845,6 +7848,7 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
7845
7848
  output_dir,
7846
7849
  install_command,
7847
7850
  node_version,
7851
+ health_check_path,
7848
7852
  env_vars
7849
7853
  }));
7850
7854
  break;
@@ -8741,6 +8745,161 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
8741
8745
  });
8742
8746
  }
8743
8747
 
8748
+ // src/shared/tools/branch-tools.ts
8749
+ function resolveProjectRef(ref, projectRef) {
8750
+ const resolved = typeof ref === "string" && ref.trim() ? ref.trim() : projectRef || "";
8751
+ if (!resolved)
8752
+ throw new Error("'ref' is required for this action");
8753
+ return resolved;
8754
+ }
8755
+ function requireString(value, field) {
8756
+ if (typeof value !== "string" || !value.trim())
8757
+ throw new Error(`'${field}' is required`);
8758
+ return value.trim();
8759
+ }
8760
+ function responseError(result) {
8761
+ if (result.data && typeof result.data === "object") {
8762
+ const data = result.data;
8763
+ const message = data.error || data.message;
8764
+ if (typeof message === "string") {
8765
+ const details = [message];
8766
+ if (Array.isArray(data.applied) && data.applied.length > 0) {
8767
+ const versions = data.applied.map((entry) => entry && typeof entry === "object" ? entry.version : null).filter((version) => typeof version === "string");
8768
+ details.push(`Applied before failure: ${versions.join(", ") || data.applied.length}`);
8769
+ details.push("Fetch a fresh promotion_plan before retrying.");
8770
+ }
8771
+ if (data.replacement_committed === true) {
8772
+ details.push(`Database replacement committed; recovery is required${typeof data.backup_database === "string" ? ` using backup ${data.backup_database}` : ""}.`);
8773
+ }
8774
+ return details.join(`
8775
+ `);
8776
+ }
8777
+ }
8778
+ return `Request failed with HTTP ${result.status}`;
8779
+ }
8780
+ function isPromotionPlan(candidate) {
8781
+ if (!candidate || typeof candidate !== "object")
8782
+ return false;
8783
+ const plan = candidate;
8784
+ 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";
8785
+ }
8786
+ function projectPath(ref) {
8787
+ return `/v1/projects/${encodeURIComponent(ref)}/branches`;
8788
+ }
8789
+ function branchPath(ref, branchRef) {
8790
+ return `${projectPath(ref)}/${encodeURIComponent(branchRef)}`;
8791
+ }
8792
+ function formatPromotionPlan(plan) {
8793
+ const lines = [
8794
+ `Migration promotion plan: ${plan.safe_to_apply ? "READY" : "BLOCKED"}`,
8795
+ `Plan checksum: ${plan.plan_checksum}`,
8796
+ `Pending: ${plan.pending.length}`,
8797
+ `Already applied: ${plan.applied.length}`,
8798
+ `Blocked: ${plan.blocked.length}`,
8799
+ "Branch data will not be automatically copied to the parent project."
8800
+ ];
8801
+ if (plan.pending.length > 0) {
8802
+ lines.push("", "Pending migrations:");
8803
+ for (const migration of plan.pending) {
8804
+ lines.push(` - ${migration.version} ${migration.name || "(unnamed)"}` + ` checksum=${migration.checksum.slice(0, 12)}` + `${migration.destructive ? " destructive" : ""}`);
8805
+ }
8806
+ }
8807
+ if (plan.blocked.length > 0) {
8808
+ lines.push("", "Blocking findings:");
8809
+ for (const blocked of plan.blocked)
8810
+ lines.push(` - [${blocked.code}] ${blocked.message}`);
8811
+ }
8812
+ if (plan.warnings.length > 0) {
8813
+ lines.push("", "Warnings:", ...plan.warnings.map((warning) => ` - ${warning}`));
8814
+ }
8815
+ if (plan.requires_destructive_confirmation) {
8816
+ lines.push("", "Re-run promote with --confirm_destructive true after reviewing destructive SQL.");
8817
+ }
8818
+ return lines.join(`
8819
+ `);
8820
+ }
8821
+ function readOnlyResult() {
8822
+ return {
8823
+ isError: true,
8824
+ content: [{ type: "text", text: "⚠️ Branch write blocked in read-only mode." }]
8825
+ };
8826
+ }
8827
+ function formatPromotionResult(responseValue) {
8828
+ if (!responseValue || typeof responseValue !== "object")
8829
+ return "Migrations promoted.";
8830
+ const response = responseValue;
8831
+ const applied = Array.isArray(response.applied) ? response.applied : [];
8832
+ const versions = applied.map((entry) => entry && typeof entry === "object" ? entry.version : null).filter((version) => typeof version === "string");
8833
+ return [
8834
+ `Migration promotion completed: ${applied.length} applied.`,
8835
+ ...versions.length > 0 ? [`Versions: ${versions.join(", ")}`] : [],
8836
+ "Branch data was not automatically copied to the parent project."
8837
+ ].join(`
8838
+ `);
8839
+ }
8840
+ function registerBranchTools(server, http, options = {}) {
8841
+ server.tool("branch", "Preview branch lifecycle and safe migration promotion. Whole-database replacement is intentionally not exposed by this project CLI.", {
8842
+ action: withDescription(stringEnum([
8843
+ "list",
8844
+ "create",
8845
+ "delete",
8846
+ "promotion_plan",
8847
+ "promote"
8848
+ ]), "Action to perform"),
8849
+ ref: optional(Type.String(), "[*] Optional parent project override"),
8850
+ branch_ref: optional(Type.String(), "[delete/promotion_plan/promote] Preview branch ref"),
8851
+ name: optional(Type.String(), "[create] Branch name"),
8852
+ data_mode: optional(stringEnum(["schema_only", "full_clone"]), "[create] Preview data mode (default: schema_only)"),
8853
+ plan_checksum: optional(Type.String(), "[promote] Reviewed plan checksum from promotion_plan"),
8854
+ confirm_destructive: optional(Type.Boolean(), "[promote] Confirm reviewed destructive migrations")
8855
+ }, async (args) => {
8856
+ const action = requireString(args.action, "action");
8857
+ const ref = resolveProjectRef(args.ref, options.projectRef);
8858
+ const writeAction = action === "create" || action === "delete" || action === "promote";
8859
+ if (writeAction && options.readOnly)
8860
+ return readOnlyResult();
8861
+ let result;
8862
+ if (action === "list") {
8863
+ result = await http.get(projectPath(ref));
8864
+ } else if (action === "create") {
8865
+ const name = requireString(args.name, "name");
8866
+ result = await http.post(projectPath(ref), {
8867
+ name,
8868
+ data_mode: args.data_mode === "full_clone" ? "full_clone" : "schema_only"
8869
+ });
8870
+ } else if (action === "delete") {
8871
+ const branchRef = requireString(args.branch_ref, "branch_ref");
8872
+ result = await http.delete(branchPath(ref, branchRef));
8873
+ } else if (action === "promotion_plan") {
8874
+ const branchRef = requireString(args.branch_ref, "branch_ref");
8875
+ result = await http.get(`${branchPath(ref, branchRef)}/promote/plan`);
8876
+ if (result.ok && isPromotionPlan(result.data)) {
8877
+ return { content: [{ type: "text", text: formatPromotionPlan(result.data) }] };
8878
+ }
8879
+ } else if (action === "promote") {
8880
+ const branchRef = requireString(args.branch_ref, "branch_ref");
8881
+ const planChecksum = requireString(args.plan_checksum, "plan_checksum");
8882
+ result = await http.post(`${branchPath(ref, branchRef)}/promote`, {
8883
+ mode: "migrations",
8884
+ plan_checksum: planChecksum,
8885
+ confirm_destructive: args.confirm_destructive === true
8886
+ });
8887
+ } else {
8888
+ throw new Error(`Unknown branch action: ${action}`);
8889
+ }
8890
+ if (!result.ok) {
8891
+ return {
8892
+ isError: true,
8893
+ content: [{ type: "text", text: `❌ ${responseError(result)}` }]
8894
+ };
8895
+ }
8896
+ if (action === "promote") {
8897
+ return { content: [{ type: "text", text: formatPromotionResult(result.data) }] };
8898
+ }
8899
+ return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
8900
+ });
8901
+ }
8902
+
8744
8903
  // src/shared/tools/supabase-cli-tools.ts
8745
8904
  import { spawn } from "node:child_process";
8746
8905
  import { chmodSync, existsSync as existsSync5, mkdirSync, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
@@ -9461,6 +9620,9 @@ EXAMPLES
9461
9620
  ${preferredCommand} supabase db_diff --schema public --name add_accounts
9462
9621
  ${preferredCommand} supabase push --ref abc123 --dir supabase/migrations --dry_run
9463
9622
  ${preferredCommand} supabase db_dump --db_url "postgresql://..." --file backups/schema.sql
9623
+ ${preferredCommand} branch create --name feature-auth --data_mode schema_only
9624
+ ${preferredCommand} branch promotion_plan --branch_ref preview123
9625
+ ${preferredCommand} branch promote --branch_ref preview123 --plan_checksum <sha256>
9464
9626
  ${preferredCommand} ai show_skill
9465
9627
  ${preferredCommand} ai install_skill --dry_run
9466
9628
  ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello
@@ -9517,7 +9679,7 @@ function createCliTools() {
9517
9679
  ]
9518
9680
  })
9519
9681
  };
9520
- for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "diagnostics", "gateway"]) {
9682
+ for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "diagnostics", "gateway", "branch"]) {
9521
9683
  tools[name] = {
9522
9684
  schema: { action: genericActionSchema },
9523
9685
  callback: async () => ({
@@ -9531,6 +9693,13 @@ function createCliTools() {
9531
9693
  })
9532
9694
  };
9533
9695
  }
9696
+ const branchContextCallback = tools.branch.callback;
9697
+ const branchHelpTool = captureTools((server) => registerBranchTools(server, {}, {
9698
+ readOnly: true
9699
+ })).branch;
9700
+ if (branchHelpTool) {
9701
+ tools.branch = { schema: branchHelpTool.schema, callback: branchContextCallback };
9702
+ }
9534
9703
  };
9535
9704
  if (!context.apiUrl || !context.apiToken) {
9536
9705
  registerContextAwareHelp();
@@ -9586,6 +9755,10 @@ function createCliTools() {
9586
9755
  assign(captureTools((server) => registerGatewayTools(server, http, {
9587
9756
  projectRef: context.projectRef || undefined
9588
9757
  })));
9758
+ assign(captureTools((server) => registerBranchTools(server, http, {
9759
+ projectRef: context.projectRef || undefined,
9760
+ readOnly: context.readOnly
9761
+ })));
9589
9762
  assign(captureTools((server) => registerQueueTools(server, http, {
9590
9763
  projectRef: context.projectRef || undefined
9591
9764
  })));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",