gencow 0.1.267 → 0.1.269

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.
@@ -1,5 +1,10 @@
1
1
  import { formatProcedureRpcDiagnostic } from "./procedure-rpc-diagnostic.mjs";
2
2
  import { describeMigrationFolderFormatFailure } from "./migration-folder-format.mjs";
3
+ import {
4
+ buildHintsRequiredCommand,
5
+ HINTS_DATA_LOSS_NOTICE,
6
+ SAFE_COLUMN_REFERENCE,
7
+ } from "./migration-hints.mjs";
3
8
 
4
9
  const SAFE_CORRELATION_ID = /^[A-Za-z0-9_-]{8,128}$/u;
5
10
  const SAFE_APP_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
@@ -166,8 +171,61 @@ const SAFE_LOCAL_ERROR_CODES = new Map([
166
171
  },
167
172
  },
168
173
  ],
174
+ [
175
+ "MIGRATION_HINTS_REQUIRED",
176
+ {
177
+ message: (caught) => {
178
+ const count = safeHintCount(caught);
179
+ const columns = safeHintColumns(caught);
180
+ if (columns.length === 0) {
181
+ return `Migration generation stopped: ${count} schema change(s) need your decision.`;
182
+ }
183
+ return `Migration generation stopped: ${count} column change(s) need your decision (${columns.join(", ")}).`;
184
+ },
185
+ stage: "migration",
186
+ action: (caught) => {
187
+ const example = `\`${buildHintsRequiredCommand(safeHintColumns(caught))}\``;
188
+ const createExample = `{"type":"create",...} to add a new column instead`;
189
+ const tail = `${HINTS_DATA_LOSS_NOTICE} Do not edit applied migration files.`;
190
+ if (caught.migrationHints?.hintsProvided) {
191
+ return `Retryable: yes, after your --hints array decides every listed column change — rerun with one rename or create decision per column, for example ${example}, or use ${createExample}. ${tail}`;
192
+ }
193
+ return `Retryable: yes, after you declare the decision — rerun ${example} (or use ${createExample}). ${tail}`;
194
+ },
195
+ },
196
+ ],
197
+ [
198
+ "MIGRATION_HINTS_INVALID",
199
+ {
200
+ message: (caught) => {
201
+ const source = caught.hintsSource === "file" ? "--hints-file" : "--hints";
202
+ const reasons = {
203
+ conflicting_options: "use only one of --hints or --hints-file.",
204
+ value_missing: `${source} needs a value.`,
205
+ json_invalid: `${source} value is not valid JSON.`,
206
+ not_array: `${source} must be a JSON array of decisions.`,
207
+ too_large: `${source} value is too large.`,
208
+ file_unreadable: "the --hints-file could not be read.",
209
+ };
210
+ return `Migration generation not started: ${reasons[caught.hintsReason] ?? "the hints value is not accepted."}`;
211
+ },
212
+ stage: "input",
213
+ action:
214
+ "Retryable: yes, after the hints value is fixed — pass a JSON array with `--hints '<json-array>'` or a JSON file with `--hints-file <path>`, then rerun `npx gencow@latest db:generate`.",
215
+ },
216
+ ],
169
217
  ]);
170
218
 
219
+ function safeHintCount(caught) {
220
+ const count = caught.migrationHints?.count;
221
+ return Number.isInteger(count) && count > 0 && count <= 9999 ? count : 1;
222
+ }
223
+
224
+ function safeHintColumns(caught) {
225
+ const columns = caught.migrationHints?.columns;
226
+ return Array.isArray(columns) ? columns.filter((value) => typeof value === "string" && SAFE_COLUMN_REFERENCE.test(value)) : [];
227
+ }
228
+
171
229
  function safeDisplayDir(caught) {
172
230
  const { displayDir } = caught;
173
231
  return typeof displayDir === "string" && SAFE_DISPLAY_DIR.test(displayDir) ? displayDir : null;
@@ -14,8 +14,10 @@ import {
14
14
  import { dirname, resolve } from "path";
15
15
  import { tmpdir } from "os";
16
16
  import { createDbCheckCommand, createDbPushCommand } from "./db-push-command.mjs";
17
+ import { renderMaintenanceFailure } from "./db-seed-failure-render.mjs";
17
18
  import { logEnvFileOperationWithType } from "./backend-env-resolver.mjs";
18
19
  import { runDrizzleGenerateWithFallback } from "./drizzle-generate.mjs";
20
+ import { HINTS_DATA_LOSS_NOTICE, parseMigrationHintsArgs } from "./migration-hints.mjs";
19
21
  import { BOLD, CYAN, DIM, GREEN, RED, RESET, error, info, log, success, warn } from "./output.mjs";
20
22
  import {
21
23
  describeProdTargetFailure,
@@ -150,8 +152,17 @@ function renderDbCheckHelp(logImpl = log) {
150
152
 
151
153
  function renderDbGenerateHelp(logImpl = log) {
152
154
  logImpl(`\n${BOLD}${CYAN}gencow db:generate${RESET} — Generate SQL migration files\n`);
153
- logImpl(` ${BOLD}Usage:${RESET} gencow db:generate\n`);
155
+ logImpl(` ${BOLD}Usage:${RESET} gencow db:generate [--hints '<json-array>' | --hints-file <path>]\n`);
154
156
  logImpl(` Generates migration files from schema.ts using drizzle-kit.\n`);
157
+ logImpl(` ${BOLD}Options:${RESET}`);
158
+ logImpl(` --hints '<json-array>' Decide each changed column: rename or create.`);
159
+ logImpl(` --hints-file <path> Same decisions, read from a JSON array file.\n`);
160
+ logImpl(` Example (rename column label to tag in table files):`);
161
+ logImpl(
162
+ ` gencow db:generate --hints '[{"type":"rename","kind":"column","from":["public","files","label"],"to":["public","files","tag"]}]'\n`,
163
+ );
164
+ logImpl(` Use only one of --hints or --hints-file. Hints are never inferred automatically.`);
165
+ logImpl(` ${HINTS_DATA_LOSS_NOTICE}\n`);
155
166
  }
156
167
 
157
168
  function renderLocalSeedMissingFile({ infoImpl = info, logImpl = log, warnImpl = warn } = {}) {
@@ -200,6 +211,7 @@ export function createDbCommands({
200
211
  resolvePathImpl = resolve,
201
212
  rmSyncImpl = rmSync,
202
213
  runInServer,
214
+ spawnSyncImpl,
203
215
  statSyncImpl = statSync,
204
216
  successImpl = success,
205
217
  tarCreateImpl,
@@ -296,12 +308,23 @@ export function createDbCommands({
296
308
  headers: { "Content-Type": "application/json" },
297
309
  body: JSON.stringify({ confirm: "delete all data" }),
298
310
  });
299
- const data = await res.json();
311
+ const data = await res.json().catch(() => null);
312
+ if (data === null) {
313
+ errorImpl("Server connection failed: the server response could not be read.");
314
+ logImpl("");
315
+ return 1;
316
+ }
300
317
 
301
318
  if (!res.ok) {
302
- errorImpl(`Reset failed: ${data.error || "Unknown error"}`);
319
+ renderMaintenanceFailure(data, res.status, {
320
+ errorImpl,
321
+ infoImpl,
322
+ label: "Reset failed",
323
+ retryCommand: "gencow db:reset",
324
+ impactNote: "verify which tables still contain data before rerunning",
325
+ });
303
326
  logImpl("");
304
- return;
327
+ return 1;
305
328
  }
306
329
 
307
330
  successImpl(`TRUNCATE done: ${data.truncated?.length || 0} tables`);
@@ -315,6 +338,8 @@ export function createDbCommands({
315
338
  }
316
339
  } catch (e) {
317
340
  errorImpl(`Server connection failed: ${e.message}`);
341
+ logImpl("");
342
+ return 1;
318
343
  }
319
344
  } else {
320
345
  const dbPath = resolvePathImpl(cwd, config.db.url);
@@ -442,13 +467,18 @@ export function createDbCommands({
442
467
  headers: { "Content-Type": "application/json" },
443
468
  signal: AbortSignal.timeout(30000),
444
469
  });
445
- const data = await res.json();
470
+ const data = await res.json().catch(() => null);
471
+ if (data === null) {
472
+ errorImpl("Cloud app connection failed: the platform response could not be read.");
473
+ infoImpl(`${DIM}Check if the platform is reachable: ${targetCreds.platformUrl}${RESET}`);
474
+ return 1;
475
+ }
446
476
 
447
477
  if (res.status === 404 && String(data.error || "").includes("seed.ts")) {
448
478
  renderCloudSeedMissingFile({ infoImpl, logImpl, warnImpl });
449
479
  return 1;
450
480
  } else if (!res.ok) {
451
- errorImpl(`Cloud seed failed: ${data.error || "Unknown error"}`);
481
+ renderMaintenanceFailure(data, res.status, { errorImpl, infoImpl });
452
482
  return 1;
453
483
  } else {
454
484
  successImpl("Cloud seed completed ✓");
@@ -531,6 +561,12 @@ export function createDbCommands({
531
561
  return;
532
562
  }
533
563
 
564
+ // Hints are validated before the config is read or the generator starts.
565
+ const hints = parseMigrationHintsArgs(args, {
566
+ cwd: cwdImpl(),
567
+ readFileSyncImpl,
568
+ resolvePathImpl,
569
+ });
534
570
  const config = await loadConfig();
535
571
  logImpl(`\n${BOLD}${CYAN}Gencow DB Generate${RESET}\n`);
536
572
  infoImpl("Generating migration files from schema.ts...");
@@ -540,6 +576,8 @@ export function createDbCommands({
540
576
  drizzleKitCmdImpl,
541
577
  env: buildEnv(config),
542
578
  execSyncImpl,
579
+ generatorArgs: hints.args,
580
+ spawnSyncImpl,
543
581
  stdio: "inherit",
544
582
  warnImpl,
545
583
  });
@@ -28,7 +28,9 @@ import {
28
28
  buildClientPreflightFailureDiagnostic,
29
29
  buildClientUserFixableDiagnostic,
30
30
  buildMigrationFolderFormatDiagnostic,
31
+ buildMigrationHintsRequiredDiagnostic,
31
32
  } from "./migration-client-diagnostic.mjs";
33
+ import { MIGRATION_HINTS_REQUIRED } from "./migration-hints.mjs";
32
34
  import { detectConflictingAuthSchemaFiles, formatAuthSchemaConflictMessage } from "./codegen-command.mjs";
33
35
  import { BOLD, CYAN, DIM, RESET } from "./output.mjs";
34
36
  import { stageMigrationBundleArchive } from "./migration-bundle-staging.mjs";
@@ -468,7 +470,9 @@ function createDbMigrationCommand(deps, command) {
468
470
  emitFailure(
469
471
  folderFormat
470
472
  ? buildMigrationFolderFormatDiagnostic(folderFormat, clientCorrelationId())
471
- : buildClientUserFixableDiagnostic("MIGRATION_GENERATION_FAILED", clientCorrelationId()),
473
+ : error?.code === MIGRATION_HINTS_REQUIRED
474
+ ? buildMigrationHintsRequiredDiagnostic(error.migrationHints, clientCorrelationId())
475
+ : buildClientUserFixableDiagnostic("MIGRATION_GENERATION_FAILED", clientCorrelationId()),
472
476
  );
473
477
  return;
474
478
  }
@@ -0,0 +1,84 @@
1
+ import { DIM, RESET } from "./output.mjs";
2
+
3
+ const TYPED_CODE_PATTERN = /^(SEED|RESET)_[A-Z_]+$/;
4
+ const UNAUTHORIZED_TEXT = "Seed request was not authorized for this app.";
5
+ const UNEXPECTED_TEXT = "the platform returned an unexpected response.";
6
+ const GENERIC_NEXT_ACTION = "Verify the app state, then rerun";
7
+ const PLAIN_TEXT_LIMIT = 400;
8
+
9
+ // Exact platform messages from packages/platform/gencow/apps-seed-handler.ts and apps-account-hold.ts.
10
+ // Any other untyped text is never printed, even when it looks harmless.
11
+ const KNOWN_PLATFORM_MESSAGES = new Set([
12
+ "App not found or not owned by you",
13
+ "Static-only app has no backend to seed",
14
+ "App is frozen — thaw before seed",
15
+ "App is locked because the owner account is suspended.",
16
+ "App is locked because the owner account is pending deletion.",
17
+ "App is locked because the owner account is deleted.",
18
+ ]);
19
+ const KNOWN_NOT_RUNNING_PATTERN = /^Cloud app "[^"\n]{1,128}" is not running$/;
20
+
21
+ // Typed text is rebuilt from the server catalog by the platform. This screen only blocks SQL-shaped text
22
+ // that a tampered or future body could carry, because the CLI does not import the catalog.
23
+ const SQL_SHAPED_PATTERN =
24
+ /Failed query|params:|\b(select|insert|update|delete|truncate|from|into|values|where)\b|\$\d/i;
25
+
26
+ export function isTypedMaintenanceFailure(data) {
27
+ return (
28
+ data !== null &&
29
+ typeof data === "object" &&
30
+ typeof data.code === "string" &&
31
+ TYPED_CODE_PATTERN.test(data.code)
32
+ );
33
+ }
34
+
35
+ function typedText(value, fallback) {
36
+ if (typeof value !== "string" || value.trim() === "" || value.length > PLAIN_TEXT_LIMIT) return fallback;
37
+ return SQL_SHAPED_PATTERN.test(value) ? fallback : value;
38
+ }
39
+
40
+ function isKnownPlatformMessage(text) {
41
+ return KNOWN_PLATFORM_MESSAGES.has(text) || KNOWN_NOT_RUNNING_PATTERN.test(text);
42
+ }
43
+
44
+ /**
45
+ * Renders a failed db:seed or db:reset response. The label and retry command name the operation.
46
+ * - Typed platform failures show the code, stage, data impact, retry condition, next action and correlation ID.
47
+ * - Untyped responses print only a fixed sentence per status, or an exact known platform message.
48
+ * Everything else, including raw database text, is replaced by the generic sentence.
49
+ */
50
+ export function renderMaintenanceFailure(
51
+ data,
52
+ status,
53
+ { errorImpl, infoImpl, label = "Cloud seed failed", retryCommand = "gencow db:seed --prod", impactNote },
54
+ ) {
55
+ if (!isTypedMaintenanceFailure(data)) {
56
+ const text = typeof data?.error === "string" ? data.error : "";
57
+ if (status === 401 || status === 403) {
58
+ errorImpl(`${label}: ${UNAUTHORIZED_TEXT}`);
59
+ return;
60
+ }
61
+ if (isKnownPlatformMessage(text)) {
62
+ errorImpl(`${label}: ${text}`);
63
+ return;
64
+ }
65
+ errorImpl(`${label}: ${UNEXPECTED_TEXT}`);
66
+ infoImpl(`${DIM}Next action: ${GENERIC_NEXT_ACTION} \`${retryCommand}\`.${RESET}`);
67
+ return;
68
+ }
69
+
70
+ errorImpl(`${label}: ${typedText(data.error, "the platform reported a failure.")}`);
71
+ infoImpl(`${DIM}Code: ${data.code}${RESET}`);
72
+ infoImpl(`${DIM}Stage: ${typedText(data.stage, "unknown")}${RESET}`);
73
+ infoImpl(
74
+ `${DIM}Data impact: unknown — ${impactNote ?? "verify what was inserted before rerunning"}${RESET}`,
75
+ );
76
+ infoImpl(`${DIM}Retryable: ${data.retryable === true ? "yes" : "no"}${RESET}`);
77
+ infoImpl(
78
+ `${DIM}Retry condition: ${typedText(data.retryPrecondition, "Verify the current data before retrying.")}${RESET}`,
79
+ );
80
+ infoImpl(
81
+ `${DIM}Next action: ${typedText(data.nextAction, `${GENERIC_NEXT_ACTION} \`${retryCommand}\`.`)}${RESET}`,
82
+ );
83
+ infoImpl(`${DIM}Correlation: ${typedText(data.correlationId, "unavailable")}${RESET}`);
84
+ }
@@ -7,6 +7,7 @@ import { fetchAppDiagnosticLogsViaRest, normalizeDiagnosticLogLines } from "./ap
7
7
  import { createCloudAppIfNeeded as createCloudAppIfNeededImpl } from "./deploy-app-creation.mjs";
8
8
  import { rejectExplicitAppNotFound } from "./deploy-app-target.mjs";
9
9
  import { loadProjectConfigForDeploy } from "./deploy-project-config-load.mjs";
10
+ import { loadPackageJson } from "./deploy-project-package-json.mjs";
10
11
  import { resolveConfiguredAuditorImplementation } from "./auditor-mode.mjs";
11
12
  import {
12
13
  loadDeployMigrationGenerationContext,
@@ -398,12 +399,13 @@ export function createDeployPackageRuntime({
398
399
  warnRuntimePathsOutsideBackendRoot({ entryPoint, projectRoot: cwd, backendRoot, warnImpl });
399
400
  let dependencyAuditManifest = null;
400
401
  let dependencyVersionMessage = "";
401
- let projectPkg;
402
+ const packageJson = loadPackageJson({ cwd, readFileSyncImpl, resolvePathImpl, errorImpl, exitImpl });
403
+ if (!packageJson) return null;
404
+ const projectPkg = packageJson.pkg;
402
405
  try {
403
406
  const { auditDependencyVersions, formatDependencyVersionAuditError } = await (
404
407
  loadDeployAuditorImpl ?? loadDeployAuditorDefault
405
408
  )();
406
- projectPkg = JSON.parse(readFileSyncImpl(resolvePathImpl(cwd, "package.json"), "utf8"));
407
409
  const auditOptions = { cwd, existsSyncImpl, readFileSyncImpl };
408
410
  const dependencyVersionResult = auditDependencyVersions(projectPkg, auditOptions);
409
411
  dependencyVersionMessage = formatDependencyVersionAuditError(dependencyVersionResult);
@@ -447,9 +449,6 @@ export function createDeployPackageRuntime({
447
449
  }
448
450
  }
449
451
 
450
- // Keep the manifest byte-compatible with the canonical pnpm lock importer.
451
- const stagedPackageJson = readFileSyncImpl(resolvePathImpl(cwd, "package.json"), "utf8");
452
-
453
452
  let bundleBackendRoot;
454
453
  let bundleFileRows = [];
455
454
  try {
@@ -472,7 +471,7 @@ export function createDeployPackageRuntime({
472
471
  readFileSyncImpl,
473
472
  resolvePathImpl,
474
473
  rmSyncImpl,
475
- stagedPackageJson,
474
+ stagedPackageJson: packageJson.text,
476
475
  tarCreateImpl,
477
476
  tmpBundle,
478
477
  writeCronManifestForProjectImpl,
@@ -0,0 +1,79 @@
1
+ // Reads the project package.json for deploy packaging. A missing, unreadable or non-JSON file is reported
2
+ // as a typed, secret-free diagnosis (no raw fs/parser text, no absolute workspace path) and returns null.
3
+
4
+ export const DEPLOY_PACKAGE_JSON_CODES = Object.freeze({
5
+ MISSING: "DEPLOY_PACKAGE_JSON_MISSING",
6
+ UNREADABLE: "DEPLOY_PACKAGE_JSON_UNREADABLE",
7
+ INVALID: "DEPLOY_PACKAGE_JSON_INVALID",
8
+ });
9
+
10
+ const NEXT_ACTION_FOR_MISSING =
11
+ "Next action: for a static-only site, run `gencow static --prod <build-output-dir>` (for example `gencow static --prod dist/`); for a backend app, run `npx gencow@latest init` to create a project with a package.json, then rerun deploy.";
12
+
13
+ const DIAGNOSES = Object.freeze({
14
+ [DEPLOY_PACKAGE_JSON_CODES.MISSING]: {
15
+ message: "DEPLOY BLOCKED — package.json was not found in the project root.",
16
+ retryable:
17
+ "Retryable: yes, after package.json exists in the project root — run `ls package.json` to confirm, then rerun the same deploy command.",
18
+ nextAction: NEXT_ACTION_FOR_MISSING,
19
+ },
20
+ [DEPLOY_PACKAGE_JSON_CODES.UNREADABLE]: {
21
+ message: "DEPLOY BLOCKED — package.json exists but could not be read.",
22
+ retryable:
23
+ "Retryable: yes, after package.json is readable by the current user — check its file permissions, then rerun the same deploy command.",
24
+ nextAction:
25
+ "Next action: make package.json a readable regular file in the project root, then rerun deploy.",
26
+ },
27
+ [DEPLOY_PACKAGE_JSON_CODES.INVALID]: {
28
+ message: "DEPLOY BLOCKED — package.json is not a valid JSON object.",
29
+ retryable:
30
+ "Retryable: yes, after package.json is fixed — run `node -p \"require('./package.json').name\"` to see the syntax error, correct the file, then rerun the same deploy command.",
31
+ nextAction:
32
+ "Next action: correct the JSON syntax of package.json in the project root, then rerun deploy.",
33
+ },
34
+ });
35
+
36
+ export function formatDeployPackageJsonDiagnostic(code) {
37
+ const diagnosis = DIAGNOSES[code];
38
+ return [
39
+ diagnosis.message,
40
+ `Code: ${code}`,
41
+ "Stage: packaging",
42
+ "Database mutation: NONE",
43
+ "Incumbent serving: PRESERVED",
44
+ diagnosis.retryable,
45
+ diagnosis.nextAction,
46
+ ].join("\n");
47
+ }
48
+
49
+ function isPlainObject(value) {
50
+ return value !== null && typeof value === "object" && !Array.isArray(value);
51
+ }
52
+
53
+ // Returns { pkg, text } (text is the exact file content, so the staged manifest stays byte-identical to what
54
+ // was validated), or null after printing the typed diagnosis and exiting with code 1.
55
+ export function loadPackageJson({ cwd, readFileSyncImpl, resolvePathImpl, errorImpl, exitImpl }) {
56
+ let text;
57
+ let code = null;
58
+ try {
59
+ text = readFileSyncImpl(resolvePathImpl(cwd, "package.json"), "utf8");
60
+ } catch (caught) {
61
+ code =
62
+ caught?.code === "ENOENT" ? DEPLOY_PACKAGE_JSON_CODES.MISSING : DEPLOY_PACKAGE_JSON_CODES.UNREADABLE;
63
+ }
64
+ let pkg;
65
+ if (code === null) {
66
+ try {
67
+ pkg = JSON.parse(text);
68
+ } catch {
69
+ pkg = undefined;
70
+ }
71
+ if (!isPlainObject(pkg)) code = DEPLOY_PACKAGE_JSON_CODES.INVALID;
72
+ }
73
+ if (code !== null) {
74
+ errorImpl(formatDeployPackageJsonDiagnostic(code));
75
+ exitImpl(1);
76
+ return null;
77
+ }
78
+ return { pkg, text };
79
+ }
@@ -4,6 +4,11 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
5
 
6
6
  import { classifyMigrationFolderFormatFailure } from "./migration-folder-format.mjs";
7
+ import {
8
+ classifyMissingHintsOutput,
9
+ createMigrationHintsRequiredError,
10
+ MIGRATION_HINTS_REQUIRED,
11
+ } from "./migration-hints.mjs";
7
12
 
8
13
  export const SUPPORTED_DRIZZLE_KIT_GENERATOR_VERSION = SUPPORTED_MIGRATION_GENERATOR_VERSION;
9
14
  export const UNKNOWN_DRIZZLE_KIT_GENERATOR_VERSION = "unknown";
@@ -119,7 +124,22 @@ export function getMigrationFolderFormatFailure(error) {
119
124
  );
120
125
  }
121
126
 
127
+ // Every raw stream the generator wrote; drizzle-kit reports hint requests on stdout.
128
+ function rawGeneratorOutput(error) {
129
+ return (
130
+ error?.gencowRawGeneratorOutput ??
131
+ [error?.stdout, error?.stderr, error?.message].map((value) => String(value ?? "")).join("\n")
132
+ );
133
+ }
134
+
135
+ function getMissingHintsFailure(error) {
136
+ return classifyMissingHintsOutput(rawGeneratorOutput(error));
137
+ }
138
+
122
139
  export function buildMigrationGenerationError(error) {
140
+ if (error?.code === MIGRATION_HINTS_REQUIRED && error.migrationHints) {
141
+ return createMigrationHintsRequiredError(error.migrationHints);
142
+ }
123
143
  const firstLine = getCommandErrorMessage(error).split("\n")[0] || "unknown generator error";
124
144
  const failure = new Error(`Migration generation failed: ${firstLine}`);
125
145
  const folderFormat = getMigrationFolderFormatFailure(error);
@@ -135,11 +155,17 @@ export function shouldRetryLegacyGenerate(error) {
135
155
  return message.includes("unknown command 'generate'") && message.includes("generate:pg");
136
156
  }
137
157
 
158
+ // Quotes one argv element for the legacy string-command path (never used for spawn).
159
+ function shellQuote(value) {
160
+ return `'${String(value).replace(/'/gu, `'\\''`)}'`;
161
+ }
162
+
138
163
  export function runDrizzleGenerateWithFallback({
139
164
  cwd,
140
165
  drizzleKitCmdImpl,
141
166
  env = {},
142
167
  execSyncImpl,
168
+ generatorArgs = [],
143
169
  spawnSyncImpl = spawnSync,
144
170
  stderrWriteImpl = (value) => process.stderr.write(value),
145
171
  stdio = "inherit",
@@ -151,7 +177,7 @@ export function runDrizzleGenerateWithFallback({
151
177
  try {
152
178
  const generatorEnv = selectDrizzleGeneratorEnvironment(env, dk.env);
153
179
  if (typeof dk.command === "string" && Array.isArray(dk.args)) {
154
- const result = spawnSyncImpl(dk.command, dk.args, {
180
+ const result = spawnSyncImpl(dk.command, [...dk.args, ...generatorArgs], {
155
181
  cwd,
156
182
  encoding: "utf8",
157
183
  env: generatorEnv,
@@ -169,6 +195,7 @@ export function runDrizzleGenerateWithFallback({
169
195
  Object.defineProperties(failure, {
170
196
  gencowGeneratorCwd: { value: cwd },
171
197
  gencowRawGeneratorMessage: { value: rawMessage },
198
+ gencowRawGeneratorOutput: { value: `${rawStdout}\n${rawStderr}\n${result.error?.message ?? ""}` },
172
199
  });
173
200
  throw failure;
174
201
  }
@@ -179,7 +206,10 @@ export function runDrizzleGenerateWithFallback({
179
206
  if (stderr) stderrWriteImpl(stderr);
180
207
  }
181
208
  } else {
182
- execSyncImpl(dk.cmd, { cwd, env: generatorEnv, stdio });
209
+ const command = generatorArgs.length
210
+ ? [dk.cmd, ...generatorArgs.map(shellQuote)].join(" ")
211
+ : dk.cmd;
212
+ execSyncImpl(command, { cwd, env: generatorEnv, stdio });
183
213
  }
184
214
  } catch (error) {
185
215
  if (error && typeof error === "object") {
@@ -188,6 +218,16 @@ export function runDrizzleGenerateWithFallback({
188
218
  } catch {
189
219
  /* preserve the original generator error when the thrown value is immutable */
190
220
  }
221
+ const missingHints = getMissingHintsFailure(error);
222
+ if (missingHints) {
223
+ try {
224
+ error.code = MIGRATION_HINTS_REQUIRED;
225
+ error.migrationHints = { ...missingHints, hintsProvided: generatorArgs.length > 0 };
226
+ } catch {
227
+ /* keep the raw generator error; the hints code is then unavailable */
228
+ }
229
+ throw error;
230
+ }
191
231
  const folderFormat = getMigrationFolderFormatFailure(error);
192
232
  if (folderFormat) {
193
233
  try {
@@ -6,6 +6,7 @@ import {
6
6
  } from "@gencow/migration-contract";
7
7
 
8
8
  import { describeMigrationFolderFormatFailure } from "./migration-folder-format.mjs";
9
+ import { buildHintsRequiredCommand, HINTS_DATA_LOSS_NOTICE, SAFE_COLUMN_REFERENCE } from "./migration-hints.mjs";
9
10
 
10
11
  const FORBIDDEN_ACTIONS = [
11
12
  "Do not use --force to bypass migration safety checks.",
@@ -50,6 +51,13 @@ const USER_FIXABLE_FAILURES = {
50
51
  detail:
51
52
  "Fix schema imports, types, or Drizzle configuration before uploading any existing migration bundle.",
52
53
  },
54
+ MIGRATION_HINTS_REQUIRED: {
55
+ phase: "GENERATE",
56
+ target: "SCHEMA_SOURCE",
57
+ summary: "Drizzle needs a rename or create decision for changed columns before it can generate migrations.",
58
+ detail:
59
+ "Run `npx gencow@latest db:generate --hints '<json-array>'` with one rename or create decision per changed column, then rerun this command. Do not edit applied migration files.",
60
+ },
53
61
  MIGRATION_BUNDLE_SCHEMA_CHANGE_WITHOUT_FORWARD_MIGRATION: {
54
62
  phase: "PREFLIGHT",
55
63
  target: "PENDING_MIGRATION",
@@ -93,6 +101,30 @@ export function buildClientUserFixableDiagnostic(code, correlationId) {
93
101
  };
94
102
  }
95
103
 
104
+ // Same stable code and state as MIGRATION_HINTS_REQUIRED from db:generate, with the exact
105
+ // retry command for the named column in text fields (USER_FIXABLE diagnostics cannot carry one).
106
+ export function buildMigrationHintsRequiredDiagnostic(hints, correlationId) {
107
+ const columns = Array.isArray(hints?.columns)
108
+ ? hints.columns.filter((value) => typeof value === "string" && SAFE_COLUMN_REFERENCE.test(value))
109
+ : [];
110
+ const command = buildHintsRequiredCommand(columns);
111
+ const detail = `Declare each changed column with \`${command}\` (use {"type":"create",...} to add a new column instead), then rerun this command. ${HINTS_DATA_LOSS_NOTICE} Do not edit applied migration files.`;
112
+ const base = buildClientUserFixableDiagnostic("MIGRATION_HINTS_REQUIRED", correlationId);
113
+ return {
114
+ ...base,
115
+ retryCondition: "Run db:generate with --hints for each changed column, then rerun this command.",
116
+ reason: { code: "migration_hints_required", summary: base.reason.summary, detail },
117
+ suggestedFixes: [
118
+ {
119
+ ...base.suggestedFixes[0],
120
+ id: "migration_hints_required-fix",
121
+ title: "Declare the column decisions with db:generate --hints",
122
+ detail,
123
+ },
124
+ ],
125
+ };
126
+ }
127
+
96
128
  // Same stable code and state as MIGRATION_GENERATION_FAILED, with the folder-format reason and the
97
129
  // pinned conversion command in text fields (USER_FIXABLE diagnostics cannot carry a command).
98
130
  export function buildMigrationFolderFormatDiagnostic(folderFormat, correlationId) {