gencow 0.1.188 → 0.1.190

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/core/index.js +34 -12
  2. package/lib/app-command.mjs +19 -5
  3. package/lib/app-response-contract.mjs +11 -7
  4. package/lib/canonical-drizzle-generator.config.mjs +61 -0
  5. package/lib/cli-artifact-guard.mjs +29 -0
  6. package/lib/cli-project-runtime.mjs +19 -4
  7. package/lib/db-migration-command-steps.mjs +16 -3
  8. package/lib/db-push-command.mjs +20 -1
  9. package/lib/deploy-auditor.mjs +1 -1
  10. package/lib/deploy-dependency-compat.mjs +1 -1
  11. package/lib/deploy-failure-diagnostic.mjs +46 -9
  12. package/lib/deploy-lockfile-selection.mjs +47 -11
  13. package/lib/deploy-package-claim.mjs +33 -3
  14. package/lib/deploy-package-runtime.mjs +26 -48
  15. package/lib/deployment-operation-poll.mjs +86 -37
  16. package/lib/dev-cloud-bundle.mjs +9 -21
  17. package/lib/dev-cloud-command.mjs +17 -2
  18. package/lib/doctor-command.mjs +49 -1
  19. package/lib/drizzle-generate.mjs +92 -8
  20. package/lib/drizzle-generator-invocation.mjs +23 -0
  21. package/lib/init-command.mjs +4 -3
  22. package/lib/install-features.mjs +4 -5
  23. package/lib/migration-bundle-manifest.mjs +6 -14
  24. package/lib/migration-diagnostic.mjs +13 -1
  25. package/lib/platform-client.mjs +3 -1
  26. package/lib/project-migration-manifest.mjs +0 -1
  27. package/lib/readme-codegen.mjs +2 -2
  28. package/lib/runtime-command.mjs +4 -11
  29. package/lib/static-deploy-command.mjs +1 -0
  30. package/package.json +3 -2
  31. package/runtime/server.mjs +62819 -16245
  32. package/runtime/tooling.mjs +4 -0
  33. package/scripts/bundle-server.mjs +52 -12
  34. package/scripts/pre-publish-check.mjs +20 -0
  35. package/templates/ai-chat/prompt.md +1 -1
  36. package/templates/auth.ts +1 -1
  37. package/templates/fullstack/prompt.md +1 -1
  38. package/templates/task-app/prompt.md +1 -1
  39. package/runtime/server.mjs.map +0 -7
package/core/index.js CHANGED
@@ -8,10 +8,12 @@ function hasStandardSchema(schema) {
8
8
  return !!schema && typeof schema === "object" && "~standard" in schema && !!schema["~standard"] && typeof schema["~standard"].validate === "function";
9
9
  }
10
10
  var HttpRouteInputValidationError = class extends Error {
11
- constructor(message) {
12
- super(message);
11
+ constructor(issues) {
12
+ super(issues[0]?.message ?? "Invalid input");
13
+ this.issues = issues;
13
14
  this.name = "HttpRouteInputValidationError";
14
15
  }
16
+ issues;
15
17
  };
16
18
  var HttpRouteOutputValidationError = class extends Error {
17
19
  constructor(message) {
@@ -26,10 +28,16 @@ async function validateInput(schema, value) {
26
28
  }
27
29
  const result = await schema["~standard"].validate(value);
28
30
  if ("issues" in result && result.issues.length > 0) {
29
- throw new HttpRouteInputValidationError(result.issues[0]?.message ?? "Invalid input");
31
+ throw new HttpRouteInputValidationError(
32
+ result.issues.map((issue) => ({
33
+ message: issue.message,
34
+ path: issue.path?.map(String),
35
+ code: issue.code === void 0 ? void 0 : String(issue.code)
36
+ }))
37
+ );
30
38
  }
31
39
  if ("value" in result) return result.value;
32
- throw new HttpRouteInputValidationError("Invalid input");
40
+ throw new HttpRouteInputValidationError([{ message: "Invalid input" }]);
33
41
  }
34
42
  async function validateOutput(schema, value) {
35
43
  if (!schema) return value;
@@ -50,7 +58,7 @@ async function buildInputFromRequest(method, req) {
50
58
  return await req.json();
51
59
  } catch (err) {
52
60
  const message = err instanceof Error ? err.message : String(err);
53
- throw new HttpRouteInputValidationError(`Invalid JSON body: ${message}`);
61
+ throw new HttpRouteInputValidationError([{ message: `Invalid JSON body: ${message}` }]);
54
62
  }
55
63
  }
56
64
  return { ...req.query };
@@ -105,7 +113,7 @@ function composeRouteMiddlewares(middlewares, handler, inputSchema, outputSchema
105
113
  };
106
114
  }
107
115
  var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
108
- constructor(method, routePath, middlewares = [], inputSchema, outputSchema, inputValidationIndex = -1, outputValidationIndex = -1, anonymousAllowed = false) {
116
+ constructor(method, routePath, middlewares = [], inputSchema, outputSchema, inputValidationIndex = -1, outputValidationIndex = -1, anonymousAllowed = false, inputErrorMapper) {
109
117
  this.method = method;
110
118
  this.routePath = routePath;
111
119
  this.middlewares = middlewares;
@@ -114,6 +122,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
114
122
  this.inputValidationIndex = inputValidationIndex;
115
123
  this.outputValidationIndex = outputValidationIndex;
116
124
  this.anonymousAllowed = anonymousAllowed;
125
+ this.inputErrorMapper = inputErrorMapper;
117
126
  }
118
127
  method;
119
128
  routePath;
@@ -123,6 +132,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
123
132
  inputValidationIndex;
124
133
  outputValidationIndex;
125
134
  anonymousAllowed;
135
+ inputErrorMapper;
126
136
  path(path) {
127
137
  return new _HttpRouteBuilderImpl(
128
138
  this.method,
@@ -132,7 +142,8 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
132
142
  this.outputSchema,
133
143
  this.inputValidationIndex,
134
144
  this.outputValidationIndex,
135
- this.anonymousAllowed
145
+ this.anonymousAllowed,
146
+ this.inputErrorMapper
136
147
  );
137
148
  }
138
149
  allowAnonymous() {
@@ -144,7 +155,8 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
144
155
  this.outputSchema,
145
156
  this.inputValidationIndex,
146
157
  this.outputValidationIndex,
147
- true
158
+ true,
159
+ this.inputErrorMapper
148
160
  );
149
161
  }
150
162
  use(middleware) {
@@ -156,10 +168,11 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
156
168
  this.outputSchema,
157
169
  this.inputValidationIndex,
158
170
  this.outputValidationIndex,
159
- this.anonymousAllowed
171
+ this.anonymousAllowed,
172
+ this.inputErrorMapper
160
173
  );
161
174
  }
162
- input(schema) {
175
+ input(schema, options) {
163
176
  const nextInputValidationIndex = this.middlewares.length;
164
177
  return new _HttpRouteBuilderImpl(
165
178
  this.method,
@@ -169,7 +182,8 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
169
182
  this.outputSchema,
170
183
  nextInputValidationIndex,
171
184
  this.outputValidationIndex,
172
- this.anonymousAllowed
185
+ this.anonymousAllowed,
186
+ options?.onError
173
187
  );
174
188
  }
175
189
  output(schema) {
@@ -182,7 +196,8 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
182
196
  schema,
183
197
  this.inputValidationIndex,
184
198
  nextOutputValidationIndex,
185
- this.anonymousAllowed
199
+ this.anonymousAllowed,
200
+ this.inputErrorMapper
186
201
  );
187
202
  }
188
203
  handler(handler) {
@@ -194,6 +209,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
194
209
  const isPublic = this.anonymousAllowed;
195
210
  const inputSchema = this.inputSchema;
196
211
  const outputSchema = this.outputSchema;
212
+ const inputErrorMapper = this.inputErrorMapper;
197
213
  const runRoute = composeRouteMiddlewares(
198
214
  this.middlewares,
199
215
  handler,
@@ -208,6 +224,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
208
224
  normalizedInput = inputSchema ? await buildInputFromRequest(method, req) : void 0;
209
225
  } catch (err) {
210
226
  if (err instanceof HttpRouteInputValidationError) {
227
+ if (inputErrorMapper) return await inputErrorMapper(err.issues);
211
228
  return { status: 400, body: { error: err.message } };
212
229
  }
213
230
  throw err;
@@ -217,6 +234,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
217
234
  result = await runRoute(ctx, normalizedInput, req);
218
235
  } catch (err) {
219
236
  if (err instanceof HttpRouteInputValidationError) {
237
+ if (inputErrorMapper) return await inputErrorMapper(err.issues);
220
238
  return { status: 400, body: { error: err.message } };
221
239
  }
222
240
  if (err instanceof HttpRouteOutputValidationError) {
@@ -3006,6 +3024,10 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = [
3006
3024
  "PGBOUNCER_PORT",
3007
3025
  "DIRECT_PG_PORT",
3008
3026
  "GENCOW_DIRECT_DATABASE_URL",
3027
+ "GENCOW_BACKUP_DATABASE_URL",
3028
+ "GENCOW_BACKUP_EXPECTED_ROLE",
3029
+ "GENCOW_BACKUP_RLS_BYPASS_APPROVED",
3030
+ "GENCOW_RESTORE_DATABASE_URL",
3009
3031
  "GENCOW_CATALOG_READ_DATABASE_URL",
3010
3032
  "GENCOW_DIRECT_DDL_DATABASE_URL",
3011
3033
  "DIRECT_DATABASE_URL",
@@ -10,7 +10,7 @@ import { resolveCreatedAppId, resolveCreatedAppResponse } from "./app-create-res
10
10
  import {
11
11
  isExactAppDeleteResponse,
12
12
  parseAppListResponse,
13
- parseAppStatusResponse,
13
+ parseAppLookupResponse,
14
14
  } from "./app-response-contract.mjs";
15
15
  import { pollAppDeleteOperation } from "./app-delete-operation.mjs";
16
16
  import { appResponseError } from "./app-response-error.mjs";
@@ -32,6 +32,10 @@ function invalidAppResponse(operation, response, kind) {
32
32
  return `CLI_APP_RESPONSE_INVALID: ${operation} returned an invalid ${kind} response (${appResponseStatus(response)})`;
33
33
  }
34
34
 
35
+ function appNotFoundError() {
36
+ return "App not found or not owned by you.\nCode: APP_NOT_FOUND";
37
+ }
38
+
35
39
  export function formatAppDeployedAgo(dateStr, now = Date.now()) {
36
40
  if (!dateStr) return `${DIM}never${RESET}`;
37
41
  const diff = now - new Date(dateStr).getTime();
@@ -322,22 +326,32 @@ ${dashboardLine}
322
326
  const res = await rpcQueryImpl(creds, "apps.get", { name });
323
327
  const body = await readJsonObjectResponse(res);
324
328
  if (!res.ok) {
325
- errorImpl(appResponseError(body, invalidAppResponse("app status", res, "error")));
329
+ errorImpl(
330
+ res.status === 404
331
+ ? appNotFoundError()
332
+ : appResponseError(body, invalidAppResponse("app status", res, "error")),
333
+ );
326
334
  processRef.exit(1);
327
335
  return;
328
336
  }
329
- const data = parseAppStatusResponse(body, name);
330
- if (!data) {
337
+ const lookup = parseAppLookupResponse(body, name);
338
+ if (lookup.kind === "not_found") {
339
+ errorImpl(appNotFoundError());
340
+ processRef.exit(1);
341
+ return;
342
+ }
343
+ if (lookup.kind === "invalid_response") {
331
344
  errorImpl(invalidAppResponse("app status", res, "success"));
332
345
  processRef.exit(1);
333
346
  return;
334
347
  }
348
+ const data = lookup.app;
335
349
  logImpl(`\n ${BOLD}${data.name}${RESET}`);
336
350
  const status = formatAppStatusForCli(data);
337
351
  logImpl(` Status: ${status.colored}`);
338
352
  const memoryExceededDetails = formatMemoryExceededDetails(data);
339
353
  if (memoryExceededDetails) logImpl(memoryExceededDetails);
340
- logImpl(` Port: ${data.port}`);
354
+ logImpl(` Port: ${data.port ?? `${DIM}not deployed${RESET}`}`);
341
355
  if (isAppFailureStatus(data)) {
342
356
  let diagnosticLines = selectAppDiagnosticLogLines(data);
343
357
  if (diagnosticLines.length === 0) {
@@ -25,22 +25,26 @@ export function parseAppListResponse(value) {
25
25
  : null;
26
26
  }
27
27
 
28
- export function parseAppStatusResponse(value, expectedAppId) {
28
+ export function parseAppLookupResponse(value, expectedAppId) {
29
29
  const body = record(value);
30
- if (!body) return null;
30
+ if (!body) return { kind: "invalid_response" };
31
31
  const hasEnvelope = Object.prototype.hasOwnProperty.call(body, "result");
32
+ if (hasEnvelope && body.result == null) return { kind: "not_found" };
32
33
  const app = record(hasEnvelope ? body.result : body);
33
34
  if (
34
35
  !app ||
35
36
  app.name !== expectedAppId ||
36
37
  !nonEmptyString(app.status) ||
37
- !Number.isSafeInteger(app.port) ||
38
- app.port <= 0 ||
39
- app.port > 65535
38
+ (app.port != null && (!Number.isSafeInteger(app.port) || app.port <= 0 || app.port > 65535))
40
39
  ) {
41
- return null;
40
+ return { kind: "invalid_response" };
42
41
  }
43
- return app;
42
+ return { kind: "found", app };
43
+ }
44
+
45
+ export function parseAppStatusResponse(value, expectedAppId) {
46
+ const result = parseAppLookupResponse(value, expectedAppId);
47
+ return result.kind === "found" ? result.app : null;
44
48
  }
45
49
 
46
50
  export function isExactAppDeleteResponse(value, expectedAppId) {
@@ -0,0 +1,61 @@
1
+ import { existsSync, lstatSync, realpathSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
3
+
4
+ export const GENCOW_DRIZZLE_CONFIG_CONTRACT_VERSION = 1;
5
+
6
+ function requiredEnvironment(name) {
7
+ const value = process.env[name]?.trim();
8
+ if (!value) throw new Error(`Gencow migration generator requires ${name}`);
9
+ return value;
10
+ }
11
+
12
+ function insideProject(projectRoot, candidate) {
13
+ const pathFromRoot = relative(projectRoot, candidate);
14
+ return pathFromRoot === "" || (!pathFromRoot.startsWith("..") && !isAbsolute(pathFromRoot));
15
+ }
16
+
17
+ function canonicalOutputPath(projectRoot, rawPath) {
18
+ const candidate = resolve(projectRoot, rawPath);
19
+ if (existsSync(candidate)) return realpathSync(candidate);
20
+ return resolve(realpathSync(dirname(candidate)), basename(candidate));
21
+ }
22
+
23
+ function resolveSchemaFiles(projectRoot) {
24
+ const rawPaths = requiredEnvironment("GENCOW_SCHEMA_PATHS").split("\n");
25
+ const seen = new Set();
26
+ const files = [];
27
+ for (const rawPath of rawPaths) {
28
+ const trimmed = rawPath.trim();
29
+ if (!trimmed) continue;
30
+ const candidate = resolve(projectRoot, trimmed);
31
+ const stat = lstatSync(candidate);
32
+ if (!stat.isFile() || stat.isSymbolicLink()) {
33
+ throw new Error("Gencow migration schema must be a regular project file");
34
+ }
35
+ const canonical = realpathSync(candidate);
36
+ if (!insideProject(projectRoot, canonical)) {
37
+ throw new Error("Gencow migration schema must stay inside the project root");
38
+ }
39
+ if (!seen.has(canonical)) {
40
+ seen.add(canonical);
41
+ files.push(canonical);
42
+ }
43
+ }
44
+ if (files.length === 0) throw new Error("Gencow migration schema list is empty");
45
+ return files;
46
+ }
47
+
48
+ const projectRoot = realpathSync(process.cwd());
49
+ const migrationsPath = canonicalOutputPath(projectRoot, requiredEnvironment("GENCOW_MIGRATIONS"));
50
+ if (!insideProject(projectRoot, migrationsPath)) {
51
+ throw new Error("Gencow migrations output must stay inside the project root");
52
+ }
53
+
54
+ export default {
55
+ dialect: "postgresql",
56
+ schema: resolveSchemaFiles(projectRoot),
57
+ out: `./${relative(projectRoot, migrationsPath) || "."}`,
58
+ tablesFilter: ["!_system_*", "!_gencow_*"],
59
+ driver: "pglite",
60
+ dbCredentials: { url: process.env.GENCOW_DB_URL || "./.gencow/data" },
61
+ };
@@ -5,6 +5,7 @@ export const REQUIRED_CLI_TARBALL_FILES = Object.freeze([
5
5
  "lib/deploy-auditor.mjs",
6
6
  "lib/cron-manifest.mjs",
7
7
  "lib/dev-cloud-bundle.mjs",
8
+ "lib/canonical-drizzle-generator.config.mjs",
8
9
  "runtime/tooling.mjs",
9
10
  "runtime/server.mjs",
10
11
  "templateFeature/ai/manifest.json",
@@ -17,6 +18,7 @@ export const REQUIRED_CLI_TARBALL_FILES = Object.freeze([
17
18
  ]);
18
19
 
19
20
  export const TOOLING_RUNTIME_BUNDLE_PATH = "runtime/tooling.mjs";
21
+ export const SERVER_RUNTIME_BUNDLE_PATH = "runtime/server.mjs";
20
22
 
21
23
  const FORBIDDEN_TOOLING_RUNTIME_PATTERNS = Object.freeze([
22
24
  {
@@ -38,6 +40,11 @@ const FORBIDDEN_CLI_TARBALL_FILES = Object.freeze([
38
40
  path: "lib/codegen/index.mjs",
39
41
  message: "legacy CLI codegen artifact는 npm tarball에 포함되면 안 됩니다",
40
42
  },
43
+ {
44
+ code: "forbidden-runtime-source-map",
45
+ path: "runtime/server.mjs.map",
46
+ message: "server runtime source map은 npm tarball에 포함되면 안 됩니다",
47
+ },
41
48
  ]);
42
49
 
43
50
  export function validateCliTarballFiles(files, requiredFiles = REQUIRED_CLI_TARBALL_FILES) {
@@ -93,6 +100,28 @@ export function validateGeneratedCodegenBundle(source, options = {}) {
93
100
  };
94
101
  }
95
102
 
103
+ export function validateServerRuntimeBundle(source, options = {}) {
104
+ const artifactPath = options.artifactPath ?? SERVER_RUNTIME_BUNDLE_PATH;
105
+ const repositorySourcePath = String.raw`(?:platform|server|route-gateway)[\\/]src[\\/][a-z0-9_./\\-]+\.(?:[cm]?[jt]s)`;
106
+ const repositorySourceImportPatterns = [
107
+ new RegExp("import\\(\\s*[`\"'][^`\"'\\n]{0,640}" + repositorySourcePath, "iu"),
108
+ new RegExp(
109
+ String.raw`(?:resolve|join|new\s+URL)\([^;\n]{0,640}(?:${repositorySourcePath}|["'](?:platform|server|route-gateway)["']\s*,\s*["']src["'])`,
110
+ "iu",
111
+ ),
112
+ ];
113
+ const errors = repositorySourceImportPatterns.some((pattern) => pattern.test(source))
114
+ ? [
115
+ {
116
+ code: "repository-source-import",
117
+ path: artifactPath,
118
+ message: "server runtime이 npm package 밖 repository source를 참조합니다",
119
+ },
120
+ ]
121
+ : [];
122
+ return { ok: errors.length === 0, errors };
123
+ }
124
+
96
125
  export function formatArtifactGuardErrors(errors) {
97
126
  return errors.map((error) => ` ❌ ${error.path} — ${error.message}`).join("\n");
98
127
  }
@@ -18,6 +18,7 @@ export { readProjectDisplayName, updateEnvLocalUrl } from "./project-metadata-ru
18
18
  import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
19
19
  import { hasWorkspaceConfigSourceRuntime } from "./runtime-mode.mjs";
20
20
  import { SUPPORTED_DRIZZLE_KIT_GENERATOR_VERSION } from "./drizzle-generate.mjs";
21
+ import { buildDrizzleGeneratorInvocation } from "./drizzle-generator-invocation.mjs";
21
22
 
22
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
24
 
@@ -327,7 +328,11 @@ export async function loadConfig(options = {}) {
327
328
  existsSyncImpl,
328
329
  resolvePathImpl,
329
330
  });
330
- warnProjectConfigSelectionOnce({ cwd: projectSelection.projectDir, selection: configSelection, warnImpl });
331
+ warnProjectConfigSelectionOnce({
332
+ cwd: projectSelection.projectDir,
333
+ selection: configSelection,
334
+ warnImpl,
335
+ });
331
336
  }
332
337
  } catch (caught) {
333
338
  if (throwOnInvalid) {
@@ -530,6 +535,10 @@ function ensureEsbuildConsistency(options = {}) {
530
535
 
531
536
  export function buildDrizzleKitCommand(subcmd, options = {}) {
532
537
  const {
538
+ canonicalConfigPath = resolve(
539
+ dirname(fileURLToPath(import.meta.url)),
540
+ "canonical-drizzle-generator.config.mjs",
541
+ ),
533
542
  cwd = process.cwd(),
534
543
  createRequireImpl = createRequire,
535
544
  dirnameImpl = dirname,
@@ -538,7 +547,7 @@ export function buildDrizzleKitCommand(subcmd, options = {}) {
538
547
  readFileSyncImpl = readFileSync,
539
548
  resolvePathImpl = resolve,
540
549
  } = options;
541
- if (!/^[a-z][a-z0-9:-]*$/.test(subcmd)) throw new Error(`Invalid drizzle-kit command: ${subcmd}`);
550
+ if (!/^[a-z][a-z0-9:-]*$/u.test(subcmd)) throw new Error(`Invalid drizzle-kit command: ${subcmd}`);
542
551
  try {
543
552
  const cliRequire = createRequireImpl(import.meta.url);
544
553
  const packageJsonPath = resolvePathImpl(dirnameImpl(cliRequire.resolve("drizzle-kit")), "package.json");
@@ -554,8 +563,14 @@ export function buildDrizzleKitCommand(subcmd, options = {}) {
554
563
  const resolvedBin = resolvePathImpl(dirnameImpl(packageJsonPath), binRelative);
555
564
  if (!existsSyncImpl(resolvedBin)) throw new Error("bin file missing");
556
565
  const env = ensureEsbuildConsistency({ ...options, cwd });
566
+ const invocation = buildDrizzleGeneratorInvocation({
567
+ configPath: canonicalConfigPath,
568
+ generatorBinPath: resolvedBin,
569
+ processExecPath,
570
+ subcommand: subcmd,
571
+ });
557
572
  return {
558
- cmd: `"${processExecPath}" "${resolvedBin}" ${subcmd}`,
573
+ ...invocation,
559
574
  env,
560
575
  generatorVersion: packageJson.version,
561
576
  };
@@ -595,7 +610,7 @@ export function findServerRoot(options = {}) {
595
610
  const siblingServer = resolvePathImpl(dirnameValue, "../../server");
596
611
  if (existsSyncImpl(siblingServer)) return siblingServer;
597
612
 
598
- errorImpl("Server runtime not found. Run 'bun install'.");
613
+ errorImpl("Server runtime not found. Run 'pnpm install'.");
599
614
  exitImpl(1);
600
615
  return null;
601
616
  }
@@ -180,9 +180,22 @@ export function emitCloudMigrationSuccess({
180
180
  ? "Migration state inspection completed; the plan is ready."
181
181
  : "Migration state inspection completed; the plan remains blocked.",
182
182
  );
183
- deps.logImpl(
184
- ` ${DIM}Code: ${checkData.inspection.code} · Next: ${checkData.inspection.nextAction} · Database unchanged${RESET}\n`,
185
- );
183
+ deps.logImpl(` ${DIM}Database unchanged${RESET}\n`);
184
+ const plan = checkData.inspection.plan;
185
+ if (
186
+ ready &&
187
+ plan?.safeToRewritePending === true &&
188
+ Array.isArray(plan.pendingMigrationNames) &&
189
+ plan.pendingMigrationNames.length > 0
190
+ ) {
191
+ deps.logImpl(` Applied migrations: ${plan.appliedVerifiedCount} (keep unchanged)`);
192
+ deps.logImpl(" Pending migrations that may be regenerated:");
193
+ plan.pendingMigrationNames.forEach((name) => deps.logImpl(` - ${name}`));
194
+ deps.logImpl(" Next: back up or commit the project, then remove only the listed pending folders.");
195
+ deps.logImpl(" gencow db:generate");
196
+ deps.logImpl(" pnpm exec drizzle-kit check");
197
+ deps.logImpl(` gencow db:check${environment === "prod" ? " --prod" : ""}`);
198
+ }
186
199
  return;
187
200
  }
188
201
  deps.successImpl("Migration plan is safe to proceed.");
@@ -110,6 +110,21 @@ function extractReadOnlyMigrationInspection(data) {
110
110
  return null;
111
111
  }
112
112
  const plan = inspection.plan;
113
+ const pendingMigrationNames = plan?.pendingMigrationNames;
114
+ const hasSafePendingTail =
115
+ plan?.safeToRewritePending === true &&
116
+ Array.isArray(pendingMigrationNames) &&
117
+ pendingMigrationNames.length === plan?.pendingCount &&
118
+ pendingMigrationNames.length > 0 &&
119
+ new Set(pendingMigrationNames).size === pendingMigrationNames.length &&
120
+ pendingMigrationNames.every(
121
+ (name) =>
122
+ typeof name === "string" &&
123
+ name.length <= 200 &&
124
+ name !== "." &&
125
+ name !== ".." &&
126
+ !/[\\/\u0000-\u001f\u007f]/u.test(name),
127
+ );
113
128
  if (
114
129
  inspection.outcome === "READY" &&
115
130
  !(
@@ -135,6 +150,9 @@ function extractReadOnlyMigrationInspection(data) {
135
150
  planId: plan.planId,
136
151
  appliedVerifiedCount: plan.appliedVerifiedCount,
137
152
  pendingCount: plan.pendingCount,
153
+ ...(hasSafePendingTail
154
+ ? { pendingMigrationNames: [...pendingMigrationNames], safeToRewritePending: true }
155
+ : {}),
138
156
  ...(plan.databaseBootstrapRequired === true ? { databaseBootstrapRequired: true } : {}),
139
157
  ...(plan.migrationStorePreparationRequired === true
140
158
  ? { migrationStorePreparationRequired: true }
@@ -374,10 +392,11 @@ function createDbMigrationCommand(deps, command) {
374
392
  } else {
375
393
  if (!jsonMode) deps.infoImpl("Generating schema migrations...");
376
394
  try {
395
+ const generationEnv = deps.buildEnv(config, { cwd: deps.cwdImpl() });
377
396
  generatorVersion = runDrizzleGenerateWithFallback({
378
397
  cwd: deps.cwdImpl(),
379
398
  drizzleKitCmdImpl: deps.drizzleKitCmdImpl,
380
- env: deps.processEnv,
399
+ env: generationEnv,
381
400
  execSyncImpl: deps.execSyncImpl,
382
401
  stdio: jsonMode ? "pipe" : "inherit",
383
402
  warnImpl: jsonMode ? () => {} : deps.warnImpl,
@@ -2,7 +2,7 @@
2
2
  * Deploy Auditor — Pre-deploy dependency analysis (informational).
3
3
  *
4
4
  * Detects user third-party dependencies that need auto-install on the
5
- * Gencow cloud runtime. User deps are installed via `bun install` on
5
+ * Gencow cloud runtime. User deps are installed via frozen `pnpm install` on
6
6
  * the server during provisioning.
7
7
  *
8
8
  * How it works:
@@ -78,7 +78,7 @@ export function formatDependencyVersionAuditError(result) {
78
78
 
79
79
  lines.push("");
80
80
  lines.push(" Fix:");
81
- lines.push(` bun add ${installTargets.join(" ")}`);
81
+ lines.push(` pnpm add ${installTargets.join(" ")}`);
82
82
  lines.push(" bunx gencow@latest deploy");
83
83
  lines.push("");
84
84
 
@@ -1,5 +1,6 @@
1
1
  import { MIGRATION_NEXT_ACTION_KINDS } from "@gencow/migration-contract";
2
2
 
3
+ import { APP_DELETE_OPERATION_PATTERN } from "./app-response-error.mjs";
3
4
  import { formatDeployCorrelationId } from "./deploy-correlation-diagnostic.mjs";
4
5
 
5
6
  const DEPLOY_FAILURE_STAGES = new Set([
@@ -13,9 +14,17 @@ const DEPLOY_FAILURE_STAGES = new Set([
13
14
  "controller",
14
15
  ]);
15
16
  const RETRY_CLASSES = new Set(["terminal-client", "retryable-infra", "platform-action"]);
16
- const USER_ACTIONS = new Set([...MIGRATION_NEXT_ACTION_KINDS, "OPERATOR_ACTION"]);
17
- const STRUCTURED_CONTRACT_FIELDS = ["code", "stage", "retryClass", "userAction", "correlationId"];
18
- const PLATFORM_ACTION_REQUIRED_MESSAGE = "Platform action is required before retrying";
17
+ const USER_ACTIONS = new Set([...MIGRATION_NEXT_ACTION_KINDS, "OPERATOR_ACTION", "VIEW_DELETE_STATUS"]);
18
+ const STRUCTURED_CONTRACT_FIELDS = [
19
+ "code",
20
+ "stage",
21
+ "retryClass",
22
+ "userAction",
23
+ "correlationId",
24
+ "operationId",
25
+ "appName",
26
+ ];
27
+ const PLATFORM_RECOVERY_MESSAGE = "Gencow is recovering this deployment. Your app files do not need changes.";
19
28
 
20
29
  function safeText(value, maxLength) {
21
30
  if (typeof value !== "string") return null;
@@ -26,13 +35,14 @@ function safeText(value, maxLength) {
26
35
  return normalized;
27
36
  }
28
37
 
29
- function readDeployFailureContract(value) {
38
+ function readDeployFailureContract(value, expectedAppName) {
30
39
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
31
40
  const code = safeText(value.code, 100);
32
41
  const stage = safeText(value.stage, 20);
33
42
  const retryClass = safeText(value.retryClass, 32);
34
43
  const userAction = safeText(value.userAction, 32);
35
44
  const correlationId = value.correlationId === undefined ? null : safeText(value.correlationId, 128);
45
+ const operationId = value.operationId === undefined ? null : safeText(value.operationId, 80);
36
46
  if (
37
47
  !code?.match(/^[A-Z][A-Z0-9_]{2,99}$/u) ||
38
48
  !stage ||
@@ -42,27 +52,33 @@ function readDeployFailureContract(value) {
42
52
  !userAction ||
43
53
  !USER_ACTIONS.has(userAction) ||
44
54
  (value.correlationId !== undefined && !correlationId) ||
45
- (correlationId && !/^[A-Za-z0-9_-]{8,128}$/u.test(correlationId))
55
+ (correlationId && !/^[A-Za-z0-9_-]{8,128}$/u.test(correlationId)) ||
56
+ (userAction === "VIEW_DELETE_STATUS" &&
57
+ (!operationId ||
58
+ !APP_DELETE_OPERATION_PATTERN.test(operationId) ||
59
+ !/^[a-z][a-z0-9]*(?:-[a-z0-9]+){2}$/u.test(expectedAppName ?? "")))
46
60
  ) {
47
61
  return null;
48
62
  }
49
63
  return {
50
64
  code,
51
65
  stage,
66
+ retryClass,
52
67
  userAction: userAction === "CONTACT_SUPPORT" ? "OPERATOR_ACTION" : userAction,
53
68
  correlationId,
69
+ operationId,
54
70
  legacySupportAction: userAction === "CONTACT_SUPPORT",
55
71
  };
56
72
  }
57
73
 
58
74
  function normalizeLegacySupportMessage(message) {
59
75
  return typeof message === "string" && /contact support/iu.test(message)
60
- ? PLATFORM_ACTION_REQUIRED_MESSAGE
76
+ ? PLATFORM_RECOVERY_MESSAGE
61
77
  : message;
62
78
  }
63
79
 
64
- export function renderDeployFailureDiagnosticLines(responseBody, statusText) {
65
- const contract = readDeployFailureContract(responseBody);
80
+ export function renderDeployFailureDiagnosticLines(responseBody, statusText, options = {}) {
81
+ const contract = readDeployFailureContract(responseBody, options.expectedAppName);
66
82
  const fallbackMessage = safeText(statusText, 120) ?? "Request failed";
67
83
  if (!contract) {
68
84
  const structuredFields =
@@ -83,8 +99,29 @@ export function renderDeployFailureDiagnosticLines(responseBody, statusText) {
83
99
  if (isLegacyCorrelationEnvelope) lines.push(correlationLine);
84
100
  return lines;
85
101
  }
102
+ const platformOwned =
103
+ contract.retryClass === "platform-action" && contract.userAction === "OPERATOR_ACTION";
104
+ if (platformOwned) {
105
+ const lines = [`Deploy failed: ${PLATFORM_RECOVERY_MESSAGE}`];
106
+ const correlationLine = formatDeployCorrelationId(contract.correlationId);
107
+ if (correlationLine) lines.push(correlationLine);
108
+ return lines;
109
+ }
110
+ if (contract.userAction === "VIEW_DELETE_STATUS") {
111
+ const publicMessage = safeText(responseBody.error, 240) ?? fallbackMessage;
112
+ const lines = [
113
+ `Deploy failed: ${publicMessage}`,
114
+ `Code: ${contract.code}`,
115
+ `Stage: ${contract.stage}`,
116
+ `Operation ID: ${contract.operationId}`,
117
+ ];
118
+ const correlationLine = formatDeployCorrelationId(contract.correlationId);
119
+ if (correlationLine) lines.push(correlationLine);
120
+ lines.push(`Next command: gencow app delete ${options.expectedAppName} --force`);
121
+ return lines;
122
+ }
86
123
  const publicMessage = contract.legacySupportAction
87
- ? PLATFORM_ACTION_REQUIRED_MESSAGE
124
+ ? PLATFORM_RECOVERY_MESSAGE
88
125
  : (safeText(responseBody.error, 240) ?? fallbackMessage);
89
126
  const lines = [`Deploy failed: ${publicMessage}`];
90
127
  lines.push(`Code: ${contract.code}`, `Stage: ${contract.stage}`);