gencow 0.1.182 → 0.1.185
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/lib/cli-project-runtime.mjs +14 -4
- package/lib/db-command.mjs +1 -0
- package/lib/db-migration-command-steps.mjs +32 -56
- package/lib/db-push-command.mjs +111 -35
- package/lib/deploy-correlation-diagnostic.mjs +7 -0
- package/lib/deploy-lockfile-selection.mjs +42 -0
- package/lib/deploy-package-claim.mjs +4 -6
- package/lib/deploy-package-runtime.mjs +27 -7
- package/lib/dev-cloud-bundle.mjs +27 -9
- package/lib/dev-cloud-command.mjs +13 -4
- package/lib/migration-advisory.mjs +41 -0
- package/lib/migration-diagnostic.mjs +81 -5
- package/lib/migration-operation-client.mjs +2 -1
- package/lib/project-config-selection.mjs +32 -0
- package/package.json +3 -3
- package/runtime/config.mjs +0 -3
- package/runtime/server.mjs +299 -96
- package/runtime/server.mjs.map +4 -4
- package/runtime/tooling.mjs +0 -3
|
@@ -13,12 +13,15 @@ import {
|
|
|
13
13
|
normalizeDiagnosticLogLines,
|
|
14
14
|
} from "./app-diagnostics.mjs";
|
|
15
15
|
import { buildDeployClaimFromArchive } from "./deploy-package-claim.mjs";
|
|
16
|
+
import { formatDeployCorrelationId } from "./deploy-correlation-diagnostic.mjs";
|
|
16
17
|
import { hasProjectDatabaseSchemaSource } from "./project-migration-manifest.mjs";
|
|
17
18
|
import {
|
|
18
19
|
extractActionableMigrationDiagnostic,
|
|
19
20
|
isActionableMigrationDiagnostic,
|
|
21
|
+
normalizeRetiredMigrationGateDiagnostic,
|
|
20
22
|
renderMigrationDiagnosticLines,
|
|
21
23
|
} from "./migration-diagnostic.mjs";
|
|
24
|
+
import { emitMigrationAdvisories, emitMigrationResponsibilityNotice } from "./migration-advisory.mjs";
|
|
22
25
|
import { resolveCreatedAppResponse } from "./app-create-response.mjs";
|
|
23
26
|
|
|
24
27
|
export function resolveDevCloudAppName({
|
|
@@ -259,6 +262,7 @@ export function createDevCloudCommand(deps) {
|
|
|
259
262
|
const form = new FormData();
|
|
260
263
|
form.append("bundle", new Blob([bundle], { type: "application/gzip" }), "bundle.tar.gz");
|
|
261
264
|
|
|
265
|
+
if (schemaExists) emitMigrationResponsibilityNotice(deps.warnImpl);
|
|
262
266
|
const res = await deps.platformFetchImpl(creds, `/platform/apps/${appName}/deploy`, {
|
|
263
267
|
method: "POST",
|
|
264
268
|
body: form,
|
|
@@ -276,7 +280,7 @@ export function createDevCloudCommand(deps) {
|
|
|
276
280
|
deps.logImpl(`${deps.DIM}${ts2}${deps.RESET} ${deps.RED}[deploy]${deps.RESET} ✗ Deploy failed!`);
|
|
277
281
|
const errMsg = data.error || "Unknown error";
|
|
278
282
|
const migrationDiagnostic = isActionableMigrationDiagnostic(data?.diagnostic)
|
|
279
|
-
? extractActionableMigrationDiagnostic(data.diagnostic)
|
|
283
|
+
? normalizeRetiredMigrationGateDiagnostic(extractActionableMigrationDiagnostic(data.diagnostic))
|
|
280
284
|
: null;
|
|
281
285
|
if (migrationDiagnostic) {
|
|
282
286
|
for (const line of renderMigrationDiagnosticLines(migrationDiagnostic)) {
|
|
@@ -284,6 +288,8 @@ export function createDevCloudCommand(deps) {
|
|
|
284
288
|
}
|
|
285
289
|
} else {
|
|
286
290
|
deps.logImpl(` ${deps.RED}${errMsg}${deps.RESET}`);
|
|
291
|
+
const correlationLine = formatDeployCorrelationId(data?.correlationId);
|
|
292
|
+
if (correlationLine) deps.logImpl(` ${correlationLine}`);
|
|
287
293
|
}
|
|
288
294
|
|
|
289
295
|
let diagnosticLines = normalizeDiagnosticLogLines(data.crashLogs);
|
|
@@ -315,6 +321,7 @@ export function createDevCloudCommand(deps) {
|
|
|
315
321
|
return;
|
|
316
322
|
}
|
|
317
323
|
|
|
324
|
+
emitMigrationAdvisories(data?.warnings, deps.warnImpl);
|
|
318
325
|
const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
319
326
|
deps.logImpl(
|
|
320
327
|
`${deps.DIM}${ts2}${deps.RESET} ${deps.GREEN}[deploy]${deps.RESET} ✔ Deploy complete ${deps.DIM}(${sizeKB}KB, ${elapsed}s)${deps.RESET}`,
|
|
@@ -395,10 +402,12 @@ export function createDevCloudCommand(deps) {
|
|
|
395
402
|
}, delayMs);
|
|
396
403
|
}
|
|
397
404
|
|
|
398
|
-
const
|
|
405
|
+
const fsModule = deps.readdirSyncImpl && deps.watchImpl ? null : await import("fs");
|
|
406
|
+
const readdirSyncImpl = deps.readdirSyncImpl ?? fsModule.readdirSync;
|
|
407
|
+
const watchImpl = deps.watchImpl ?? fsModule.watch;
|
|
399
408
|
const watchedFiles = [];
|
|
400
409
|
try {
|
|
401
|
-
const files =
|
|
410
|
+
const files = readdirSyncImpl(absoluteFunctions).filter(
|
|
402
411
|
(file) => file.endsWith(".ts") && !file.startsWith(".") && file !== "api.ts",
|
|
403
412
|
);
|
|
404
413
|
watchedFiles.push(...files);
|
|
@@ -428,7 +437,7 @@ ${deps.BOLD}${deps.CYAN}🚀 Gencow Cloud Dev${deps.RESET}
|
|
|
428
437
|
let debounceTimer = null;
|
|
429
438
|
let pendingFile = null;
|
|
430
439
|
const GENERATED_FILES = new Set(["api.ts", "README.md"]);
|
|
431
|
-
|
|
440
|
+
watchImpl(absoluteFunctions, { recursive: true }, (_eventType, filename) => {
|
|
432
441
|
if (!filename || filename.includes("node_modules")) return;
|
|
433
442
|
if (isServerCodegenWatchPath(filename, cwd, config, { resolvePathImpl: deps.resolvePathImpl })) return;
|
|
434
443
|
const base = (filename.includes("/") ? filename.split("/").pop() : filename) ?? filename;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const ADVISORY_SUMMARIES = Object.freeze({
|
|
2
|
+
MIGRATION_POTENTIAL_DATA_LOSS: "The pending migration may remove tenant data.",
|
|
3
|
+
MIGRATION_LOCK_RISK: "The pending migration may take a blocking PostgreSQL lock.",
|
|
4
|
+
MIGRATION_COMPATIBILITY_RISK: "Coordinate this schema change with application rollout compatibility.",
|
|
5
|
+
MIGRATION_BACKUP_NOT_CREATED:
|
|
6
|
+
"Gencow could not create an automatic backup; verify your own recovery point.",
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function extractMigrationAdvisories(value) {
|
|
10
|
+
if (!Array.isArray(value)) return [];
|
|
11
|
+
const advisories = [];
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
for (const item of value.slice(0, 16)) {
|
|
14
|
+
const code = typeof item?.code === "string" ? item.code : "";
|
|
15
|
+
if (!(code in ADVISORY_SUMMARIES) || seen.has(code)) continue;
|
|
16
|
+
seen.add(code);
|
|
17
|
+
advisories.push({
|
|
18
|
+
code,
|
|
19
|
+
severity: "warning",
|
|
20
|
+
responsibility: "user",
|
|
21
|
+
summary: ADVISORY_SUMMARIES[code],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return advisories;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function emitMigrationAdvisories(value, warnImpl) {
|
|
28
|
+
const advisories = extractMigrationAdvisories(value);
|
|
29
|
+
for (const advisory of advisories) {
|
|
30
|
+
warnImpl(
|
|
31
|
+
`Migration warning [${advisory.code}]: ${advisory.summary} Review and proceed at your discretion.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return advisories;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function emitMigrationResponsibilityNotice(warnImpl) {
|
|
38
|
+
warnImpl(
|
|
39
|
+
"Migration responsibility: review pending SQL and verify your own recovery point before execution; automatic backups are best-effort.",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -8,6 +8,56 @@ export {
|
|
|
8
8
|
isActionableMigrationDiagnostic,
|
|
9
9
|
} from "./migration-diagnostic-contract.mjs";
|
|
10
10
|
|
|
11
|
+
const RETIRED_PLATFORM_GATES = new Set([
|
|
12
|
+
"MIGRATION_BUNDLE_GENERATOR_UNSUPPORTED",
|
|
13
|
+
"MIGRATION_SQL_DESTRUCTIVE",
|
|
14
|
+
"MIGRATION_SQL_UNSUPPORTED",
|
|
15
|
+
"PRE_DEPLOY_SAFETY_BACKUP_REQUIRED",
|
|
16
|
+
"MIGRATION_CHECK_STATE_UNAVAILABLE",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export function normalizeRetiredMigrationGateDiagnostic(diagnostic) {
|
|
20
|
+
if (
|
|
21
|
+
!diagnostic ||
|
|
22
|
+
!RETIRED_PLATFORM_GATES.has(diagnostic.code) ||
|
|
23
|
+
diagnostic.databaseState !== "UNCHANGED" ||
|
|
24
|
+
!["PREFLIGHT", "AUDIT", "REHEARSAL", "VERIFY"].includes(diagnostic.phase)
|
|
25
|
+
) {
|
|
26
|
+
return diagnostic;
|
|
27
|
+
}
|
|
28
|
+
if (
|
|
29
|
+
diagnostic.failureClass === "RETRYABLE_INFRA" &&
|
|
30
|
+
diagnostic.retryPolicy === "AFTER_DELAY" &&
|
|
31
|
+
diagnostic.nextAction?.kind === "WAIT"
|
|
32
|
+
) {
|
|
33
|
+
return diagnostic;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
diagnosticVersion: 1,
|
|
37
|
+
code: diagnostic.code,
|
|
38
|
+
failureClass: "RETRYABLE_INFRA",
|
|
39
|
+
phase: diagnostic.phase,
|
|
40
|
+
databaseState: "UNCHANGED",
|
|
41
|
+
safeToRetryNow: false,
|
|
42
|
+
retryPolicy: "AFTER_DELAY",
|
|
43
|
+
retryAfterSeconds: 15,
|
|
44
|
+
retryCondition: "Retry the same immutable bundle after the Blue-Green platform rollout converges.",
|
|
45
|
+
correlationId: diagnostic.correlationId,
|
|
46
|
+
reason: {
|
|
47
|
+
code: "retired_platform_gate",
|
|
48
|
+
summary: "An older platform slot applied a migration gate that is no longer enforced.",
|
|
49
|
+
detail:
|
|
50
|
+
"No tenant database mutation occurred. Keep the project unchanged while the current platform policy finishes rolling out.",
|
|
51
|
+
},
|
|
52
|
+
suggestedFixes: [],
|
|
53
|
+
nextAction: { kind: "WAIT" },
|
|
54
|
+
forbiddenActions: [],
|
|
55
|
+
redaction: { serverReturnedRawSql: false, localExcerpt: "NONE" },
|
|
56
|
+
...(diagnostic.docsUrl ? { docsUrl: diagnostic.docsUrl } : {}),
|
|
57
|
+
...(diagnostic.supportedCliRange ? { supportedCliRange: diagnostic.supportedCliRange } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
11
61
|
function skipSingleQuoted(sql, start) {
|
|
12
62
|
let index = start + 1;
|
|
13
63
|
while (index < sql.length) {
|
|
@@ -161,6 +211,7 @@ export function enrichMigrationDiagnostic(
|
|
|
161
211
|
projectRoot,
|
|
162
212
|
functionsDir,
|
|
163
213
|
retryCommand,
|
|
214
|
+
inspectionCommand,
|
|
164
215
|
readFileSyncImpl = readFileSync,
|
|
165
216
|
realpathSyncImpl = realpathSync,
|
|
166
217
|
},
|
|
@@ -171,9 +222,12 @@ export function enrichMigrationDiagnostic(
|
|
|
171
222
|
diagnostic.safeToRetryNow === true &&
|
|
172
223
|
diagnostic.retryPolicy !== "BLOCKED" &&
|
|
173
224
|
(diagnostic.nextAction?.kind === "EDIT_AND_CHECK" || diagnostic.nextAction?.kind === "RETRY");
|
|
225
|
+
const canAttachInspectionCommand = inspectionCommand && diagnostic.nextAction?.kind === "INSPECT_STATE";
|
|
174
226
|
const nextAction = canAttachRetryCommand
|
|
175
227
|
? { kind: diagnostic.nextAction.kind, command: retryCommand }
|
|
176
|
-
:
|
|
228
|
+
: canAttachInspectionCommand
|
|
229
|
+
? { kind: "INSPECT_STATE", command: inspectionCommand }
|
|
230
|
+
: { kind: diagnostic.nextAction?.kind };
|
|
177
231
|
const base = {
|
|
178
232
|
...diagnostic,
|
|
179
233
|
...(diagnostic.migration
|
|
@@ -230,9 +284,30 @@ export function enrichMigrationDiagnostic(
|
|
|
230
284
|
}
|
|
231
285
|
}
|
|
232
286
|
|
|
287
|
+
function migrationDiagnosticTitle(diagnostic) {
|
|
288
|
+
if (diagnostic.failureClass === "RECOVERY_REQUIRED" || diagnostic.databaseState === "RECOVERY_REQUIRED") {
|
|
289
|
+
return "Migration outcome verification required";
|
|
290
|
+
}
|
|
291
|
+
if (diagnostic.databaseState === "ROLLED_BACK") return "Migration rolled back";
|
|
292
|
+
if (diagnostic.failureClass === "USER_FIXABLE") return "Migration input needs attention";
|
|
293
|
+
if (diagnostic.code === "SCHEMA_PUSH_IN_PROGRESS") return "Migration waiting for an active operation";
|
|
294
|
+
if (diagnostic.nextAction?.kind === "INSPECT_STATE") return "Migration state inspection required";
|
|
295
|
+
if (diagnostic.failureClass === "RETRYABLE_INFRA") return "Migration check temporarily unavailable";
|
|
296
|
+
return "Migration action required";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function shouldRenderForbiddenActions(diagnostic) {
|
|
300
|
+
return (
|
|
301
|
+
diagnostic.failureClass === "RECOVERY_REQUIRED" ||
|
|
302
|
+
/(?:HASH_DRIFT|FILE_MISSING|HISTORY|JOURNAL|POLICY_VIOLATION|SECURITY_BOUNDARY)/u.test(
|
|
303
|
+
diagnostic.code ?? "",
|
|
304
|
+
)
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
233
308
|
export function renderMigrationDiagnosticLines(diagnostic) {
|
|
234
309
|
const lines = [
|
|
235
|
-
|
|
310
|
+
migrationDiagnosticTitle(diagnostic),
|
|
236
311
|
"",
|
|
237
312
|
`Code: ${diagnostic.code}`,
|
|
238
313
|
`Responsibility: ${diagnostic.failureClass}`,
|
|
@@ -267,12 +342,13 @@ export function renderMigrationDiagnosticLines(diagnostic) {
|
|
|
267
342
|
lines.push(` ${index + 1}. ${fix.title}`, ` ${fix.detail}`),
|
|
268
343
|
);
|
|
269
344
|
}
|
|
270
|
-
if (diagnostic.forbiddenActions?.length > 0) {
|
|
345
|
+
if (shouldRenderForbiddenActions(diagnostic) && diagnostic.forbiddenActions?.length > 0) {
|
|
271
346
|
lines.push("", "Do not:");
|
|
272
347
|
diagnostic.forbiddenActions.forEach((action) => lines.push(` - ${action}`));
|
|
273
348
|
}
|
|
274
349
|
const nextText =
|
|
275
|
-
diagnostic.nextAction?.kind === "VIEW_STATUS"
|
|
350
|
+
(diagnostic.nextAction?.kind === "VIEW_STATUS" || diagnostic.nextAction?.kind === "INSPECT_STATE") &&
|
|
351
|
+
diagnostic.nextAction?.command
|
|
276
352
|
? diagnostic.nextAction.command
|
|
277
353
|
: diagnostic.safeToRetryNow && diagnostic.nextAction?.command
|
|
278
354
|
? diagnostic.nextAction.command
|
|
@@ -282,7 +358,7 @@ export function renderMigrationDiagnosticLines(diagnostic) {
|
|
|
282
358
|
INSPECT_STATE:
|
|
283
359
|
diagnostic.code === "MIGRATION_LOCAL_EXECUTION_FAILED"
|
|
284
360
|
? "Inspect the local Drizzle journal and database catalog before retrying"
|
|
285
|
-
: "Run read-only
|
|
361
|
+
: "Run the platform's read-only migration-state inspection before any new mutation request",
|
|
286
362
|
UPDATE_CLI: "Update the CLI to the exact supported range shown above",
|
|
287
363
|
VIEW_STATUS: "View the exact operation receipt before any new mutation request",
|
|
288
364
|
RETRY: "Regenerate a stable bundle and retry from a fresh plan",
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
extractMigrationSchemaOperationReceipt,
|
|
4
4
|
extractPrimaryMigrationExecutionResult,
|
|
5
5
|
} from "@gencow/migration-contract";
|
|
6
|
+
import { extractMigrationAdvisories } from "./migration-advisory.mjs";
|
|
6
7
|
|
|
7
8
|
const RECEIPT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
8
9
|
|
|
@@ -53,7 +54,7 @@ export function extractMigrationPushSuccess(data, expected) {
|
|
|
53
54
|
) {
|
|
54
55
|
return null;
|
|
55
56
|
}
|
|
56
|
-
return { migration, receipt };
|
|
57
|
+
return { migration, receipt, warnings: extractMigrationAdvisories(data?.warnings) };
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export async function fetchMigrationOperationStatus({
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const PROJECT_CONFIG_CANDIDATES = Object.freeze(["gencow.config.js", "gencow.config.ts"]);
|
|
2
|
+
|
|
3
|
+
const reportedSelections = new Set();
|
|
4
|
+
|
|
5
|
+
export function selectProjectConfigFile(availableFiles) {
|
|
6
|
+
const available = new Set(availableFiles);
|
|
7
|
+
const detectedFiles = PROJECT_CONFIG_CANDIDATES.filter((file) => available.has(file));
|
|
8
|
+
return {
|
|
9
|
+
selectedFile: detectedFiles[0] ?? null,
|
|
10
|
+
ignoredFiles: detectedFiles.slice(1),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl }) {
|
|
15
|
+
return selectProjectConfigFile(
|
|
16
|
+
PROJECT_CONFIG_CANDIDATES.filter((file) => existsSyncImpl(resolvePathImpl(cwd, file))),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatProjectConfigSelectionWarning(selection) {
|
|
21
|
+
if (!selection.selectedFile || selection.ignoredFiles.length === 0) return null;
|
|
22
|
+
return `[gencow] Multiple project config files found. Using \`${selection.selectedFile}\`; ignoring deprecated \`${selection.ignoredFiles.join("`, `")}\`.`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function warnProjectConfigSelectionOnce({ cwd, selection, warnImpl }) {
|
|
26
|
+
const warning = formatProjectConfigSelectionWarning(selection);
|
|
27
|
+
if (!warning) return;
|
|
28
|
+
const key = `${cwd}\0${selection.selectedFile}\0${selection.ignoredFiles.join("\0")}`;
|
|
29
|
+
if (reportedSelections.has(key)) return;
|
|
30
|
+
reportedSelections.add(key);
|
|
31
|
+
warnImpl(warning);
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gencow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.185",
|
|
4
4
|
"description": "Gencow — AI Backend Engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"drizzle-orm": "1.0.0-rc.4",
|
|
25
25
|
"esbuild": "^0.27.7",
|
|
26
26
|
"open": "^10.2.0",
|
|
27
|
-
"tar": "
|
|
27
|
+
"tar": "7.5.15",
|
|
28
28
|
"ws": "^8.21.1",
|
|
29
29
|
"zod": "^4.4.3",
|
|
30
|
-
"@gencow/migration-contract": "0.1.
|
|
30
|
+
"@gencow/migration-contract": "0.1.8"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^25.9.5",
|
package/runtime/config.mjs
CHANGED
|
@@ -14611,9 +14611,6 @@ var gencowConfigSchema = rawConfigSchema.transform((v) => {
|
|
|
14611
14611
|
const trimPath = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
14612
14612
|
const rootDir = trimPath(v.rootDir);
|
|
14613
14613
|
const functionsDir = trimPath(v.functionsDir);
|
|
14614
|
-
if (rootDir && functionsDir) {
|
|
14615
|
-
throw new Error("[gencow] Invalid gencow.config: set either `rootDir` or `functionsDir`, not both.");
|
|
14616
|
-
}
|
|
14617
14614
|
const resolvedRootDir = rootDir ?? functionsDir ?? GENCOW_CONFIG_DEFAULTS.rootDir;
|
|
14618
14615
|
const envFile = trimPath(v.envFile) ?? `${resolvedRootDir.replace(/[\\/]+$/, "")}/.env`;
|
|
14619
14616
|
const clientCodegenDir = trimPath(v.codegen?.clientOutDir) ?? trimPath(v.codegen?.outDir) ?? GENCOW_CONFIG_DEFAULTS.codegen.clientOutDir;
|