@supacloud/cli 0.3.3 → 0.3.4

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 +104 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4418,7 +4418,7 @@ function registerDatabaseTools(server, http, config = {}) {
4418
4418
  "project_url",
4419
4419
  "generate_types"
4420
4420
  ];
4421
- const writeActions = ["apply_migration", "push_migrations", "create_table_rls"];
4421
+ const writeActions = ["apply_migration", "push_migrations", "baseline_migrations", "create_table_rls"];
4422
4422
  const allActions = readOnly ? actions : [...actions, ...writeActions];
4423
4423
  server.tool("database", `Database operations: query, schema, RLS, migrations, stats.
4424
4424
  Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
@@ -4426,8 +4426,8 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
4426
4426
  ref: projectRef ? exports_external.string().optional() : exports_external.string().optional().describe("Project ref"),
4427
4427
  sql: exports_external.string().optional().describe("[query/apply_migration] SQL statement"),
4428
4428
  file: exports_external.string().optional().describe("[query/apply_migration] Read SQL from local file path (avoids shell escaping issues with $$ and multi-statement DDL)"),
4429
- dir: exports_external.string().optional().describe("[push_migrations] Directory containing .sql migration files (default: supabase/migrations)"),
4430
- dry_run: exports_external.boolean().optional().describe("[push_migrations] List pending migration files without applying them"),
4429
+ dir: exports_external.string().optional().describe("[push_migrations/baseline_migrations] Directory containing .sql migration files (default: supabase/migrations)"),
4430
+ dry_run: exports_external.boolean().optional().describe("[push_migrations/baseline_migrations] Preview changes without applying them"),
4431
4431
  schema: exports_external.string().optional().describe("[*] Schema name (default: public)"),
4432
4432
  table: exports_external.string().optional().describe("[describe_columns/indexes/constraints/rls_*] Table name"),
4433
4433
  schemas: exports_external.array(exports_external.string()).optional().describe("[list_tables/generate_types] Schemas array"),
@@ -4447,7 +4447,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
4447
4447
  return { content: [{ type: "text", text: `❌ Failed to read file ${args.file}: ${e.message}` }] };
4448
4448
  }
4449
4449
  }
4450
- const execSql = async (sql) => http.post(`/v1/projects/${ref}/database/sql`, { sql });
4450
+ const execSql = async (sql, mode) => http.post(`/v1/projects/${ref}/database/sql`, mode ? { sql, mode } : { sql });
4451
4451
  let text;
4452
4452
  switch (action) {
4453
4453
  case "query": {
@@ -4649,6 +4649,64 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
4649
4649
  `);
4650
4650
  break;
4651
4651
  }
4652
+ case "baseline_migrations": {
4653
+ const dir = args.dir || "supabase/migrations";
4654
+ if (!existsSync2(dir) || !statSync(dir).isDirectory()) {
4655
+ throw new Error(`Migration directory not found: ${dir}`);
4656
+ }
4657
+ const files = readdirSync(dir).filter((file) => file.endsWith(".sql")).sort();
4658
+ if (!files.length) {
4659
+ text = `No .sql migration files found in ${dir}`;
4660
+ break;
4661
+ }
4662
+ const migrationFiles = files.map((file, i) => ({
4663
+ file,
4664
+ name: basename(file, ".sql"),
4665
+ version: migrationVersionFromFilename(file, i)
4666
+ }));
4667
+ const r = await http.get(`/v1/projects/${ref}/database/migrations`);
4668
+ if (!r.ok) {
4669
+ text = `❌ Failed to load applied migrations (${r.status}): ${JSON.stringify(r.data)}`;
4670
+ break;
4671
+ }
4672
+ const appliedKeys = appliedMigrationKeys(r.data);
4673
+ const missing = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
4674
+ const alreadyApplied = migrationFiles.filter(({ name, version }) => appliedKeys.has(name) || appliedKeys.has(String(version)));
4675
+ if (args.dry_run) {
4676
+ text = [
4677
+ `Migration baseline dry run for ${dir}`,
4678
+ `Total: ${migrationFiles.length}`,
4679
+ "",
4680
+ "Would mark as applied:",
4681
+ ...missing.length ? missing.map(({ file, version }) => ` - ${file} (${version})`) : [" - none"],
4682
+ "",
4683
+ "Already applied:",
4684
+ ...alreadyApplied.length ? alreadyApplied.map(({ file, version }) => ` - ${file} (${version})`) : [" - none"]
4685
+ ].join(`
4686
+ `);
4687
+ break;
4688
+ }
4689
+ if (!missing.length) {
4690
+ text = [
4691
+ `✅ Migration baseline already aligned for ${dir}`,
4692
+ `Already applied: ${alreadyApplied.length}`
4693
+ ].join(`
4694
+ `);
4695
+ break;
4696
+ }
4697
+ const baselineSql = buildMigrationBaselineSql(missing);
4698
+ const baselineResult = await execSql(baselineSql, "migration");
4699
+ text = baselineResult.ok ? [
4700
+ `✅ Migration baseline completed for ${dir}`,
4701
+ `Marked applied: ${missing.length}`,
4702
+ `Already applied: ${alreadyApplied.length}`,
4703
+ "",
4704
+ "Marked files:",
4705
+ ...missing.map(({ file, version }) => ` - ${file} (${version})`)
4706
+ ].join(`
4707
+ `) : `❌ Failed (${baselineResult.status}): ${JSON.stringify(baselineResult.data)}`;
4708
+ break;
4709
+ }
4652
4710
  case "create_table_rls": {
4653
4711
  if (!args.table || !args.columns)
4654
4712
  throw new Error("'table' and 'columns' required");
@@ -4663,6 +4721,48 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
4663
4721
  return { content: [{ type: "text", text }] };
4664
4722
  });
4665
4723
  }
4724
+ function sqlString(value) {
4725
+ return `'${value.replace(/'/g, "''")}'`;
4726
+ }
4727
+ function buildMigrationBaselineSql(migrations) {
4728
+ const values = migrations.map(({ name, version }) => `(${version}, ${sqlString(name)}, ARRAY[${sqlString(`baseline:${name}`)}]::text[])`).join(`,
4729
+ `);
4730
+ return `
4731
+ CREATE SCHEMA IF NOT EXISTS supabase_migrations;
4732
+
4733
+ CREATE TABLE IF NOT EXISTS supabase_migrations.schema_migrations (
4734
+ version BIGINT PRIMARY KEY,
4735
+ statements TEXT[],
4736
+ name TEXT
4737
+ );
4738
+
4739
+ CREATE TABLE IF NOT EXISTS public.schema_migrations (
4740
+ version VARCHAR(255) PRIMARY KEY,
4741
+ statements TEXT[],
4742
+ name TEXT
4743
+ );
4744
+
4745
+ WITH baseline(version, name, statements) AS (
4746
+ VALUES
4747
+ ${values}
4748
+ )
4749
+ INSERT INTO supabase_migrations.schema_migrations (version, statements, name)
4750
+ SELECT version, statements, name FROM baseline
4751
+ ON CONFLICT (version) DO UPDATE
4752
+ SET statements = EXCLUDED.statements,
4753
+ name = EXCLUDED.name;
4754
+
4755
+ WITH baseline(version, name, statements) AS (
4756
+ VALUES
4757
+ ${values}
4758
+ )
4759
+ INSERT INTO public.schema_migrations (version, statements, name)
4760
+ SELECT version::text, statements, name FROM baseline
4761
+ ON CONFLICT (version) DO UPDATE
4762
+ SET statements = EXCLUDED.statements,
4763
+ name = EXCLUDED.name;
4764
+ `.trim();
4765
+ }
4666
4766
  function formatSqlResult(data) {
4667
4767
  if (!data || typeof data !== "object")
4668
4768
  return JSON.stringify(data, null, 2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",