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.
@@ -13,6 +13,7 @@ import { resolveCodegenServerPublishDir } from "./codegen-command.mjs";
13
13
  import { logEnvFileOperationWithType, resolveBackendEnvFile } from "./backend-env-resolver.mjs";
14
14
  import { parseEnvFile } from "./env-parser.mjs";
15
15
  import { BOLD, DIM, RESET, error, info, success, warn } from "./output.mjs";
16
+ import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
16
17
  import { hasWorkspaceConfigSourceRuntime } from "./runtime-mode.mjs";
17
18
  import { SUPPORTED_DRIZZLE_KIT_GENERATOR_VERSION } from "./drizzle-generate.mjs";
18
19
 
@@ -244,9 +245,9 @@ export async function loadConfig(options = {}) {
244
245
 
245
246
  // Prefer gencow.config.js. gencow.config.ts is a legacy path handled by a
246
247
  // regex parser; it will be removed once all projects migrate.
247
- const jsPath = resolvePathImpl(cwd, "gencow.config.js");
248
- const tsPath = resolvePathImpl(cwd, "gencow.config.ts");
249
- const path = existsSyncImpl(jsPath) ? jsPath : existsSyncImpl(tsPath) ? tsPath : null;
248
+ const configSelection = detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl });
249
+ warnProjectConfigSelectionOnce({ cwd, selection: configSelection, warnImpl });
250
+ const path = configSelection.selectedFile ? resolvePathImpl(cwd, configSelection.selectedFile) : null;
250
251
 
251
252
  if (!path) {
252
253
  warnImpl("No gencow.config.js found — using defaults");
@@ -286,10 +287,19 @@ export async function loadConfig(options = {}) {
286
287
  let parsed;
287
288
  try {
288
289
  const trimPath = (value) => (typeof value === "string" && value.trim().length > 0 ? value.trim() : null);
289
- if (!trimPath(raw?.rootDir) && trimPath(raw?.functionsDir) && !functionsDirDeprecationWarned) {
290
+ const rootDir = trimPath(raw?.rootDir);
291
+ const functionsDir = trimPath(raw?.functionsDir);
292
+ if (rootDir && functionsDir) {
293
+ warnImpl("[gencow] Both config paths are set. Using `rootDir`; ignoring deprecated `functionsDir`.");
294
+ } else if (!rootDir && functionsDir && !functionsDirDeprecationWarned) {
290
295
  warnImpl("[gencow] `functionsDir` is deprecated; use `rootDir` instead (same meaning).");
291
296
  functionsDirDeprecationWarned = true;
292
297
  }
298
+ if (trimPath(raw?.codegen?.clientOutDir) && trimPath(raw?.codegen?.outDir)) {
299
+ warnImpl(
300
+ "[gencow] Both codegen output paths are set. Using `codegen.clientOutDir`; ignoring deprecated `codegen.outDir`.",
301
+ );
302
+ }
293
303
  parsed = gencowConfigSchema.parse(raw ?? {});
294
304
  } catch (caught) {
295
305
  const message = `Invalid gencow.config: ${caught.message}`;
@@ -115,6 +115,7 @@ function renderDbCheckHelp(logImpl = log) {
115
115
  logImpl(` ${BOLD}Options:${RESET}`);
116
116
  logImpl(` ${DIM}--prod${RESET} Check the production DB (no mutation or confirmation)`);
117
117
  logImpl(` ${DIM}--app, -a${RESET} Target specific app`);
118
+ logImpl(` ${DIM}--inspect-state${RESET} Run the dedicated platform read-only state inspection`);
118
119
  logImpl(` ${DIM}--json${RESET} Print a stable machine-readable result\n`);
119
120
  logImpl(` ${DIM}db:check inspects committed migrations and never runs drizzle-kit.${RESET}\n`);
120
121
  }
@@ -1,21 +1,12 @@
1
- import {
2
- MIGRATION_PUSH_RESULT_VERSION,
3
- isCanonicalMigrationCapabilityManifestV1,
4
- } from "@gencow/migration-contract";
5
1
  import {
6
2
  buildClientUserFixableDiagnostic,
7
3
  buildLocalExecutionFailureDiagnostic,
8
- buildMigrationCapabilityPreflightUnsupportedDiagnostic,
9
4
  } from "./migration-client-diagnostic.mjs";
10
5
  import { resolveBackendEnvFile } from "./backend-env-resolver.mjs";
11
- import {
12
- buildMigrationNoResponseDiagnostic,
13
- enrichMigrationDiagnostic,
14
- extractActionableMigrationDiagnostic,
15
- isActionableMigrationDiagnostic,
16
- } from "./migration-diagnostic.mjs";
6
+ import { buildMigrationNoResponseDiagnostic } from "./migration-diagnostic.mjs";
17
7
  import { buildMigrationStatusCommand, fetchMigrationOperationStatus } from "./migration-operation-client.mjs";
18
8
  import { BOLD, CYAN, DIM, RED, RESET } from "./output.mjs";
9
+ import { emitMigrationAdvisories, extractMigrationAdvisories } from "./migration-advisory.mjs";
19
10
 
20
11
  const NO_CHANGE_MARKERS = ["No schema changes", "nothing to migrate", "No changes detected"];
21
12
 
@@ -146,50 +137,6 @@ export async function handleMigrationStatus({
146
137
  }
147
138
  }
148
139
 
149
- export async function checkMigrationCapability({
150
- deps,
151
- creds,
152
- appId,
153
- isCheck,
154
- functionsDir,
155
- retryCommand,
156
- correlationId,
157
- emitFailure,
158
- }) {
159
- try {
160
- const response = await deps.platformFetchImpl(creds, `/platform/apps/${appId}/migration-capabilities`, {
161
- method: "GET",
162
- headers: { "X-Gencow-CLI-Version": deps.cliVersion },
163
- });
164
- const data = await response.json().catch(() => ({}));
165
- if (!response.ok) {
166
- emitFailure(
167
- isActionableMigrationDiagnostic(data)
168
- ? enrichMigrationDiagnostic(extractActionableMigrationDiagnostic(data), {
169
- projectRoot: deps.cwdImpl(),
170
- functionsDir,
171
- retryCommand,
172
- })
173
- : buildMigrationCapabilityPreflightUnsupportedDiagnostic(correlationId()),
174
- );
175
- return false;
176
- }
177
- if (
178
- data?.success !== true ||
179
- data?.app !== appId ||
180
- !isCanonicalMigrationCapabilityManifestV1(data?.capability, deps.cliVersion) ||
181
- (!isCheck && data?.pushResultVersion !== MIGRATION_PUSH_RESULT_VERSION)
182
- ) {
183
- emitFailure(buildMigrationCapabilityPreflightUnsupportedDiagnostic(correlationId()));
184
- return false;
185
- }
186
- return true;
187
- } catch {
188
- emitFailure(buildMigrationCapabilityPreflightUnsupportedDiagnostic(correlationId()));
189
- return false;
190
- }
191
- }
192
-
193
140
  export function emitCloudMigrationSuccess({
194
141
  deps,
195
142
  command,
@@ -198,11 +145,13 @@ export function emitCloudMigrationSuccess({
198
145
  existingBundleMode,
199
146
  isCheck,
200
147
  checkData,
148
+ isInspection,
201
149
  pushSuccess,
202
150
  jsonMode,
203
151
  emitJson,
204
152
  }) {
205
153
  const migration = pushSuccess?.migration ?? null;
154
+ const warnings = extractMigrationAdvisories(isCheck ? checkData?.plan?.advisories : pushSuccess?.warnings);
206
155
  if (jsonMode) {
207
156
  emitJson({
208
157
  diagnosticVersion: 1,
@@ -212,17 +161,43 @@ export function emitCloudMigrationSuccess({
212
161
  environment,
213
162
  databaseState: isCheck ? "UNCHANGED" : "APPLIED",
214
163
  ...(existingBundleMode ? { projectState: "UNCHANGED" } : {}),
215
- ...(isCheck ? { plan: checkData.plan, capability: checkData.capability } : {}),
164
+ ...(isCheck
165
+ ? isInspection
166
+ ? { inspection: checkData.inspection, capability: checkData.capability }
167
+ : { plan: checkData.plan, capability: checkData.capability }
168
+ : {}),
216
169
  ...(migration ? { migration } : {}),
217
170
  ...(pushSuccess ? { receiptId: pushSuccess.receipt.receiptId } : {}),
171
+ ...(warnings.length > 0 ? { warnings } : {}),
218
172
  });
219
173
  return;
220
174
  }
221
175
  if (isCheck) {
176
+ if (isInspection) {
177
+ const ready = checkData.inspection.outcome === "READY";
178
+ (ready ? deps.successImpl : deps.warnImpl)(
179
+ ready
180
+ ? "Migration state inspection completed; the plan is ready."
181
+ : "Migration state inspection completed; the plan remains blocked.",
182
+ );
183
+ deps.logImpl(
184
+ ` ${DIM}Code: ${checkData.inspection.code} · Next: ${checkData.inspection.nextAction} · Database unchanged${RESET}\n`,
185
+ );
186
+ return;
187
+ }
222
188
  deps.successImpl("Migration plan is safe to proceed.");
189
+ if (checkData.plan.databaseBootstrapRequired === true) {
190
+ deps.logImpl(` ${DIM}Fresh database will be bootstrapped by deploy or db:push.${RESET}`);
191
+ }
192
+ if (checkData.plan.migrationStorePreparationRequired === true) {
193
+ deps.logImpl(
194
+ ` ${DIM}Gencow will prepare the classified migration store during deploy or db:push.${RESET}`,
195
+ );
196
+ }
223
197
  deps.logImpl(
224
198
  ` ${DIM}Applied verified: ${checkData.plan.appliedVerifiedCount} · Pending: ${checkData.plan.pendingCount} · Database unchanged${RESET}\n`,
225
199
  );
200
+ emitMigrationAdvisories(warnings, deps.warnImpl);
226
201
  return;
227
202
  }
228
203
  deps.successImpl(
@@ -231,4 +206,5 @@ export function emitCloudMigrationSuccess({
231
206
  : "Versioned migrations applied.",
232
207
  );
233
208
  deps.logImpl(` ${DIM}Receipt: ${pushSuccess.receipt.receiptId}${RESET}\n`);
209
+ emitMigrationAdvisories(warnings, deps.warnImpl);
234
210
  }
@@ -1,4 +1,7 @@
1
- import { isCanonicalMigrationCapabilityManifestV1 } from "@gencow/migration-contract";
1
+ import {
2
+ MIGRATION_NEXT_ACTION_KINDS,
3
+ isCompatibleMigrationCapabilityManifestV1,
4
+ } from "@gencow/migration-contract";
2
5
  import {
3
6
  resolveDrizzleKitGeneratorVersion,
4
7
  runDrizzleGenerateWithFallback,
@@ -12,6 +15,7 @@ import {
12
15
  enrichMigrationDiagnostic,
13
16
  extractActionableMigrationDiagnostic,
14
17
  isActionableMigrationDiagnostic,
18
+ normalizeRetiredMigrationGateDiagnostic,
15
19
  renderMigrationDiagnosticLines,
16
20
  } from "./migration-diagnostic.mjs";
17
21
  import {
@@ -31,7 +35,6 @@ import {
31
35
  readMigrationStatusReceiptId,
32
36
  } from "./migration-operation-client.mjs";
33
37
  import {
34
- checkMigrationCapability,
35
38
  emitCloudMigrationSuccess,
36
39
  handleLocalMigrationCommand,
37
40
  handleMigrationStatus,
@@ -39,6 +42,7 @@ import {
39
42
  promptDbPushConfirm,
40
43
  } from "./db-migration-command-steps.mjs";
41
44
  import { resolveConfiguredSchemaPaths, resolveDrizzleSchemaPaths } from "./cli-schema-paths.mjs";
45
+ import { emitMigrationResponsibilityNotice } from "./migration-advisory.mjs";
42
46
 
43
47
  function clientCorrelationId() {
44
48
  return globalThis.crypto?.randomUUID?.() ?? `client-${Date.now()}`;
@@ -65,6 +69,16 @@ export function buildDbMigrationRetryCommand({ command, appId, args, isProd, jso
65
69
  if (explicitAppRequested(args)) parts.push("--app", shellQuote(appId));
66
70
  if (isProd) parts.push("--prod");
67
71
  if (args.includes("--existing-bundle")) parts.push("--existing-bundle");
72
+ if (args.includes("--inspect-state")) parts.push("--inspect-state");
73
+ if (jsonMode) parts.push("--json");
74
+ return parts.join(" ");
75
+ }
76
+
77
+ export function buildDbMigrationInspectionCommand({ appId, args, isProd, jsonMode }) {
78
+ const parts = ["gencow", "db:check"];
79
+ if (explicitAppRequested(args)) parts.push("--app", shellQuote(appId));
80
+ if (isProd) parts.push("--prod");
81
+ parts.push("--inspect-state");
68
82
  if (jsonMode) parts.push("--json");
69
83
  return parts.join(" ");
70
84
  }
@@ -75,9 +89,65 @@ function renderFailure(diagnostic, { errorImpl, logImpl }) {
75
89
  lines.slice(1).forEach((line) => logImpl(line));
76
90
  }
77
91
 
92
+ function extractReadOnlyMigrationInspection(data) {
93
+ const inspection = data?.inspection;
94
+ if (
95
+ inspection?.readOnly !== true ||
96
+ !["READY", "BLOCKED"].includes(inspection.outcome) ||
97
+ typeof inspection.code !== "string" ||
98
+ !/^[A-Z][A-Z0-9_]{0,127}$/u.test(inspection.code) ||
99
+ typeof inspection.nextAction !== "string" ||
100
+ !(
101
+ MIGRATION_NEXT_ACTION_KINDS.includes(inspection.nextAction) ||
102
+ inspection.nextAction === "RETRY_OR_PUSH" ||
103
+ inspection.nextAction === "PLATFORM_RECONCILIATION"
104
+ ) ||
105
+ (inspection.correlationId !== undefined &&
106
+ (typeof inspection.correlationId !== "string" ||
107
+ !/^[A-Za-z0-9._:-]{1,200}$/u.test(inspection.correlationId)))
108
+ ) {
109
+ return null;
110
+ }
111
+ const plan = inspection.plan;
112
+ if (
113
+ inspection.outcome === "READY" &&
114
+ !(
115
+ typeof plan?.planId === "string" &&
116
+ /^mig_[a-f0-9]{24}$/u.test(plan.planId) &&
117
+ Number.isSafeInteger(plan.appliedVerifiedCount) &&
118
+ plan.appliedVerifiedCount >= 0 &&
119
+ Number.isSafeInteger(plan.pendingCount) &&
120
+ plan.pendingCount >= 0
121
+ )
122
+ ) {
123
+ return null;
124
+ }
125
+ return {
126
+ readOnly: true,
127
+ outcome: inspection.outcome,
128
+ code: inspection.code,
129
+ nextAction: inspection.nextAction,
130
+ ...(inspection.correlationId ? { correlationId: inspection.correlationId } : {}),
131
+ ...(inspection.outcome === "READY"
132
+ ? {
133
+ plan: {
134
+ planId: plan.planId,
135
+ appliedVerifiedCount: plan.appliedVerifiedCount,
136
+ pendingCount: plan.pendingCount,
137
+ ...(plan.databaseBootstrapRequired === true ? { databaseBootstrapRequired: true } : {}),
138
+ ...(plan.migrationStorePreparationRequired === true
139
+ ? { migrationStorePreparationRequired: true }
140
+ : {}),
141
+ },
142
+ }
143
+ : {}),
144
+ };
145
+ }
146
+
78
147
  function createDbMigrationCommand(deps, command) {
79
148
  return async (...args) => {
80
149
  const isCheck = command === "db:check";
150
+ const isInspection = isCheck && args.includes("--inspect-state");
81
151
  const existingBundleMode = isCheck || args.includes("--existing-bundle");
82
152
  const sourceMode = existingBundleMode ? "EXISTING_BUNDLE" : "GENERATED";
83
153
  const jsonMode = args.includes("--json");
@@ -98,7 +168,7 @@ function createDbMigrationCommand(deps, command) {
98
168
  const execute = async () => {
99
169
  let config;
100
170
  try {
101
- config = await deps.loadConfig();
171
+ config = await deps.loadConfig(jsonMode ? { warnImpl: () => {} } : undefined);
102
172
  } catch (error) {
103
173
  if (!jsonMode) throw error;
104
174
  emitFailure(
@@ -161,6 +231,10 @@ function createDbMigrationCommand(deps, command) {
161
231
  return;
162
232
  }
163
233
 
234
+ if (!isCheck && !statusReceiptId && !jsonMode) {
235
+ emitMigrationResponsibilityNotice(deps.warnImpl);
236
+ }
237
+
164
238
  if (
165
239
  !isCheck &&
166
240
  !statusReceiptId &&
@@ -320,22 +394,7 @@ function createDbMigrationCommand(deps, command) {
320
394
  }
321
395
 
322
396
  const retryCommand = buildDbMigrationRetryCommand({ command, appId, args, isProd, jsonMode });
323
- remoteRequestStarted = true;
324
- const capabilityReady = await checkMigrationCapability({
325
- deps,
326
- creds,
327
- appId,
328
- isCheck,
329
- functionsDir,
330
- retryCommand,
331
- correlationId: clientCorrelationId,
332
- emitFailure,
333
- });
334
- remoteRequestStarted = false;
335
- if (!capabilityReady) {
336
- return;
337
- }
338
-
397
+ const inspectionCommand = buildDbMigrationInspectionCommand({ appId, args, isProd, jsonMode });
339
398
  let stagedBundle;
340
399
  try {
341
400
  stagedBundle = await stageMigrationBundleArchive({
@@ -387,7 +446,7 @@ function createDbMigrationCommand(deps, command) {
387
446
  remoteRequestStarted = true;
388
447
  res = await deps.platformFetchImpl(
389
448
  creds,
390
- `/platform/apps/${appId}/${isCheck ? "schema-check" : "schema-push"}`,
449
+ `/platform/apps/${appId}/${isInspection ? "schema-inspection" : isCheck ? "schema-check" : "schema-push"}`,
391
450
  {
392
451
  method: "POST",
393
452
  headers: {
@@ -431,11 +490,15 @@ function createDbMigrationCommand(deps, command) {
431
490
  const data = recoveredSuccess ? null : await res.json().catch(() => ({}));
432
491
  if (!recoveredSuccess && !res.ok) {
433
492
  const diagnostic = isActionableMigrationDiagnostic(data)
434
- ? enrichMigrationDiagnostic(extractActionableMigrationDiagnostic(data), {
435
- projectRoot: deps.cwdImpl(),
436
- functionsDir,
437
- retryCommand,
438
- })
493
+ ? enrichMigrationDiagnostic(
494
+ normalizeRetiredMigrationGateDiagnostic(extractActionableMigrationDiagnostic(data)),
495
+ {
496
+ projectRoot: deps.cwdImpl(),
497
+ functionsDir,
498
+ retryCommand,
499
+ inspectionCommand,
500
+ },
501
+ )
439
502
  : isCheck
440
503
  ? buildMigrationCheckUnsupportedResponseDiagnostic(
441
504
  typeof data.correlationId === "string" && data.correlationId.length <= 200
@@ -452,17 +515,29 @@ function createDbMigrationCommand(deps, command) {
452
515
  return;
453
516
  }
454
517
 
455
- const validCheckSuccess =
456
- isCheck &&
518
+ const inspectionResult = isInspection ? extractReadOnlyMigrationInspection(data) : null;
519
+ const validInspectionSuccess =
520
+ isInspection &&
457
521
  data?.dryRun === true &&
458
522
  data?.databaseState === "UNCHANGED" &&
459
- typeof data?.plan?.planId === "string" &&
460
- /^mig_[a-f0-9]{24}$/u.test(data.plan.planId) &&
461
- Number.isSafeInteger(data?.plan?.appliedVerifiedCount) &&
462
- data.plan.appliedVerifiedCount >= 0 &&
463
- Number.isSafeInteger(data?.plan?.pendingCount) &&
464
- data.plan.pendingCount >= 0 &&
465
- isCanonicalMigrationCapabilityManifestV1(data?.capability, deps.cliVersion);
523
+ inspectionResult !== null &&
524
+ isCompatibleMigrationCapabilityManifestV1(data?.capability, deps.cliVersion);
525
+ const validCheckSuccess =
526
+ isCheck &&
527
+ (validInspectionSuccess ||
528
+ (data?.dryRun === true &&
529
+ data?.databaseState === "UNCHANGED" &&
530
+ typeof data?.plan?.planId === "string" &&
531
+ /^mig_[a-f0-9]{24}$/u.test(data.plan.planId) &&
532
+ Number.isSafeInteger(data?.plan?.appliedVerifiedCount) &&
533
+ data.plan.appliedVerifiedCount >= 0 &&
534
+ Number.isSafeInteger(data?.plan?.pendingCount) &&
535
+ data.plan.pendingCount >= 0 &&
536
+ (data.plan.databaseBootstrapRequired === undefined ||
537
+ data.plan.databaseBootstrapRequired === true) &&
538
+ (data.plan.migrationStorePreparationRequired === undefined ||
539
+ data.plan.migrationStorePreparationRequired === true) &&
540
+ isCompatibleMigrationCapabilityManifestV1(data?.capability, deps.cliVersion)));
466
541
  if (
467
542
  !recoveredSuccess &&
468
543
  (data?.success !== true || data?.app !== appId || (isCheck && !validCheckSuccess))
@@ -504,7 +579,8 @@ function createDbMigrationCommand(deps, command) {
504
579
  environment: envLabel,
505
580
  existingBundleMode,
506
581
  isCheck,
507
- checkData: data,
582
+ checkData: isInspection ? { ...data, inspection: inspectionResult } : data,
583
+ isInspection,
508
584
  pushSuccess,
509
585
  jsonMode,
510
586
  emitJson,
@@ -0,0 +1,7 @@
1
+ const SAFE_DEPLOY_CORRELATION_ID = /^[A-Za-z0-9_-]{8,128}$/u;
2
+
3
+ export function formatDeployCorrelationId(value) {
4
+ return typeof value === "string" && SAFE_DEPLOY_CORRELATION_ID.test(value)
5
+ ? `Correlation ID: ${value}`
6
+ : null;
7
+ }
@@ -0,0 +1,42 @@
1
+ export const DEPLOY_LOCKFILE_CANDIDATES = Object.freeze([
2
+ { file: "bun.lock", packageManager: "bun" },
3
+ { file: "bun.lockb", packageManager: "bun" },
4
+ { file: "pnpm-lock.yaml", packageManager: "pnpm" },
5
+ { file: "package-lock.json", packageManager: "npm" },
6
+ ]);
7
+
8
+ function declaredPackageManager(value) {
9
+ if (typeof value !== "string") return null;
10
+ return /^(bun|pnpm|npm)(?:@|$)/u.exec(value.trim())?.[1] ?? null;
11
+ }
12
+
13
+ export function selectDeployLockfile({ availableFiles, packageManager }) {
14
+ const available = new Set(availableFiles);
15
+ const detected = DEPLOY_LOCKFILE_CANDIDATES.filter(({ file }) => available.has(file));
16
+ const declared = declaredPackageManager(packageManager);
17
+ const declaredMatch = declared ? detected.find((candidate) => candidate.packageManager === declared) : null;
18
+ const selected = declaredMatch ?? detected[0] ?? null;
19
+
20
+ return {
21
+ selectedFile: selected?.file ?? null,
22
+ selectedPackageManager: selected?.packageManager ?? "none",
23
+ ignoredFiles: detected.filter(({ file }) => file !== selected?.file).map(({ file }) => file),
24
+ selectionSource: declaredMatch
25
+ ? "package.json#packageManager"
26
+ : selected
27
+ ? "deterministic priority"
28
+ : "none",
29
+ };
30
+ }
31
+
32
+ export function detectDeployLockfile({ cwd, packageManager, existsSyncImpl, resolvePathImpl }) {
33
+ const availableFiles = DEPLOY_LOCKFILE_CANDIDATES.filter(({ file }) =>
34
+ existsSyncImpl(resolvePathImpl(cwd, file)),
35
+ ).map(({ file }) => file);
36
+ return selectDeployLockfile({ availableFiles, packageManager });
37
+ }
38
+
39
+ export function formatDeployLockfileSelectionWarning(selection) {
40
+ if (!selection.selectedFile || selection.ignoredFiles.length === 0) return null;
41
+ return `Multiple lockfiles found. Using ${selection.selectedFile} ${selection.selectionSource === "package.json#packageManager" ? "from" : "by"} ${selection.selectionSource}; ignoring ${selection.ignoredFiles.join(", ")}.`;
42
+ }
@@ -6,15 +6,13 @@ import { list } from "tar";
6
6
  import { MIGRATION_BUNDLE_CAPABILITY_V1 } from "@gencow/migration-contract";
7
7
 
8
8
  import { canonicalizeDeployClaimV1, createDeployRequestId, encodeDeployClaimV1 } from "./deploy-contract.mjs";
9
+ import { DEPLOY_LOCKFILE_CANDIDATES } from "./deploy-lockfile-selection.mjs";
9
10
 
10
11
  const PACKAGE_MANIFEST = "package.json";
11
12
  const MIGRATION_MANIFEST = ".gencow/migration-bundle-manifest.json";
12
- const LOCKFILES = new Map([
13
- ["pnpm-lock.yaml", "pnpm"],
14
- ["package-lock.json", "npm"],
15
- ["bun.lock", "bun"],
16
- ["bun.lockb", "bun"],
17
- ]);
13
+ const LOCKFILES = new Map(
14
+ DEPLOY_LOCKFILE_CANDIDATES.map(({ file, packageManager }) => [file, packageManager]),
15
+ );
18
16
  const PROJECT_AUTH = new Set(["gencow/schema-auth.ts", "gencow/schema-auth.js"]);
19
17
  const LEGACY_AUTH = new Set([
20
18
  "gencow/auth-schema.ts",
@@ -27,8 +27,13 @@ import { assertToolingRuntimeCapabilities, loadToolingRuntime } from "./tooling-
27
27
  import {
28
28
  extractActionableMigrationDiagnostic,
29
29
  isActionableMigrationDiagnostic,
30
+ normalizeRetiredMigrationGateDiagnostic,
30
31
  renderMigrationDiagnosticLines,
31
32
  } from "./migration-diagnostic.mjs";
33
+ import { emitMigrationAdvisories, emitMigrationResponsibilityNotice } from "./migration-advisory.mjs";
34
+ import { formatDeployCorrelationId } from "./deploy-correlation-diagnostic.mjs";
35
+ import { detectDeployLockfile, formatDeployLockfileSelectionWarning } from "./deploy-lockfile-selection.mjs";
36
+ import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
32
37
 
33
38
  async function loadInternalToolingDefault() {
34
39
  return loadToolingRuntime({
@@ -340,10 +345,11 @@ export function createDeployPackageRuntime({
340
345
  let dependencyAuditManifestPath = null;
341
346
  let dependencyAuditManifest = null;
342
347
  let dependencyVersionMessage = "";
348
+ let projectPkg;
343
349
  try {
344
350
  const { DEPLOY_DEPENDENCY_AUDIT_MANIFEST, auditDependencyVersions, formatDependencyVersionAuditError } =
345
351
  await (loadDeployAuditorImpl ?? loadDeployAuditorDefault)();
346
- const projectPkg = JSON.parse(readFileSyncImpl(resolvePathImpl(cwd, "package.json"), "utf8"));
352
+ projectPkg = JSON.parse(readFileSyncImpl(resolvePathImpl(cwd, "package.json"), "utf8"));
347
353
  const dependencyVersionResult = auditDependencyVersions(projectPkg);
348
354
  dependencyVersionMessage = formatDependencyVersionAuditError(dependencyVersionResult);
349
355
  dependencyAuditManifestPath = resolvePathImpl(cwd, DEPLOY_DEPENDENCY_AUDIT_MANIFEST);
@@ -360,6 +366,17 @@ export function createDeployPackageRuntime({
360
366
  return null;
361
367
  }
362
368
 
369
+ const lockfileSelection = detectDeployLockfile({
370
+ cwd,
371
+ packageManager: projectPkg.packageManager,
372
+ existsSyncImpl,
373
+ resolvePathImpl,
374
+ });
375
+ const lockfileWarning = formatDeployLockfileSelectionWarning(lockfileSelection);
376
+ if (lockfileWarning) warnImpl(lockfileWarning);
377
+ const projectConfigSelection = detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl });
378
+ warnProjectConfigSelectionOnce({ cwd, selection: projectConfigSelection, warnImpl });
379
+
363
380
  try {
364
381
  mkdirSyncImpl(dirname(dependencyAuditManifestPath), { recursive: true });
365
382
  writeFileSyncImpl(dependencyAuditManifestPath, JSON.stringify(dependencyAuditManifest, null, 2));
@@ -438,17 +455,14 @@ export function createDeployPackageRuntime({
438
455
  exitImpl(1);
439
456
  return null;
440
457
  }
441
- if (existsSyncImpl(resolvePathImpl(cwd, "gencow.config.ts"))) filesToPack.push("gencow.config.ts");
442
- else if (existsSyncImpl(resolvePathImpl(cwd, "gencow.config.js"))) filesToPack.push("gencow.config.js");
458
+ if (projectConfigSelection.selectedFile) filesToPack.push(projectConfigSelection.selectedFile);
443
459
  if (dependencyAuditManifestPath) filesToPack.push(".gencow/deploy-dependency-audit.json");
444
460
  if (cronManifestPath) filesToPack.push(CRON_MANIFEST_RELATIVE_PATH);
445
- if (existsSyncImpl(resolvePathImpl(cwd, "bun.lockb"))) filesToPack.push("bun.lockb");
446
- if (existsSyncImpl(resolvePathImpl(cwd, "package-lock.json"))) filesToPack.push("package-lock.json");
461
+ if (lockfileSelection.selectedFile) filesToPack.push(lockfileSelection.selectedFile);
447
462
  if (existsSyncImpl(resolvePathImpl(cwd, "tsconfig.json"))) filesToPack.push("tsconfig.json");
448
463
 
449
464
  if (auditResult?.runtimeDeps?.length >= 0) {
450
465
  try {
451
- const projectPkg = JSON.parse(readFileSyncImpl(resolvePathImpl(cwd, "package.json"), "utf8"));
452
466
  const allDeps = { ...projectPkg.dependencies, ...projectPkg.devDependencies };
453
467
  const runtimeDeps = {};
454
468
  for (const dependency of auditResult.runtimeDeps) {
@@ -618,6 +632,9 @@ export function createDeployPackageRuntime({
618
632
  explicitRequestId = null,
619
633
  }) {
620
634
  const cwd = cwdImpl();
635
+ if (deployClaim?.claim?.migrationBundle?.manifestSha256) {
636
+ emitMigrationResponsibilityNotice(warnImpl);
637
+ }
621
638
  let receipt = deployClaim
622
639
  ? acquireDeployReceipt({
623
640
  cwd,
@@ -717,7 +734,7 @@ export function createDeployPackageRuntime({
717
734
  errData?.retryClass === "retryable-infra";
718
735
  if (receipt && !retryableResponse) releaseDeployReceipt(receipt);
719
736
  const migrationDiagnostic = isActionableMigrationDiagnostic(errData?.diagnostic)
720
- ? extractActionableMigrationDiagnostic(errData.diagnostic)
737
+ ? normalizeRetiredMigrationGateDiagnostic(extractActionableMigrationDiagnostic(errData.diagnostic))
721
738
  : null;
722
739
  if (migrationDiagnostic) {
723
740
  const lines = renderMigrationDiagnosticLines(migrationDiagnostic);
@@ -725,6 +742,8 @@ export function createDeployPackageRuntime({
725
742
  lines.slice(1).forEach((line) => logImpl(line));
726
743
  } else {
727
744
  errorImpl(`Deploy failed: ${errData.error || deployRes.statusText}`);
745
+ const correlationLine = formatDeployCorrelationId(errData?.correlationId);
746
+ if (correlationLine) errorImpl(correlationLine);
728
747
  }
729
748
  await showCrashLogs(errData, "Server startup failure cause:", targetAppId);
730
749
  exitImpl(1);
@@ -732,6 +751,7 @@ export function createDeployPackageRuntime({
732
751
  }
733
752
 
734
753
  const deployData = await deployRes.json();
754
+ emitMigrationAdvisories(deployData?.warnings, warnImpl);
735
755
  if (receipt) releaseDeployReceipt(receipt);
736
756
  const deployElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
737
757
  logImpl("");
@@ -14,16 +14,10 @@ import {
14
14
  } from "./deploy-auditor.mjs";
15
15
  import { shouldIncludeTarEntry } from "./tar-archive.mjs";
16
16
  import { verifyMigrationBundleArchiveRoundTrip } from "./migration-bundle-roundtrip.mjs";
17
+ import { detectDeployLockfile, formatDeployLockfileSelectionWarning } from "./deploy-lockfile-selection.mjs";
18
+ import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
17
19
 
18
- const OPTIONAL_BUNDLE_FILES = [
19
- "gencow.config.js",
20
- "gencow.config.ts",
21
- "bun.lock",
22
- "bun.lockb",
23
- "package-lock.json",
24
- "pnpm-lock.yaml",
25
- "tsconfig.json",
26
- ];
20
+ const OPTIONAL_BUNDLE_FILES = ["tsconfig.json"];
27
21
 
28
22
  function readJson(path, readFileSyncImpl) {
29
23
  return JSON.parse(readFileSyncImpl(path, "utf8"));
@@ -126,6 +120,16 @@ export async function createDevCloudDeployBundle({
126
120
  if (!existsSyncImpl(functionsSource)) throw new Error(`Backend root dir not found: ${functionsSource}`);
127
121
 
128
122
  const projectPkg = readJson(resolvePathImpl(cwd, "package.json"), readFileSyncImpl);
123
+ const lockfileSelection = detectDeployLockfile({
124
+ cwd,
125
+ packageManager: projectPkg.packageManager,
126
+ existsSyncImpl,
127
+ resolvePathImpl,
128
+ });
129
+ const lockfileWarning = formatDeployLockfileSelectionWarning(lockfileSelection);
130
+ if (lockfileWarning) warnImpl(lockfileWarning);
131
+ const projectConfigSelection = detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl });
132
+ warnProjectConfigSelectionOnce({ cwd, selection: projectConfigSelection, warnImpl });
129
133
  const dependencyVersionResult = auditDependencyVersionsImpl(projectPkg);
130
134
  const dependencyVersionMessage = formatDependencyVersionAuditError(dependencyVersionResult);
131
135
  if (dependencyVersionMessage) throw new Error(dependencyVersionMessage);
@@ -173,6 +177,20 @@ export async function createDevCloudDeployBundle({
173
177
  existsSyncImpl,
174
178
  resolvePathImpl,
175
179
  });
180
+ if (projectConfigSelection.selectedFile) {
181
+ copyFileSyncImpl(
182
+ resolvePathImpl(cwd, projectConfigSelection.selectedFile),
183
+ resolvePathImpl(stagingDir, projectConfigSelection.selectedFile),
184
+ );
185
+ filesToPack.push(projectConfigSelection.selectedFile);
186
+ }
187
+ if (lockfileSelection.selectedFile) {
188
+ copyFileSyncImpl(
189
+ resolvePathImpl(cwd, lockfileSelection.selectedFile),
190
+ resolvePathImpl(stagingDir, lockfileSelection.selectedFile),
191
+ );
192
+ filesToPack.push(lockfileSelection.selectedFile);
193
+ }
176
194
 
177
195
  const migrationManifestStage = stageProjectMigrationManifest({
178
196
  cwd: stagingDir,