@supacloud/cli 0.40.0 → 0.42.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 +488 -19
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -238838,7 +238838,8 @@ var ACTION_POLICY = {
238838
238838
  },
238839
238839
  ai: { local: ["show_skill", "install_skill"] },
238840
238840
  app: { local: ["generate", "compile", "check", "graph", "explain"] },
238841
- db: { local: ["lint", "explain"], read: ["module_check"] }
238841
+ db: { local: ["lint", "explain"], read: ["module_check"] },
238842
+ dev: { read: ["status"], write: ["sync", "watch", "migrate"] }
238842
238843
  };
238843
238844
  function declaredMode(moduleName, action) {
238844
238845
  const policy = ACTION_POLICY[moduleName];
@@ -238857,6 +238858,8 @@ function executionMode(moduleName, action, args) {
238857
238858
  return "read";
238858
238859
  if (moduleName === "supabase" && action === "push" && args.dry_run === true)
238859
238860
  return "read";
238861
+ if (moduleName === "dev" && action === "migrate" && args.apply !== true)
238862
+ return "read";
238860
238863
  return declaredMode(moduleName, action);
238861
238864
  }
238862
238865
  function authorizeExecution(moduleName, args, authorization) {
@@ -239502,6 +239505,61 @@ function migrationExecutionStatements(statements) {
239502
239505
  const hasOuterTransaction = /^(?:BEGIN(?:\s+(?:WORK|TRANSACTION))?|START\s+TRANSACTION)$/i.test(first) && /^(?:COMMIT|END)(?:\s+(?:WORK|TRANSACTION))?$/i.test(last);
239503
239506
  return hasOuterTransaction ? statements.slice(1, -1) : [...statements];
239504
239507
  }
239508
+ function skipSqlTrivia(sql, start) {
239509
+ let cursor = start;
239510
+ for (;; ) {
239511
+ while (cursor < sql.length && /\s/.test(sql[cursor]))
239512
+ cursor++;
239513
+ if (sql.startsWith("--", cursor)) {
239514
+ cursor = lineCommentEnd(sql, cursor);
239515
+ continue;
239516
+ }
239517
+ if (sql.startsWith("/*", cursor)) {
239518
+ cursor = blockCommentEnd(sql, cursor);
239519
+ continue;
239520
+ }
239521
+ return cursor;
239522
+ }
239523
+ }
239524
+ function doStatementBody(sql, keywordEnd) {
239525
+ let cursor = skipSqlTrivia(sql, keywordEnd);
239526
+ if (/^LANGUAGE\b/i.test(sql.slice(cursor))) {
239527
+ cursor += "LANGUAGE".length;
239528
+ cursor = skipSqlTrivia(sql, cursor);
239529
+ if (sql[cursor] === "'")
239530
+ cursor = maskSingleQuotedString(sql, cursor).end;
239531
+ else if (sql[cursor] === '"')
239532
+ cursor = maskDoubleQuotedIdentifier(sql, cursor).end;
239533
+ else {
239534
+ const languageName = /^[A-Za-z_][A-Za-z0-9_]*/.exec(sql.slice(cursor));
239535
+ if (!languageName)
239536
+ return null;
239537
+ cursor += languageName[0].length;
239538
+ }
239539
+ cursor = skipSqlTrivia(sql, cursor);
239540
+ }
239541
+ if (sql[cursor] === "'") {
239542
+ const end = maskSingleQuotedString(sql, cursor).end;
239543
+ return sql.slice(cursor + 1, Math.max(cursor + 1, end - 1));
239544
+ }
239545
+ const tag = sql[cursor] === "$" ? dollarQuoteTagAt(sql, cursor) : "";
239546
+ if (!tag)
239547
+ return null;
239548
+ const bodyEnd = sql.indexOf(tag, cursor + tag.length);
239549
+ return bodyEnd === -1 ? null : sql.slice(cursor + tag.length, bodyEnd);
239550
+ }
239551
+ function topLevelDoBodies(sql) {
239552
+ const masked = maskSqlPolicyNoise(sql);
239553
+ const doKeywordPattern = /(?:^|;)\s*DO\b/gi;
239554
+ const bodies = [];
239555
+ let match;
239556
+ while ((match = doKeywordPattern.exec(masked)) !== null) {
239557
+ const body = doStatementBody(sql, doKeywordPattern.lastIndex);
239558
+ if (body !== null)
239559
+ bodies.push(body);
239560
+ }
239561
+ return bodies;
239562
+ }
239505
239563
  function splitTopLevelClauses(sql) {
239506
239564
  const masked = maskSqlNoise(sql);
239507
239565
  const clauses = [];
@@ -239596,14 +239654,6 @@ var RISK_RULES = [
239596
239654
  description: "Renames table. Causes immediate downtime for application code.",
239597
239655
  recommendation: "Follow Expand-Contract: Create a view or alias table during transition."
239598
239656
  },
239599
- {
239600
- type: "manual_review_do_block",
239601
- level: "HIGH",
239602
- pattern: /^\s*DO\b/i,
239603
- description: "DO blocks can execute dynamic or procedural DDL that static rules cannot inspect safely.",
239604
- recommendation: "Move schema changes into explicit SQL statements; push_migrations rejects opaque procedural SQL.",
239605
- blocksTransactionalPush: true
239606
- },
239607
239657
  {
239608
239658
  type: "manual_review_procedural_definition",
239609
239659
  level: "HIGH",
@@ -239705,7 +239755,7 @@ var MIGRATION_LEDGER_RELATION_END = String.raw`(?=$|[\s,;(*])`;
239705
239755
  var MIGRATION_LEDGER_DDL_OR_MAINTENANCE_PREFIX = String.raw`(?:CREATE\s+(?:UNIQUE\s+)?INDEX\b[^;]*\bON\s+(?:ONLY\s+)?|CREATE\s+(?:CONSTRAINT\s+)?TRIGGER\b[^;]*\bON\s+|DROP\s+TRIGGER\s+(?:IF\s+EXISTS\s+)?[^;]*\bON\s+|CREATE\s+RULE\b[^;]*\bTO\s+|DROP\s+RULE\s+(?:IF\s+EXISTS\s+)?[^;]*\bON\s+|CREATE\s+POLICY\b[^;]*\bON\s+|ALTER\s+POLICY\b[^;]*\bON\s+|DROP\s+POLICY\s+(?:IF\s+EXISTS\s+)?[^;]*\bON\s+|COMMENT\s+ON\s+TABLE\s+|SECURITY\s+LABEL(?:\s+FOR\s+[^;\s]+)?\s+ON\s+TABLE\s+|REINDEX(?:\s*\([^;)]*\))?\s+TABLE\s+(?:CONCURRENTLY\s+)?|CLUSTER(?:\s+VERBOSE)?\s+|VACUUM(?:\s*\([^;)]*\))?(?:\s+(?:FULL|FREEZE|VERBOSE|ANALYZE))*\s+|ANALYZE(?:\s*\([^;)]*\))?(?:\s+VERBOSE)?\s+)`;
239706
239756
  var MIGRATION_LEDGER_MODIFICATION_PATTERN = new RegExp(String.raw`\b(?:(?:CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?|INSERT\s+INTO\s+(?:ONLY\s+)?|UPDATE\s+(?:ONLY\s+)?|DELETE\s+FROM\s+(?:ONLY\s+)?|MERGE\s+INTO\s+(?:ONLY\s+)?|ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?|COPY\s+)` + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`|(?:DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?|TRUNCATE(?:\s+TABLE)?\s+|LOCK\s+(?:TABLE\s+)?)[^;]*` + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`|` + MIGRATION_LEDGER_DDL_OR_MAINTENANCE_PREFIX + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`)`, "i");
239707
239757
  var NON_TABLE_PRIVILEGE_TARGET = String.raw`(?:ALL\s+(?:FUNCTIONS|PROCEDURES|ROUTINES|SEQUENCES)\s+IN\s+SCHEMA|DATABASE|DOMAIN|FOREIGN\s+DATA\s+WRAPPER|FOREIGN\s+SERVER|FUNCTION|LANGUAGE|LARGE\s+OBJECT|PARAMETER|PROCEDURE|ROUTINE|SCHEMA|SEQUENCE|TABLESPACE|TYPE)\b`;
239708
- var MIGRATION_LEDGER_PRIVILEGE_PATTERN = new RegExp(String.raw`\b(?:GRANT|REVOKE)\b[^;]*\bON\s+(?:(?:TABLE\s+)?(?!` + NON_TABLE_PRIVILEGE_TARGET + String.raw`)(?:(?!\b(?:TO|FROM)\b)[^;])*?` + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`(?=[^;]*\b(?:TO|FROM)\b)|ALL\s+TABLES\s+IN\s+SCHEMA\s+"?(?:supabase_migrations|public)"?(?=$|[\s,;]))`, "i");
239758
+ var MIGRATION_LEDGER_PRIVILEGE_PATTERN = new RegExp(String.raw`\b(?:GRANT|REVOKE)\b[^;]*\bON\s+(?:(?:TABLE\s+)?(?!` + NON_TABLE_PRIVILEGE_TARGET + String.raw`)(?:(?!\b(?:TO|FROM)\b)[^;])*?` + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`(?=[^;]*\b(?:TO|FROM)\b)|ALL\s+TABLES\s+IN\s+SCHEMA\s+"?supabase_migrations"?(?=$|[\s,;]))`, "i");
239709
239759
  var PUSH_BLOCKER_RULES = [
239710
239760
  {
239711
239761
  type: "unsupported_project_scope_management",
@@ -239832,6 +239882,7 @@ var MIGRATION_LEDGER_BLOCKER_RULES = [
239832
239882
  blocksTransactionalPush: true
239833
239883
  }
239834
239884
  ];
239885
+ var DO_BODY_BLOCKER_RULES = PUSH_BLOCKER_RULES.filter((rule) => rule.type !== "unsupported_transaction_control");
239835
239886
  function matchingRisks(rawStatement, maskedStatement, rules) {
239836
239887
  return rules.filter((rule) => rule.pattern.test(maskedStatement) && !rule.excludePattern?.test(maskedStatement)).map((rule) => ({
239837
239888
  level: rule.level,
@@ -239908,6 +239959,13 @@ function analyzeMigrationSql(sql) {
239908
239959
  risks.push(...matchingRisks(rawStatement, masked, PUSH_BLOCKER_RULES));
239909
239960
  risks.push(...matchingRisks(rawStatement, policyMasked, QUOTED_FUNCTION_BLOCKER_RULES));
239910
239961
  risks.push(...matchingRisks(rawStatement, policyMasked, MIGRATION_LEDGER_BLOCKER_RULES));
239962
+ for (const body of topLevelDoBodies(rawStatement)) {
239963
+ const maskedBody = maskSqlNoise(body);
239964
+ const policyMaskedBody = maskSqlPolicyNoise(body);
239965
+ risks.push(...matchingRisks(body, maskedBody, DO_BODY_BLOCKER_RULES));
239966
+ risks.push(...matchingRisks(body, policyMaskedBody, QUOTED_FUNCTION_BLOCKER_RULES));
239967
+ risks.push(...matchingRisks(body, policyMaskedBody, MIGRATION_LEDGER_BLOCKER_RULES));
239968
+ }
239911
239969
  }
239912
239970
  return risks;
239913
239971
  }
@@ -246635,9 +246693,9 @@ function parseModule(candidate, nameByNode, ctx) {
246635
246693
  className: cls.getName() ?? "<anonymous>",
246636
246694
  name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>",
246637
246695
  permission: stringLiteralProp(meta, "permission"),
246638
- transaction: stringLiteralProp(meta, "transaction"),
246696
+ transaction: commandModeProp(meta, "transaction") ?? "none",
246639
246697
  audit: stringLiteralProp(meta, "audit"),
246640
- idempotency: stringLiteralProp(meta, "idempotency")
246698
+ idempotency: commandModeProp(meta, "idempotency") ?? "none"
246641
246699
  });
246642
246700
  }
246643
246701
  }
@@ -246665,6 +246723,10 @@ function parseModule(candidate, nameByNode, ctx) {
246665
246723
  exports
246666
246724
  };
246667
246725
  }
246726
+ function commandModeProp(object, name) {
246727
+ const value = stringLiteralProp(object, name);
246728
+ return value === "required" || value === "none" ? value : undefined;
246729
+ }
246668
246730
  function parseProvider(el, exportsSet, ctx) {
246669
246731
  const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
246670
246732
  const line = el.getStartLineNumber();
@@ -246817,6 +246879,11 @@ function parseController(el, ctx) {
246817
246879
  schemaImports[schemaExpr.getText()] = importPath;
246818
246880
  }
246819
246881
  }
246882
+ const commandExpr = getProp(optionsArg, "command");
246883
+ if (commandExpr && import_ts_morph.Node.isIdentifier(commandExpr)) {
246884
+ const commandDecl = resolveDeclaration(commandExpr)[0];
246885
+ route.command = commandDecl && import_ts_morph.Node.isClassDeclaration(commandDecl) ? commandDecl.getName() ?? commandExpr.getText() : commandExpr.getText();
246886
+ }
246820
246887
  }
246821
246888
  routes.push(route);
246822
246889
  }
@@ -247052,6 +247119,16 @@ var INTERFACES = `export interface CompiledRoute {
247052
247119
  params?: unknown;
247053
247120
  query?: unknown;
247054
247121
  response?: unknown;
247122
+ command?: string;
247123
+ }
247124
+
247125
+ export interface CompiledCommand {
247126
+ className: string;
247127
+ name: string;
247128
+ permission: string;
247129
+ transaction: "required" | "none";
247130
+ audit?: string;
247131
+ idempotency: "required" | "none";
247055
247132
  }
247056
247133
 
247057
247134
  export interface CompiledController {
@@ -247070,12 +247147,15 @@ export interface CompiledModule {
247070
247147
  createRequestScope?(
247071
247148
  services: Record<string, unknown>,
247072
247149
  ctx: unknown,
247150
+ imported?: Record<string, Record<string, unknown>>,
247073
247151
  ): Record<string, unknown>;
247074
247152
  createJobScope?(
247075
247153
  services: Record<string, unknown>,
247076
247154
  ctx: unknown,
247155
+ imported?: Record<string, Record<string, unknown>>,
247077
247156
  ): Record<string, unknown>;
247078
247157
  controllers: CompiledController[];
247158
+ commands: CompiledCommand[];
247079
247159
  }`;
247080
247160
  async function generateApplication(graph, options) {
247081
247161
  const modules = topoSortModules(graph.modules);
@@ -247094,7 +247174,7 @@ async function generateApplication(graph, options) {
247094
247174
  ...imports.size > 0 ? [""] : [],
247095
247175
  INTERFACES,
247096
247176
  "",
247097
- "export function createCompiledModules(deps: Record<string, unknown>): CompiledModule[] {",
247177
+ "export function createCompiledModules(): CompiledModule[] {",
247098
247178
  " return [",
247099
247179
  ...descriptorEntries.map((entry) => indent(entry, 4) + ","),
247100
247180
  " ];",
@@ -247225,6 +247305,7 @@ class ModuleGenerator {
247225
247305
  lines.push(` createJobScope: create${this.pascal}JobScope,`);
247226
247306
  }
247227
247307
  lines.push(` controllers: ${this.renderControllers()},`);
247308
+ lines.push(` commands: ${JSON.stringify(this.module.commands)},`);
247228
247309
  lines.push(`}`);
247229
247310
  return lines.join(`
247230
247311
  `);
@@ -247249,6 +247330,8 @@ class ModuleGenerator {
247249
247330
  fields.push(`${field}: ${local}`);
247250
247331
  }
247251
247332
  }
247333
+ if (route.command)
247334
+ fields.push(`command: ${JSON.stringify(route.command)}`);
247252
247335
  return `{ ${fields.join(", ")} }`;
247253
247336
  });
247254
247337
  return [
@@ -247282,6 +247365,7 @@ ${indent(item, 2)}`).join(",")}
247282
247365
  `function create${this.pascal}${suffix}(`,
247283
247366
  ` services: Record<string, unknown>,`,
247284
247367
  ` ctx: unknown,`,
247368
+ ` imported: Record<string, Record<string, unknown>> = {},`,
247285
247369
  `): Record<string, unknown> {`,
247286
247370
  indent(this.renderFactoryBody(kind), 2),
247287
247371
  `}`
@@ -247380,7 +247464,7 @@ ${indent(item, 2)}`).join(",")}
247380
247464
  continue;
247381
247465
  if (kind === "services")
247382
247466
  return `imported.${importName}.${camelName(token)}`;
247383
- return `services.${camelName(token)}`;
247467
+ return `imported.${importName}.${camelName(token)}`;
247384
247468
  }
247385
247469
  if (kind === "services")
247386
247470
  return `deps.${camelName(token)}`;
@@ -247441,6 +247525,42 @@ function validateGraph(graph, strict = false) {
247441
247525
  const warn2 = (code, message, file, line) => {
247442
247526
  diagnostics.push({ severity: strict ? "error" : "warn", code, message, file, line });
247443
247527
  };
247528
+ const modulesByName = new Map;
247529
+ const commandsByName = new Map;
247530
+ const routesByKey = new Map;
247531
+ for (const module of graph.modules) {
247532
+ const previousModule = modulesByName.get(module.name);
247533
+ if (previousModule) {
247534
+ error("duplicate-module", `模块名 ${module.name} 重复(首次声明于 ${previousModule.file}:${previousModule.line})`, module.file, module.line);
247535
+ } else {
247536
+ modulesByName.set(module.name, module);
247537
+ }
247538
+ for (const command of module.commands) {
247539
+ const previousName = commandsByName.get(command.name);
247540
+ if (previousName) {
247541
+ error("duplicate-command", `command 名 ${command.name} 重复(首次由模块 ${previousName.module.name} 的 ${previousName.className} 声明)`, module.file, module.line);
247542
+ } else {
247543
+ commandsByName.set(command.name, { module, className: command.className });
247544
+ }
247545
+ }
247546
+ }
247547
+ for (const module of graph.modules) {
247548
+ for (const controller of module.controllers) {
247549
+ for (const route of controller.routes) {
247550
+ const fullPath = joinRoutePaths(controller.path, route.path);
247551
+ const key = `${route.method} ${fullPath}`;
247552
+ const previous = routesByKey.get(key);
247553
+ if (previous) {
247554
+ error("duplicate-route", `路由 ${key} 重复(首次声明于模块 ${previous.module.name} 的 ${previous.controller.className})`, controller.file);
247555
+ } else {
247556
+ routesByKey.set(key, { module, controller });
247557
+ }
247558
+ if (route.command && !module.commands.some((command) => command.className === route.command)) {
247559
+ error("route-command-unresolved", `路由 ${key} 绑定的 command 类 ${route.command} 未在模块 ${module.name} 声明`, controller.file);
247560
+ }
247561
+ }
247562
+ }
247563
+ }
247444
247564
  for (const module of graph.modules) {
247445
247565
  const seen = new Map;
247446
247566
  for (const provider of module.providers) {
@@ -247472,13 +247592,18 @@ function validateGraph(graph, strict = false) {
247472
247592
  }
247473
247593
  for (const command of module.commands) {
247474
247594
  if (!command.permission) {
247475
- warn2("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
247595
+ error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
247476
247596
  }
247477
247597
  }
247478
247598
  }
247479
247599
  diagnostics.push(...detectCycles(graph, resolveDep));
247480
247600
  return diagnostics;
247481
247601
  }
247602
+ function joinRoutePaths(prefix, path) {
247603
+ const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
247604
+ const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
247605
+ return normalized.replace(/:[^/]+/g, ":param");
247606
+ }
247482
247607
  function detectCycles(graph, resolveDep) {
247483
247608
  const diagnostics = [];
247484
247609
  const nodeId = (ref) => `${ref.module.name}:${ref.provider.token}`;
@@ -247921,6 +248046,18 @@ JOIN pg_language l ON l.oid = p.prolang
247921
248046
  WHERE n.nspname = ANY($1)
247922
248047
  ORDER BY n.nspname, p.proname
247923
248048
  `;
248049
+ var TRIGGERS_SQL = `
248050
+ SELECT n.nspname AS schema,
248051
+ c.relname AS table,
248052
+ t.tgname AS name,
248053
+ t.tgenabled <> 'D' AS enabled
248054
+ FROM pg_trigger t
248055
+ JOIN pg_class c ON c.oid = t.tgrelid
248056
+ JOIN pg_namespace n ON n.oid = c.relnamespace
248057
+ WHERE NOT t.tgisinternal
248058
+ AND n.nspname = ANY($1)
248059
+ ORDER BY n.nspname, c.relname, t.tgname
248060
+ `;
247924
248061
  var GRANTS_SQL = `
247925
248062
  SELECT table_schema AS object_schema,
247926
248063
  table_name AS object_name,
@@ -247953,10 +248090,11 @@ function extractSearchPath(config) {
247953
248090
  }
247954
248091
  async function readCatalog(executor, schemas = ["public"]) {
247955
248092
  const params = [schemas];
247956
- const [tableRows, policyRows, functionRows, grantRows] = await Promise.all([
248093
+ const [tableRows, policyRows, functionRows, triggerRows, grantRows] = await Promise.all([
247957
248094
  executor.query(TABLES_SQL, params),
247958
248095
  executor.query(POLICIES_SQL, params),
247959
248096
  executor.query(FUNCTIONS_SQL, params),
248097
+ executor.query(TRIGGERS_SQL, params),
247960
248098
  executor.query(GRANTS_SQL, params)
247961
248099
  ]);
247962
248100
  return {
@@ -247982,6 +248120,12 @@ async function readCatalog(executor, schemas = ["public"]) {
247982
248120
  searchPath: extractSearchPath(row.config),
247983
248121
  language: row.language
247984
248122
  })),
248123
+ triggers: triggerRows.map((row) => ({
248124
+ schema: row.schema,
248125
+ table: row.table,
248126
+ name: row.name,
248127
+ enabled: row.enabled
248128
+ })),
247985
248129
  grants: grantRows.map((row) => ({
247986
248130
  objectSchema: row.object_schema,
247987
248131
  objectName: row.object_name,
@@ -248035,6 +248179,20 @@ function reconcileModule(module, catalog) {
248035
248179
  push("error", "definer-without-search-path", fn.name, `security definer 函数 ${fn.name} 未设置固定 search_path(当前: ${cf.searchPath ?? "未设置"})`);
248036
248180
  }
248037
248181
  }
248182
+ for (const trigger of module.triggers) {
248183
+ const [schema, table] = splitQualifiedName(trigger.table);
248184
+ const found = catalog.triggers.some((ct) => ct.schema === schema && ct.table === table && ct.name === trigger.name);
248185
+ if (!found) {
248186
+ push("error", "missing-trigger", `${trigger.table}.${trigger.name}`, `声明的触发器 ${trigger.name} 在表 ${trigger.table} 的 catalog 中不存在`);
248187
+ }
248188
+ }
248189
+ const declaredTriggerKeys = new Set(module.triggers.map((t) => `${t.table}::${t.name}`));
248190
+ for (const ct of catalog.triggers) {
248191
+ const qualified = `${ct.schema}.${ct.table}`;
248192
+ if (ownedTables.has(qualified) && !declaredTriggerKeys.has(`${qualified}::${ct.name}`)) {
248193
+ push("warn", "undeclared-trigger", `${qualified}.${ct.name}`, `归属表 ${qualified} 上存在未声明的触发器 ${ct.name}${ct.enabled ? "" : "(已禁用)"},可能发生漂移`);
248194
+ }
248195
+ }
248038
248196
  for (const table of module.tables) {
248039
248197
  const [schema, name] = splitQualifiedName(table);
248040
248198
  const ct = catalog.tables.find((t) => t.schema === schema && t.name === name);
@@ -248066,6 +248224,8 @@ var SET_SEARCH_PATH_RE = /\bset\s+search_path\b/i;
248066
248224
  var GRANT_TO_PUBLIC_RE = /\bgrant\b[^;]*\bto\s+public\b/i;
248067
248225
  var DROP_WITHOUT_IF_EXISTS_RE = /\bdrop\s+(?:table|column)\s+(?!if\s+exists\b)/i;
248068
248226
  var ENABLE_RLS_RE = /\benable\s+row\s+level\s+security\b/i;
248227
+ var CREATE_POLICY_RE = /\bcreate\s+policy\b/i;
248228
+ var DROP_POLICY_IF_EXISTS_RE = /\bdrop\s+policy\s+if\s+exists\b/i;
248069
248229
  function lineOf(sql, index) {
248070
248230
  let line = 1;
248071
248231
  for (let i = 0;i < index; i += 1) {
@@ -248106,6 +248266,19 @@ function lintSql(sql, file) {
248106
248266
  line: lineOf(sql, drop.index)
248107
248267
  });
248108
248268
  }
248269
+ const createPolicy = CREATE_POLICY_RE.exec(sql);
248270
+ if (createPolicy) {
248271
+ const dropPolicy = DROP_POLICY_IF_EXISTS_RE.exec(sql);
248272
+ if (!dropPolicy || dropPolicy.index > createPolicy.index) {
248273
+ issues.push({
248274
+ severity: "warn",
248275
+ code: "non-idempotent-policy",
248276
+ message: "create policy 前缺少 drop policy if exists,策略不可重复执行",
248277
+ file,
248278
+ line: lineOf(sql, createPolicy.index)
248279
+ });
248280
+ }
248281
+ }
248109
248282
  return issues;
248110
248283
  }
248111
248284
  async function lintModule(module, readFile2) {
@@ -250638,10 +250811,282 @@ function registerDeployTools(server2, http, options = {}) {
250638
250811
  }
250639
250812
  });
250640
250813
  }
250814
+
250815
+ // src/shared/tools/remote-dev-tools.ts
250816
+ import { spawn as spawn4 } from "node:child_process";
250817
+ import { existsSync as existsSync11 } from "node:fs";
250818
+ import { readFile as readFile3 } from "node:fs/promises";
250819
+ import { readdir } from "node:fs/promises";
250820
+ import { createHash as createHash5 } from "node:crypto";
250821
+ import { join as join10, resolve as resolve11 } from "node:path";
250822
+ var SAFE_TOKEN = /^[A-Za-z0-9._:@/+,-]+$/;
250823
+ var SAFE_REMOTE_ROOT = /^\/(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+\/?$/;
250824
+ var remoteDevToolSchema = {
250825
+ action: withDescription(stringEnum(["sync", "watch", "status", "migrate"]), "Remote development action"),
250826
+ target: optional(stringEnum(["db", "functions", "frontend", "project"]), "Sync target (default: project)"),
250827
+ project_dir: optional(Type.String(), "Local project directory (default: current directory)"),
250828
+ remote_root: optional(Type.String(), "Remote development root"),
250829
+ remote_host: optional(Type.String(), "Remote test server host"),
250830
+ remote_user: optional(Type.String(), "SSH user"),
250831
+ remote_port: optional(Type.Number(), "SSH port"),
250832
+ remote_key: optional(Type.String(), "SSH private key path"),
250833
+ function: optional(Type.String(), "Function slug"),
250834
+ delete: optional(Type.Boolean(), "Delete remote files absent locally"),
250835
+ reload: optional(Type.Boolean(), "Reload the affected target after sync (default: true)"),
250836
+ interval_ms: optional(Type.Number(), "Watch debounce interval in milliseconds (default: 300)"),
250837
+ json: optional(Type.Boolean(), "Emit machine-readable JSON"),
250838
+ apply: optional(Type.Boolean(), "Apply generated migrations to the selected test database"),
250839
+ drizzle_config: optional(Type.String(), "Drizzle config path"),
250840
+ migrations_dir: optional(Type.String(), "Migration directory"),
250841
+ drizzle_bin: optional(Type.String(), "drizzle-kit executable")
250842
+ };
250843
+ function runProcess(executable, args, cwd) {
250844
+ return new Promise((resolveResult, reject) => {
250845
+ const child = spawn4(executable, args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
250846
+ let stdout = "";
250847
+ let stderr = "";
250848
+ child.stdout?.setEncoding("utf8");
250849
+ child.stderr?.setEncoding("utf8");
250850
+ child.stdout?.on("data", (chunk) => {
250851
+ stdout += String(chunk);
250852
+ });
250853
+ child.stderr?.on("data", (chunk) => {
250854
+ stderr += String(chunk);
250855
+ });
250856
+ child.once("error", reject);
250857
+ child.once("close", (exitCode) => resolveResult({ exitCode: exitCode ?? 1, stdout, stderr }));
250858
+ });
250859
+ }
250860
+ function resolveDrizzleCommand(root, configured) {
250861
+ if (configured?.trim())
250862
+ return configured.trim();
250863
+ const local = join10(root, "node_modules", ".bin", "drizzle-kit");
250864
+ return existsSync11(local) ? local : "drizzle-kit";
250865
+ }
250866
+ function toolFailed(value) {
250867
+ return value?.isError === true || value?.content?.some((chunk) => typeof chunk?.text === "string" && chunk.text.trimStart().startsWith("❌"));
250868
+ }
250869
+ async function migrateDatabase(args, options) {
250870
+ const root = resolve11(String(args.project_dir || options.cwd || process.cwd()));
250871
+ const projectConfig = await readProjectConfig(root);
250872
+ const config = projectConfig.dev || {};
250873
+ const database = config.database || {};
250874
+ const execute = options.execute || ((command, commandArgs, cwd) => runProcess(command, commandArgs, cwd));
250875
+ const drizzleConfig = resolve11(root, String(args.drizzle_config || database.drizzleConfig || "drizzle.config.ts"));
250876
+ const migrationsDir = String(args.migrations_dir || database.migrationsDir || "supabase/migrations");
250877
+ if (!existsSync11(drizzleConfig))
250878
+ throw new Error(`Drizzle config not found: ${drizzleConfig}`);
250879
+ const generated = await execute(resolveDrizzleCommand(root, typeof args.drizzle_bin === "string" ? args.drizzle_bin : database.drizzleBin), ["generate", "--config", drizzleConfig], root);
250880
+ if (generated.exitCode !== 0)
250881
+ throw new Error(`Drizzle migration generation failed: ${generated.stderr.trim() || `exit ${generated.exitCode}`}`);
250882
+ if (!options.runDatabase)
250883
+ throw new Error("dev migrate requires Management API context");
250884
+ const dryRun = await options.runDatabase({ action: "push_migrations", dir: migrationsDir, dry_run: true, strict: database.strict !== false });
250885
+ if (toolFailed(dryRun))
250886
+ throw new Error("SupaCloud migration dry-run failed");
250887
+ if (args.apply !== true)
250888
+ return { ok: true, mode: "dev", action: "migrate", generated: true, applied: false, migrations_dir: migrationsDir, dry_run: dryRun };
250889
+ const applied = await options.runDatabase({ action: "push_migrations", dir: migrationsDir, strict: database.strict !== false });
250890
+ if (toolFailed(applied))
250891
+ throw new Error("SupaCloud migration apply failed");
250892
+ return { ok: true, mode: "dev", action: "migrate", generated: true, applied: true, migrations_dir: migrationsDir, result: applied };
250893
+ }
250894
+ async function readProjectConfig(root) {
250895
+ const file = join10(root, "supacloud.json");
250896
+ if (!existsSync11(file))
250897
+ return {};
250898
+ try {
250899
+ const parsed = JSON.parse(await readFile3(file, "utf8"));
250900
+ return parsed && typeof parsed === "object" ? parsed : {};
250901
+ } catch (error) {
250902
+ throw new Error(`Invalid supacloud.json: ${error instanceof Error ? error.message : String(error)}`);
250903
+ }
250904
+ }
250905
+ async function readDevConfig(root) {
250906
+ const config = await readProjectConfig(root);
250907
+ return config.dev || {};
250908
+ }
250909
+ async function sourceFingerprint(root) {
250910
+ const files = [];
250911
+ const visit = async (directory) => {
250912
+ const entries = await readdir(directory, { withFileTypes: true });
250913
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
250914
+ const path = join10(directory, entry.name);
250915
+ if (entry.isDirectory() && ![".git", "node_modules", "dist", "generated", ".supacloud"].includes(entry.name))
250916
+ await visit(path);
250917
+ else if (entry.isFile())
250918
+ files.push(path);
250919
+ }
250920
+ };
250921
+ await visit(root);
250922
+ const hash2 = createHash5("sha256");
250923
+ for (const file of files) {
250924
+ hash2.update(file.slice(root.length).replace(/\\/g, "/"));
250925
+ hash2.update(await readFile3(file));
250926
+ }
250927
+ return hash2.digest("hex");
250928
+ }
250929
+ function safeToken(value, label) {
250930
+ if (!value || !SAFE_TOKEN.test(value))
250931
+ throw new Error(`Invalid ${label}`);
250932
+ return value;
250933
+ }
250934
+ function remoteRoot(value) {
250935
+ const normalized = value.trim().replace(/\\/g, "/");
250936
+ if (!SAFE_REMOTE_ROOT.test(normalized) || normalized.includes(".."))
250937
+ throw new Error("Invalid remote_root");
250938
+ return normalized.replace(/\/$/, "");
250939
+ }
250940
+ function targetDirectory(root, target, functionSlug, config = {}) {
250941
+ if (target === "db")
250942
+ return join10(root, "supabase", "migrations");
250943
+ if (target === "functions") {
250944
+ const targets = config.targets && typeof config.targets === "object" ? config.targets : {};
250945
+ const match = Object.values(targets).find((entry) => entry?.type === "edge_function" && (!functionSlug || String(entry.slug || "") === functionSlug));
250946
+ const base = match?.root ? resolve11(root, String(match.root)) : join10(root, "supabase", "functions");
250947
+ return match?.root ? base : functionSlug ? join10(base, safeToken(functionSlug, "function")) : base;
250948
+ }
250949
+ if (target === "frontend") {
250950
+ const targets = config.targets && typeof config.targets === "object" ? config.targets : {};
250951
+ const match = Object.values(targets).find((entry) => entry?.type === "frontend");
250952
+ return match?.root ? resolve11(root, String(match.root)) : join10(root, "apps", "web");
250953
+ }
250954
+ return root;
250955
+ }
250956
+ function remoteTargetRoot(root, target, functionSlug) {
250957
+ if (target === "db")
250958
+ return `${root}/database/migrations`;
250959
+ if (target === "functions")
250960
+ return `${root}/functions/${functionSlug ? safeToken(functionSlug, "function") : ""}`.replace(/\/$/, "");
250961
+ if (target === "frontend")
250962
+ return `${root}/frontend`;
250963
+ return `${root}/project`;
250964
+ }
250965
+ function connectionArgs(options, config, args) {
250966
+ const host = String(args.remote_host || config.host || options.host || "").trim();
250967
+ const user = String(args.remote_user || config.user || options.sshUser || "").trim();
250968
+ const port = Number(args.remote_port || config.port || options.sshPort || 22);
250969
+ const key = String(args.remote_key || config.key || options.sshKey || "").trim();
250970
+ if (!host)
250971
+ throw new Error("Remote dev requires SUPACLOUD_HOST or --remote_host");
250972
+ safeToken(host, "remote_host");
250973
+ safeToken(user, "remote_user");
250974
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
250975
+ throw new Error("Invalid remote_port");
250976
+ if (!key || key.includes("\x00") || key.includes(`
250977
+ `))
250978
+ throw new Error("Invalid remote_key");
250979
+ return { host, user, port, key };
250980
+ }
250981
+ function targetConfigPath(target) {
250982
+ return target === "project" ? "project" : target;
250983
+ }
250984
+ function reloadCommand(config, target, projectRef2, functionSlug) {
250985
+ const command = config.reloadCommand?.trim() || "supacloud-dev-agent reload";
250986
+ if (!/^[A-Za-z0-9._/-]+(?: [A-Za-z0-9._:/=-]+)*$/.test(command))
250987
+ throw new Error("Invalid reloadCommand in supacloud.json");
250988
+ return [...command.split(" "), "--project-ref", safeToken(projectRef2 || "test", "project_ref"), "--target", targetConfigPath(target), ...functionSlug ? ["--function", safeToken(functionSlug, "function")] : []];
250989
+ }
250990
+ function sshArgs(connection, remoteCommand) {
250991
+ return ["-p", String(connection.port), "-i", resolve11(connection.key), "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=yes", `${connection.user}@${connection.host}`, ...remoteCommand];
250992
+ }
250993
+ function rsyncArgs(connection, source, destination, excludes, deleteRemote) {
250994
+ const args = ["-az", "--checksum", "--partial", "--protect-args", "-e", `ssh -p ${connection.port} -i ${resolve11(connection.key)} -o BatchMode=yes -o StrictHostKeyChecking=yes`];
250995
+ if (deleteRemote)
250996
+ args.push("--delete-delay");
250997
+ for (const exclude of excludes) {
250998
+ if (!/^\/?[A-Za-z0-9._*/-]+$/.test(exclude))
250999
+ throw new Error("Invalid dev exclude pattern");
251000
+ args.push("--exclude", exclude);
251001
+ }
251002
+ args.push(`${source.replace(/\/$/, "")}/`, `${connection.user}@${connection.host}:${destination.replace(/\/$/, "")}/`);
251003
+ return args;
251004
+ }
251005
+ async function syncOnce(args, options) {
251006
+ const root = resolve11(String(args.project_dir || options.cwd || process.cwd()));
251007
+ const projectConfig = await readProjectConfig(root);
251008
+ const config = projectConfig.dev || {};
251009
+ const target = String(args.target || "project");
251010
+ const source = targetDirectory(root, target, typeof args.function === "string" ? args.function : undefined, projectConfig);
251011
+ if (!existsSync11(source))
251012
+ throw new Error(`Dev source directory not found: ${source}`);
251013
+ let compiled = false;
251014
+ if (config.compile === true && target !== "db") {
251015
+ const compileRoot = resolve11(root, config.compileRoot || ".");
251016
+ const compileOutDir = resolve11(root, config.compileOutDir || "generated");
251017
+ const compilation = await compileProject({
251018
+ rootDir: compileRoot,
251019
+ outDir: compileOutDir,
251020
+ strict: config.compileStrict !== false
251021
+ });
251022
+ const errors = compilation.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
251023
+ if (errors.length > 0) {
251024
+ throw new Error(`DI compile failed: ${errors.map((diagnostic) => `${diagnostic.code} ${diagnostic.message}`).join("; ")}`);
251025
+ }
251026
+ compiled = true;
251027
+ }
251028
+ const remoteBase = remoteRoot(String(args.remote_root || config.remoteRoot || `/var/lib/supacloud/dev/${options.projectRef || "project"}`));
251029
+ const slug = typeof args.function === "string" ? args.function : undefined;
251030
+ const destination = remoteTargetRoot(remoteBase, target, slug);
251031
+ const connection = connectionArgs(options, config, args);
251032
+ const execute = options.execute || ((command, commandArgs, cwd) => runProcess(command, commandArgs, cwd));
251033
+ const mkdir3 = await execute("ssh", sshArgs(connection, ["mkdir", "-p", destination]), root);
251034
+ if (mkdir3.exitCode !== 0)
251035
+ throw new Error(`Remote dev prepare failed: ${mkdir3.stderr.trim() || `exit ${mkdir3.exitCode}`}`);
251036
+ const excludes = Array.isArray(config.excludes) ? config.excludes : ["node_modules", ".git", ".env*", "dist", ".supacloud"];
251037
+ const sync = await execute("rsync", rsyncArgs(connection, source, destination, excludes, args.delete === true), root);
251038
+ if (sync.exitCode !== 0)
251039
+ throw new Error(`Remote dev sync failed: ${sync.stderr.trim() || `exit ${sync.exitCode}`}`);
251040
+ const shouldReload = args.reload !== false;
251041
+ let reload = null;
251042
+ if (shouldReload) {
251043
+ reload = await execute("ssh", sshArgs(connection, reloadCommand(config, target, options.projectRef, slug)), root);
251044
+ if (reload.exitCode !== 0)
251045
+ throw new Error(`Remote dev reload failed: ${reload.stderr.trim() || `exit ${reload.exitCode}`}`);
251046
+ }
251047
+ return { ok: true, mode: "dev", action: "sync", environment: options.environment || null, target, source, destination, host: connection.host, compiled, reloaded: shouldReload };
251048
+ }
251049
+ function registerRemoteDevTools(server2, options = {}) {
251050
+ server2.tool("dev", "Remote test-server development sync. It never targets production and never syncs secrets.", remoteDevToolSchema, async (args) => {
251051
+ if (["production", "prod"].includes((options.environment || "").toLowerCase()))
251052
+ throw new Error("Remote dev mode is forbidden for production environments");
251053
+ if (args.action === "status") {
251054
+ const root = resolve11(String(args.project_dir || options.cwd || process.cwd()));
251055
+ const config = await readDevConfig(root);
251056
+ const connection = connectionArgs(options, config, args);
251057
+ const execute = options.execute || ((command, commandArgs, cwd) => runProcess(command, commandArgs, cwd));
251058
+ const status = await execute("ssh", sshArgs(connection, ["supacloud-dev-agent", "status", "--project-ref", safeToken(options.projectRef || "test", "project_ref")]), root);
251059
+ return { content: [{ type: "text", text: JSON.stringify({ ok: status.exitCode === 0, mode: "dev", action: "status", host: connection.host, output: status.stdout.trim(), error: status.stderr.trim() }, null, 2) }], isError: status.exitCode !== 0 };
251060
+ }
251061
+ if (args.action === "sync") {
251062
+ return { content: [{ type: "text", text: JSON.stringify(await syncOnce(args, options), null, 2) }] };
251063
+ }
251064
+ if (args.action === "migrate") {
251065
+ return { content: [{ type: "text", text: JSON.stringify(await migrateDatabase(args, options), null, 2) }] };
251066
+ }
251067
+ if (args.action === "watch") {
251068
+ const root = resolve11(String(args.project_dir || options.cwd || process.cwd()));
251069
+ const interval = Math.max(100, Math.min(1e4, Number(args.interval_ms || 300)));
251070
+ let fingerprint = "";
251071
+ let lastSync = null;
251072
+ for (;; ) {
251073
+ const nextFingerprint = await sourceFingerprint(root);
251074
+ if (nextFingerprint !== fingerprint) {
251075
+ lastSync = await syncOnce(args, options);
251076
+ fingerprint = nextFingerprint;
251077
+ process.stdout.write(`${JSON.stringify({ ...lastSync, watching: true })}
251078
+ `);
251079
+ }
251080
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, interval));
251081
+ }
251082
+ }
251083
+ throw new Error("Unsupported remote dev action");
251084
+ });
251085
+ }
250641
251086
  // package.json
250642
251087
  var package_default = {
250643
251088
  name: "@supacloud/cli",
250644
- version: "0.40.0",
251089
+ version: "0.42.0",
250645
251090
  description: "Project-scoped CLI for SupaCloud users",
250646
251091
  type: "module",
250647
251092
  main: "./dist/index.js",
@@ -250673,8 +251118,8 @@ var package_default = {
250673
251118
  },
250674
251119
  dependencies: {
250675
251120
  "@sinclair/typebox": "^0.34.52",
250676
- "@supacloud/compiler": "^0.1.0",
250677
- "@supacloud/db": "^0.1.0",
251121
+ "@supacloud/compiler": "^0.2.0",
251122
+ "@supacloud/db": "^0.2.0",
250678
251123
  fflate: "^0.8.3"
250679
251124
  },
250680
251125
  devDependencies: {
@@ -250892,6 +251337,11 @@ EXAMPLES
250892
251337
  ${preferredCommand} deploy
250893
251338
  ${preferredCommand} deploy --target web
250894
251339
  ${preferredCommand} deploy --target api
251340
+ ${preferredCommand} dev sync --env test --target functions --function api
251341
+ ${preferredCommand} dev status --env test
251342
+ ${preferredCommand} dev watch --env test --target project
251343
+ ${preferredCommand} dev sync --env test --target db
251344
+ ${preferredCommand} dev migrate --env test
250895
251345
  ${preferredCommand} project get
250896
251346
  ${preferredCommand} project logs --log_type database
250897
251347
  ${preferredCommand} project task_stats
@@ -251058,6 +251508,15 @@ function createCliTools(context, confirmProduction) {
251058
251508
  Object.assign(tools, captureTools((server2) => registerDatabaseTools(server2, undefined, {
251059
251509
  localOnly: true
251060
251510
  })));
251511
+ Object.assign(tools, captureTools((server2) => registerRemoteDevTools(server2, {
251512
+ cwd: process.cwd(),
251513
+ host: process.env.SUPACLOUD_DEV_HOST || context.host,
251514
+ sshUser: context.sshUser,
251515
+ sshPort: context.sshPort,
251516
+ sshKey: context.sshKey,
251517
+ projectRef: context.projectRef || undefined,
251518
+ environment: context.environment
251519
+ })));
251061
251520
  tools.setup_help = {
251062
251521
  schema: {},
251063
251522
  callback: async () => ({
@@ -251110,6 +251569,16 @@ function createCliTools(context, confirmProduction) {
251110
251569
  }));
251111
251570
  pushMigrations = databaseTools.database?.callback;
251112
251571
  assign(databaseTools);
251572
+ assign(captureTools((server2) => registerRemoteDevTools(server2, {
251573
+ cwd: process.cwd(),
251574
+ host: process.env.SUPACLOUD_DEV_HOST || context.host,
251575
+ sshUser: context.sshUser,
251576
+ sshPort: context.sshPort,
251577
+ sshKey: context.sshKey,
251578
+ projectRef: context.projectRef || undefined,
251579
+ environment: context.environment,
251580
+ runDatabase: databaseTools.database?.callback
251581
+ })));
251113
251582
  assign(captureTools((server2) => registerAuthTools(server2, http)));
251114
251583
  assign(captureTools((server2) => registerOAuthClientTools(server2, http)));
251115
251584
  assign(captureTools((server2) => registerStorageTools(server2, http)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@sinclair/typebox": "^0.34.52",
35
- "@supacloud/compiler": "^0.1.0",
36
- "@supacloud/db": "^0.1.0",
35
+ "@supacloud/compiler": "^0.2.0",
36
+ "@supacloud/db": "^0.2.0",
37
37
  "fflate": "^0.8.3"
38
38
  },
39
39
  "devDependencies": {