@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.
- package/README.md +12 -0
- package/dist/index.js +185 -6
- 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
|
-
|
|
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
|
|
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
|
})));
|