@supacloud/cli 0.1.0 → 0.2.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 (2) hide show
  1. package/dist/index.js +222 -55
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
2
  var __defProp = Object.defineProperty;
4
3
  var __returnValue = (v) => v;
5
4
  function __exportSetter(name, newValue) {
@@ -14,7 +13,6 @@ var __export = (target, all) => {
14
13
  set: __exportSetter.bind(all, name)
15
14
  });
16
15
  };
17
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
16
 
19
17
  // node_modules/zod/v3/external.js
20
18
  var exports_external = {};
@@ -4347,6 +4345,8 @@ class HttpTransport {
4347
4345
  }
4348
4346
 
4349
4347
  // src/shared/tools/database-tools.ts
4348
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
4349
+ import { basename, join } from "node:path";
4350
4350
  function normalizeSqlResponse(result) {
4351
4351
  if (!result.ok)
4352
4352
  return result;
@@ -4362,6 +4362,26 @@ function normalizeSqlResponse(result) {
4362
4362
  }
4363
4363
  return result;
4364
4364
  }
4365
+ function migrationVersionFromFilename(file, fallbackIndex = 0) {
4366
+ const match = basename(file).match(/^(\d{8,20})[_-]/);
4367
+ if (match)
4368
+ return Number(match[1]);
4369
+ return Date.now() * 1000 + fallbackIndex;
4370
+ }
4371
+ function appliedMigrationKeys(data) {
4372
+ const rows = Array.isArray(data) ? data : data?.rows || [];
4373
+ const keys = new Set;
4374
+ for (const row of rows) {
4375
+ if (!row || typeof row !== "object")
4376
+ continue;
4377
+ const migration = row;
4378
+ if (migration.version != null)
4379
+ keys.add(String(migration.version));
4380
+ if (migration.name != null)
4381
+ keys.add(String(migration.name));
4382
+ }
4383
+ return keys;
4384
+ }
4365
4385
  function registerDatabaseTools(server, http, config = {}) {
4366
4386
  const { readOnly = false, projectRef } = config;
4367
4387
  const actions = [
@@ -4382,7 +4402,7 @@ function registerDatabaseTools(server, http, config = {}) {
4382
4402
  "project_url",
4383
4403
  "generate_types"
4384
4404
  ];
4385
- const writeActions = ["apply_migration", "create_table_rls"];
4405
+ const writeActions = ["apply_migration", "push_migrations", "create_table_rls"];
4386
4406
  const allActions = readOnly ? actions : [...actions, ...writeActions];
4387
4407
  server.tool("database", `Database operations: query, schema, RLS, migrations, stats.
4388
4408
  Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
@@ -4390,6 +4410,8 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
4390
4410
  ref: projectRef ? exports_external.string().optional() : exports_external.string().optional().describe("Project ref"),
4391
4411
  sql: exports_external.string().optional().describe("[query/apply_migration] SQL statement"),
4392
4412
  file: exports_external.string().optional().describe("[query/apply_migration] Read SQL from local file path (avoids shell escaping issues with $$ and multi-statement DDL)"),
4413
+ dir: exports_external.string().optional().describe("[push_migrations] Directory containing .sql migration files (default: supabase/migrations)"),
4414
+ dry_run: exports_external.boolean().optional().describe("[push_migrations] List pending migration files without applying them"),
4393
4415
  schema: exports_external.string().optional().describe("[*] Schema name (default: public)"),
4394
4416
  table: exports_external.string().optional().describe("[describe_columns/indexes/constraints/rls_*] Table name"),
4395
4417
  schemas: exports_external.array(exports_external.string()).optional().describe("[list_tables/generate_types] Schemas array"),
@@ -4404,7 +4426,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
4404
4426
  const schemas = args.schemas || ["public"];
4405
4427
  if (args.file && !args.sql) {
4406
4428
  try {
4407
- args.sql = __require("fs").readFileSync(args.file, "utf-8");
4429
+ args.sql = readFileSync2(args.file, "utf-8");
4408
4430
  } catch (e) {
4409
4431
  return { content: [{ type: "text", text: `❌ Failed to read file ${args.file}: ${e.message}` }] };
4410
4432
  }
@@ -4532,6 +4554,72 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
4532
4554
  text = r.ok ? `✅ Migration '${args.name}' applied` : `❌ Failed (${r.status}): ${JSON.stringify(r.data)}`;
4533
4555
  break;
4534
4556
  }
4557
+ case "push_migrations": {
4558
+ const dir = args.dir || "supabase/migrations";
4559
+ if (!existsSync2(dir) || !statSync(dir).isDirectory()) {
4560
+ throw new Error(`Migration directory not found: ${dir}`);
4561
+ }
4562
+ const files = readdirSync(dir).filter((file) => file.endsWith(".sql")).sort();
4563
+ if (!files.length) {
4564
+ text = `No .sql migration files found in ${dir}`;
4565
+ break;
4566
+ }
4567
+ const migrationFiles = files.map((file, i) => ({
4568
+ file,
4569
+ name: basename(file, ".sql"),
4570
+ version: migrationVersionFromFilename(file, i)
4571
+ }));
4572
+ if (args.dry_run) {
4573
+ const r = await http.get(`/v1/projects/${ref}/database/migrations`);
4574
+ if (!r.ok) {
4575
+ text = `❌ Failed to load applied migrations (${r.status}): ${JSON.stringify(r.data)}`;
4576
+ break;
4577
+ }
4578
+ const appliedKeys = appliedMigrationKeys(r.data);
4579
+ const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
4580
+ const alreadyApplied = migrationFiles.filter(({ name, version }) => appliedKeys.has(name) || appliedKeys.has(String(version)));
4581
+ text = [
4582
+ `Migration dry run for ${dir}`,
4583
+ `Total: ${migrationFiles.length}`,
4584
+ `Pending: ${pending.length}`,
4585
+ `Already applied: ${alreadyApplied.length}`,
4586
+ ...pending.length ? ["", "Would apply:", ...pending.map(({ file, version }) => ` - ${file} (${version})`)] : [],
4587
+ ...alreadyApplied.length ? ["", "Already applied:", ...alreadyApplied.map(({ file, version }) => ` - ${file} (${version})`)] : []
4588
+ ].join(`
4589
+ `);
4590
+ break;
4591
+ }
4592
+ const applied = [];
4593
+ const skipped = [];
4594
+ for (const { file, name, version } of migrationFiles) {
4595
+ const sql = readFileSync2(join(dir, file), "utf-8");
4596
+ const r = await http.post(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
4597
+ if (r.ok) {
4598
+ applied.push(file);
4599
+ } else if (r.status === 409) {
4600
+ skipped.push(file);
4601
+ } else {
4602
+ text = [
4603
+ `❌ Failed to apply ${file} (${r.status})`,
4604
+ JSON.stringify(r.data, null, 2),
4605
+ "",
4606
+ `Applied before failure: ${applied.length}`,
4607
+ `Skipped before failure: ${skipped.length}`
4608
+ ].join(`
4609
+ `);
4610
+ return { content: [{ type: "text", text }] };
4611
+ }
4612
+ }
4613
+ text = [
4614
+ `✅ Migration push completed for ${dir}`,
4615
+ `Applied: ${applied.length}`,
4616
+ `Skipped: ${skipped.length}`,
4617
+ ...applied.length ? ["", "Applied files:", ...applied.map((file) => ` - ${file}`)] : [],
4618
+ ...skipped.length ? ["", "Skipped files:", ...skipped.map((file) => ` - ${file}`)] : []
4619
+ ].join(`
4620
+ `);
4621
+ break;
4622
+ }
4535
4623
  case "create_table_rls": {
4536
4624
  if (!args.table || !args.columns)
4537
4625
  throw new Error("'table' and 'columns' required");
@@ -5028,69 +5116,125 @@ Actions: status, list_buckets, list_files, upload_base64, delete_file`, {
5028
5116
  }
5029
5117
 
5030
5118
  // src/shared/tools/advanced-tools.ts
5119
+ import { existsSync as existsSync3, mkdtempSync, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync } from "node:fs";
5120
+ import { tmpdir } from "node:os";
5121
+ import { basename as basename2, join as join2, resolve as resolve2 } from "node:path";
5122
+ import { promisify } from "node:util";
5123
+ import { execFile } from "node:child_process";
5124
+ var execFileAsync = promisify(execFile);
5125
+ async function runBunBuild(args) {
5126
+ try {
5127
+ return await execFileAsync("bun", ["build", ...args], { maxBuffer: 10 * 1024 * 1024 });
5128
+ } catch (error) {
5129
+ const e = error;
5130
+ if (e.code === "ENOENT") {
5131
+ throw new Error("Bun is required for local edge function bundling. Install Bun or use deploy_bundle with explicit files.");
5132
+ }
5133
+ throw error;
5134
+ }
5135
+ }
5136
+ async function bundleEdgeFunctionPath(pathArg) {
5137
+ const entrypoint = resolveEntrypoint(pathArg);
5138
+ const tmpDir = mkdtempSync(join2(tmpdir(), "supacloud-edge-"));
5139
+ const outfile = join2(tmpDir, `${basename2(entrypoint).replace(/\.[^.]+$/, "") || "index"}.js`);
5140
+ try {
5141
+ const { stderr } = await runBunBuild([entrypoint, "--target", "bun", "--outfile", outfile]);
5142
+ if (!existsSync3(outfile))
5143
+ throw new Error(`Bundle failed: ${stderr}`);
5144
+ return readFileSync3(outfile, "utf-8");
5145
+ } finally {
5146
+ rmSync(tmpDir, { recursive: true, force: true });
5147
+ }
5148
+ }
5149
+ function resolveEntrypoint(pathArg) {
5150
+ const resolved = resolve2(pathArg);
5151
+ const stat = statSync2(resolved);
5152
+ if (!stat.isDirectory())
5153
+ return resolved;
5154
+ const entrypoint = join2(resolved, "index.ts");
5155
+ if (!existsSync3(entrypoint)) {
5156
+ throw new Error(`Directory provided but no index.ts found at ${entrypoint}`);
5157
+ }
5158
+ return entrypoint;
5159
+ }
5160
+ var backgroundRoutesSchema = exports_external.preprocess((value) => {
5161
+ if (typeof value !== "string")
5162
+ return value;
5163
+ const trimmed = value.trim();
5164
+ if (!trimmed)
5165
+ return [];
5166
+ if (!trimmed.startsWith("["))
5167
+ return trimmed.split(",").map((route) => route.trim()).filter(Boolean);
5168
+ try {
5169
+ return JSON.parse(trimmed);
5170
+ } catch {
5171
+ return [trimmed];
5172
+ }
5173
+ }, exports_external.array(exports_external.string()).optional().superRefine((routes, ctx) => {
5174
+ if (!routes)
5175
+ return;
5176
+ for (const route of routes) {
5177
+ if (route.trim().startsWith("[")) {
5178
+ ctx.addIssue({
5179
+ code: exports_external.ZodIssueCode.custom,
5180
+ message: "Invalid background_routes JSON array. Use a valid JSON array or comma-separated routes like /queue/*,/render/*."
5181
+ });
5182
+ return;
5183
+ }
5184
+ }
5185
+ }));
5031
5186
  function registerAdvancedTools(server, http) {
5032
5187
  server.tool("edge_functions", `Edge Function management (Deno/Bun serverless). Server auto-bundles dependencies.
5033
- Actions: list, deploy, deploy_bundle, source, delete, check`, {
5034
- action: exports_external.enum(["list", "deploy", "deploy_bundle", "source", "delete", "check"]).describe("Action"),
5188
+ Actions: list, deploy, deploy_bundle, config, source, delete, check`, {
5189
+ action: exports_external.enum(["list", "deploy", "deploy_bundle", "config", "source", "delete", "check"]).describe("Action"),
5035
5190
  ref: exports_external.string().describe("Project ref"),
5036
- slug: exports_external.string().optional().describe("[deploy/deploy_bundle/source/delete/check] Function name"),
5191
+ slug: exports_external.string().optional().describe("[deploy/deploy_bundle/config/source/delete/check] Function name"),
5037
5192
  code: exports_external.string().optional().describe("[deploy/check] Function source code (TypeScript)"),
5038
5193
  path: exports_external.string().optional().describe("[deploy/check] Local file path to read code from (alternative to code)"),
5039
5194
  files: exports_external.record(exports_external.string()).optional().describe("[deploy_bundle] File map: { 'index.ts': '...', '_shared/x.ts': '...' }"),
5040
5195
  entrypoint: exports_external.string().optional().describe("[deploy_bundle] Entrypoint file (default: index.ts)"),
5041
- minify: exports_external.boolean().optional().describe("[deploy/deploy_bundle] Minify bundle")
5196
+ minify: exports_external.boolean().optional().describe("[deploy/deploy_bundle] Minify bundle"),
5197
+ verify_jwt: exports_external.boolean().optional().describe("[deploy/deploy_bundle/config] Set JWT verification for this function"),
5198
+ background_routes: backgroundRoutesSchema.describe("[deploy/deploy_bundle/config] Background route paths; pass comma-separated or JSON array in CLI")
5042
5199
  }, async (args) => {
5043
- const { action, ref, slug, path: pathArg, files, entrypoint, minify } = args;
5200
+ const { action, ref, slug, path: pathArg, files, entrypoint, minify, verify_jwt, background_routes } = args;
5044
5201
  let code = args.code;
5045
5202
  const need = (f, v) => {
5046
5203
  if (!v)
5047
5204
  throw new Error(`'${f}' required for '${action}'`);
5048
5205
  };
5049
5206
  let text;
5207
+ const functionConfig = () => ({
5208
+ ...typeof verify_jwt === "boolean" ? { verify_jwt } : {},
5209
+ ...Array.isArray(background_routes) ? { background_routes } : {}
5210
+ });
5211
+ const hasFunctionConfig = () => Object.keys(functionConfig()).length > 0;
5212
+ const updateFunctionConfig = async () => {
5213
+ need("slug", slug);
5214
+ if (!hasFunctionConfig()) {
5215
+ throw new Error("'verify_jwt' or 'background_routes' required for 'config'");
5216
+ }
5217
+ const cr = await http.patch(`/v1/projects/${ref}/functions/${slug}/config`, functionConfig());
5218
+ return cr.ok ? `✅ Function ${slug} config updated
5219
+ ${JSON.stringify(cr.data, null, 2)}` : `❌ Config update failed (${cr.status}): ${JSON.stringify(cr.data)}`;
5220
+ };
5050
5221
  const checkSyntax = async (sourceCode) => {
5051
- const fs = __require("fs");
5052
- const os = __require("os");
5053
- const { promisify } = __require("util");
5054
- const execAsync = promisify(__require("child_process").exec);
5055
- const tmpFile = `${os.tmpdir()}/supacloud_edge_${Date.now()}.ts`;
5056
- fs.writeFileSync(tmpFile, sourceCode);
5222
+ const tmpDir = mkdtempSync(join2(tmpdir(), "supacloud-edge-check-"));
5223
+ const tmpFile = join2(tmpDir, "index.ts");
5224
+ writeFileSync(tmpFile, sourceCode);
5057
5225
  try {
5058
- await execAsync(`bun build ${tmpFile} --external='*'`);
5226
+ await runBunBuild([tmpFile, "--external", "*"]);
5059
5227
  return { ok: true };
5060
5228
  } catch (e) {
5061
- return { ok: false, err: e.stdout + `
5062
- ` + (e.stderr || e.message) };
5229
+ return { ok: false, err: `${e.stdout || ""}
5230
+ ${e.stderr || e.message}` };
5063
5231
  } finally {
5064
- try {
5065
- fs.unlinkSync(tmpFile);
5066
- } catch (e) {}
5232
+ rmSync(tmpDir, { recursive: true, force: true });
5067
5233
  }
5068
5234
  };
5069
5235
  if (pathArg && !code) {
5070
5236
  try {
5071
- const fs = __require("fs");
5072
- const os = __require("os");
5073
- const { promisify } = __require("util");
5074
- const execAsync = promisify(__require("child_process").exec);
5075
- const stat = fs.statSync(pathArg);
5076
- let entrypoint2 = pathArg;
5077
- if (stat.isDirectory()) {
5078
- entrypoint2 = `${pathArg}/index.ts`;
5079
- if (!fs.existsSync(entrypoint2)) {
5080
- throw new Error(`Directory provided but no index.ts found at ${entrypoint2}`);
5081
- }
5082
- }
5083
- const tmpOut = `${os.tmpdir()}/supacloud_bundled_${Date.now()}.js`;
5084
- try {
5085
- const { stderr } = await execAsync(`bun build ${entrypoint2} --target bun --outfile ${tmpOut}`);
5086
- if (!fs.existsSync(tmpOut))
5087
- throw new Error(`Bundle failed: ${stderr}`);
5088
- code = fs.readFileSync(tmpOut, "utf-8");
5089
- } finally {
5090
- try {
5091
- fs.unlinkSync(tmpOut);
5092
- } catch (e) {}
5093
- }
5237
+ code = await bundleEdgeFunctionPath(pathArg);
5094
5238
  } catch (e) {
5095
5239
  throw new Error(`Failed to bundle/read path ${pathArg}: ${e.message}`);
5096
5240
  }
@@ -5119,13 +5263,26 @@ ${deployCheck.err}`;
5119
5263
  break;
5120
5264
  }
5121
5265
  const dr = await http.post(`/v1/projects/${ref}/functions/${slug}`, { code, minify });
5122
- text = dr.ok ? `✅ Function ${slug} deployed` : `❌ Failed (${dr.status}): ${JSON.stringify(dr.data)}`;
5266
+ if (!dr.ok) {
5267
+ text = `❌ Failed (${dr.status}): ${JSON.stringify(dr.data)}`;
5268
+ break;
5269
+ }
5270
+ text = hasFunctionConfig() ? `✅ Function ${slug} deployed
5271
+ ${await updateFunctionConfig()}` : `✅ Function ${slug} deployed`;
5123
5272
  break;
5124
5273
  case "deploy_bundle":
5125
5274
  need("slug", slug);
5126
5275
  need("files", files);
5127
5276
  const br = await http.post(`/v1/projects/${ref}/functions/${slug}/bundle`, { files, entrypoint, minify });
5128
- text = br.ok ? `✅ Function ${slug} bundle deployed (${Object.keys(files).length} files)` : `❌ Failed (${br.status}): ${JSON.stringify(br.data)}`;
5277
+ if (!br.ok) {
5278
+ text = `❌ Failed (${br.status}): ${JSON.stringify(br.data)}`;
5279
+ break;
5280
+ }
5281
+ text = hasFunctionConfig() ? `✅ Function ${slug} bundle deployed (${Object.keys(files).length} files)
5282
+ ${await updateFunctionConfig()}` : `✅ Function ${slug} bundle deployed (${Object.keys(files).length} files)`;
5283
+ break;
5284
+ case "config":
5285
+ text = await updateFunctionConfig();
5129
5286
  break;
5130
5287
  case "source":
5131
5288
  need("slug", slug);
@@ -5230,7 +5387,8 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status})`;
5230
5387
  }
5231
5388
 
5232
5389
  // src/shared/tools/frontend-tools.ts
5233
- import { basename } from "node:path";
5390
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
5391
+ import { basename as basename3 } from "node:path";
5234
5392
  function registerFrontendTools(server, http) {
5235
5393
  server.tool("frontend", `Frontend hosting (static sites & SSR). Supports: static, react, vue, svelte, sveltekit, nextjs, nuxt, astro.
5236
5394
  Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy, build_logs, add_domain, remove_domain, set_env, list_frameworks, list_records`, {
@@ -5324,14 +5482,12 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
5324
5482
  need("ref", ref);
5325
5483
  need("id", id);
5326
5484
  need("zip_path", zip_path);
5327
- const file = Bun.file(zip_path);
5328
- if (!await file.exists()) {
5485
+ if (!existsSync4(zip_path)) {
5329
5486
  throw new Error(`Zip file not found: ${zip_path}`);
5330
5487
  }
5488
+ const zipBuffer = readFileSync4(zip_path);
5331
5489
  const form = new FormData;
5332
- form.append("file", new File([await file.arrayBuffer()], basename(zip_path), {
5333
- type: "application/zip"
5334
- }));
5490
+ form.append("file", new Blob([zipBuffer], { type: "application/zip" }), basename3(zip_path));
5335
5491
  text = ok(await http.postMultipart(`/v1/projects/${ref}/frontend/deployments/${id}/deploy/upload`, form));
5336
5492
  break;
5337
5493
  case "redeploy":
@@ -5462,6 +5618,15 @@ function registerUserProjectCliTools(server, http, options = {}) {
5462
5618
  // src/index.ts
5463
5619
  var projectActionSchema = exports_external.enum(["get", "health", "logs", "api_keys", "settings", "tasks"]);
5464
5620
  var genericActionSchema = exports_external.string();
5621
+ function unwrapMcpSchema(schema) {
5622
+ if (schema && typeof schema === "object" && !Array.isArray(schema) && "args" in schema) {
5623
+ const argsSchema = schema.args;
5624
+ if (argsSchema && typeof argsSchema === "object" && !Array.isArray(argsSchema)) {
5625
+ return argsSchema;
5626
+ }
5627
+ }
5628
+ return schema;
5629
+ }
5465
5630
  function captureTools(register) {
5466
5631
  const tools = {};
5467
5632
  const server = {
@@ -5469,7 +5634,7 @@ function captureTools(register) {
5469
5634
  if (typeof schemaOrCallback === "function") {
5470
5635
  tools[name] = { schema: {}, callback: schemaOrCallback };
5471
5636
  } else {
5472
- tools[name] = { schema: schemaOrCallback, callback };
5637
+ tools[name] = { schema: unwrapMcpSchema(schemaOrCallback), callback };
5473
5638
  }
5474
5639
  }
5475
5640
  };
@@ -5507,6 +5672,7 @@ EXAMPLES
5507
5672
  supacloud frontend list --ref abc123
5508
5673
  supacloud database query --sql "select now()"
5509
5674
  supacloud edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello
5675
+ supacloud edge_functions config --ref abc123 --slug hello --verify_jwt false --background_routes "/queue/*,/render/*"
5510
5676
 
5511
5677
  SEPARATE ADMIN CLI
5512
5678
 
@@ -5615,10 +5781,11 @@ function createCliTools() {
5615
5781
  assign(captureTools((server) => registerUserProjectCliTools(server, http, {
5616
5782
  projectRef: context.projectRef || undefined
5617
5783
  })));
5618
- assign(captureTools((server) => registerDatabaseTools(server, http, {
5784
+ const databaseTools = captureTools((server) => registerDatabaseTools(server, http, {
5619
5785
  projectRef: context.projectRef || undefined,
5620
5786
  readOnly: context.readOnly
5621
- })));
5787
+ }));
5788
+ assign(databaseTools);
5622
5789
  assign(captureTools((server) => registerAuthTools(server, http)));
5623
5790
  assign(captureTools((server) => registerStorageTools(server, http)));
5624
5791
  assign(captureTools((server) => registerAdvancedTools(server, http)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",