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