@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.
- package/dist/index.js +488 -19
- 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+"?
|
|
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:
|
|
246696
|
+
transaction: commandModeProp(meta, "transaction") ?? "none",
|
|
246639
246697
|
audit: stringLiteralProp(meta, "audit"),
|
|
246640
|
-
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(
|
|
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 `
|
|
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
|
-
|
|
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.
|
|
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.
|
|
250677
|
-
"@supacloud/db": "^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.
|
|
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.
|
|
36
|
-
"@supacloud/db": "^0.
|
|
35
|
+
"@supacloud/compiler": "^0.2.0",
|
|
36
|
+
"@supacloud/db": "^0.2.0",
|
|
37
37
|
"fflate": "^0.8.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|