gencow 0.1.189 → 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.
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
  };
@@ -392,10 +392,11 @@ function createDbMigrationCommand(deps, command) {
392
392
  } else {
393
393
  if (!jsonMode) deps.infoImpl("Generating schema migrations...");
394
394
  try {
395
+ const generationEnv = deps.buildEnv(config, { cwd: deps.cwdImpl() });
395
396
  generatorVersion = runDrizzleGenerateWithFallback({
396
397
  cwd: deps.cwdImpl(),
397
398
  drizzleKitCmdImpl: deps.drizzleKitCmdImpl,
398
- env: deps.processEnv,
399
+ env: generationEnv,
399
400
  execSyncImpl: deps.execSyncImpl,
400
401
  stdio: jsonMode ? "pipe" : "inherit",
401
402
  warnImpl: jsonMode ? () => {} : deps.warnImpl,
@@ -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,10 +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_RECOVERY_MESSAGE =
19
- "Gencow is recovering this deployment. Your app files do not need changes.";
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.";
20
28
 
21
29
  function safeText(value, maxLength) {
22
30
  if (typeof value !== "string") return null;
@@ -27,13 +35,14 @@ function safeText(value, maxLength) {
27
35
  return normalized;
28
36
  }
29
37
 
30
- function readDeployFailureContract(value) {
38
+ function readDeployFailureContract(value, expectedAppName) {
31
39
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
32
40
  const code = safeText(value.code, 100);
33
41
  const stage = safeText(value.stage, 20);
34
42
  const retryClass = safeText(value.retryClass, 32);
35
43
  const userAction = safeText(value.userAction, 32);
36
44
  const correlationId = value.correlationId === undefined ? null : safeText(value.correlationId, 128);
45
+ const operationId = value.operationId === undefined ? null : safeText(value.operationId, 80);
37
46
  if (
38
47
  !code?.match(/^[A-Z][A-Z0-9_]{2,99}$/u) ||
39
48
  !stage ||
@@ -43,7 +52,11 @@ function readDeployFailureContract(value) {
43
52
  !userAction ||
44
53
  !USER_ACTIONS.has(userAction) ||
45
54
  (value.correlationId !== undefined && !correlationId) ||
46
- (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 ?? "")))
47
60
  ) {
48
61
  return null;
49
62
  }
@@ -53,6 +66,7 @@ function readDeployFailureContract(value) {
53
66
  retryClass,
54
67
  userAction: userAction === "CONTACT_SUPPORT" ? "OPERATOR_ACTION" : userAction,
55
68
  correlationId,
69
+ operationId,
56
70
  legacySupportAction: userAction === "CONTACT_SUPPORT",
57
71
  };
58
72
  }
@@ -63,8 +77,8 @@ function normalizeLegacySupportMessage(message) {
63
77
  : message;
64
78
  }
65
79
 
66
- export function renderDeployFailureDiagnosticLines(responseBody, statusText) {
67
- const contract = readDeployFailureContract(responseBody);
80
+ export function renderDeployFailureDiagnosticLines(responseBody, statusText, options = {}) {
81
+ const contract = readDeployFailureContract(responseBody, options.expectedAppName);
68
82
  const fallbackMessage = safeText(statusText, 120) ?? "Request failed";
69
83
  if (!contract) {
70
84
  const structuredFields =
@@ -85,13 +99,27 @@ export function renderDeployFailureDiagnosticLines(responseBody, statusText) {
85
99
  if (isLegacyCorrelationEnvelope) lines.push(correlationLine);
86
100
  return lines;
87
101
  }
88
- const platformOwned = contract.retryClass === "platform-action" && contract.userAction === "OPERATOR_ACTION";
102
+ const platformOwned =
103
+ contract.retryClass === "platform-action" && contract.userAction === "OPERATOR_ACTION";
89
104
  if (platformOwned) {
90
105
  const lines = [`Deploy failed: ${PLATFORM_RECOVERY_MESSAGE}`];
91
106
  const correlationLine = formatDeployCorrelationId(contract.correlationId);
92
107
  if (correlationLine) lines.push(correlationLine);
93
108
  return lines;
94
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
+ }
95
123
  const publicMessage = contract.legacySupportAction
96
124
  ? PLATFORM_RECOVERY_MESSAGE
97
125
  : (safeText(responseBody.error, 240) ?? fallbackMessage);
@@ -3,13 +3,10 @@ import { Readable } from "node:stream";
3
3
  import { pipeline } from "node:stream/promises";
4
4
  import { list } from "tar";
5
5
 
6
- import { MIGRATION_BUNDLE_CAPABILITY_V1 } from "@gencow/migration-contract";
6
+ import { MIGRATION_BUNDLE_CAPABILITY_V1, inspectPnpmDependencyLock } from "@gencow/migration-contract";
7
7
 
8
8
  import { canonicalizeDeployClaimV1, createDeployRequestId, encodeDeployClaimV1 } from "./deploy-contract.mjs";
9
- import {
10
- DEPLOY_LOCKFILE_CANDIDATES,
11
- UNSUPPORTED_DEPLOY_LOCKFILES,
12
- } from "./deploy-lockfile-selection.mjs";
9
+ import { DEPLOY_LOCKFILE_CANDIDATES, UNSUPPORTED_DEPLOY_LOCKFILES } from "./deploy-lockfile-selection.mjs";
13
10
 
14
11
  const PACKAGE_MANIFEST = "package.json";
15
12
  const MIGRATION_MANIFEST = ".gencow/migration-bundle-manifest.json";
@@ -57,6 +54,22 @@ function findAuthSchemaFiles(allFiles, pattern) {
57
54
  return [...allFiles].filter((path) => pattern.test(path)).sort();
58
55
  }
59
56
 
57
+ function assertDeployDependencyLock(packageBytes, lockfileBytes) {
58
+ const admission = inspectPnpmDependencyLock({
59
+ packageJsonText: packageBytes.toString("utf8"),
60
+ pnpmLockText: lockfileBytes?.toString("utf8"),
61
+ });
62
+ if (admission.ok) return;
63
+ const action =
64
+ admission.code === "APP_DEPENDENCY_LOCK_MISSING"
65
+ ? "pnpm-lock.yaml is required. Run pnpm install and retry."
66
+ : "package.json and pnpm-lock.yaml do not describe the same standalone app. Run pnpm install and retry.";
67
+ const error = new Error(`${admission.code}: ${action}`);
68
+ error.code = admission.code;
69
+ error.files = [...admission.files];
70
+ throw error;
71
+ }
72
+
60
73
  async function selectedArchiveFiles(bundle) {
61
74
  const selected = new Map();
62
75
  const allFiles = new Set();
@@ -99,7 +112,9 @@ export async function buildDeployClaimFromArchive({
99
112
  const lockfiles = [...LOCKFILES].filter(([path]) => selected.has(path));
100
113
  const unsupportedLockfiles = UNSUPPORTED_DEPLOY_LOCKFILES.filter((path) => selected.has(path));
101
114
  if (unsupportedLockfiles.length > 0) {
102
- throw new Error(`Deploy dependency lockfile is ambiguous or unsupported: ${unsupportedLockfiles.join(", ")}`);
115
+ throw new Error(
116
+ `Deploy dependency lockfile is ambiguous or unsupported: ${unsupportedLockfiles.join(", ")}`,
117
+ );
103
118
  }
104
119
  if (lockfiles.length > 1) throw new Error("Deploy dependency lockfile is ambiguous");
105
120
  const projectAuth = findAuthSchemaFiles(allFiles, CANONICAL_PROJECT_AUTH_PATTERN);
@@ -120,6 +135,9 @@ export async function buildDeployClaimFromArchive({
120
135
  : { mode: "builtin", path: null };
121
136
  const packageBytes = selected.get(PACKAGE_MANIFEST);
122
137
  const lockfile = lockfiles[0];
138
+ if (declaredRuntime === "server" && packageBytes) {
139
+ assertDeployDependencyLock(packageBytes, lockfile ? selected.get(lockfile[0]) : null);
140
+ }
123
141
  const migrationBytes = selected.get(MIGRATION_MANIFEST);
124
142
  const claim = canonicalizeDeployClaimV1({
125
143
  protocolVersion: 1,
@@ -209,6 +209,11 @@ export function createDeployPackageRuntime({
209
209
  resolvePathImpl,
210
210
  })
211
211
  ) {
212
+ const selection = detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl });
213
+ if (selection.selectedFile && migrationProjectConfig?.schema) {
214
+ throw new Error("Configured schema source is missing from the project");
215
+ }
216
+ migrationProjectConfig = null;
212
217
  logImpl(`${DIM} ok No schema source — migration generation not required${RESET}`);
213
218
  return SUPPORTED_DRIZZLE_KIT_GENERATOR_VERSION;
214
219
  }
@@ -381,12 +386,14 @@ export function createDeployPackageRuntime({
381
386
  return null;
382
387
  }
383
388
 
384
- const lockfileSelection = assertCanonicalDeployLockfile(detectDeployLockfile({
385
- cwd,
386
- packageManager: projectPkg.packageManager,
387
- existsSyncImpl,
388
- resolvePathImpl,
389
- }));
389
+ const lockfileSelection = assertCanonicalDeployLockfile(
390
+ detectDeployLockfile({
391
+ cwd,
392
+ packageManager: projectPkg.packageManager,
393
+ existsSyncImpl,
394
+ resolvePathImpl,
395
+ }),
396
+ );
390
397
  const lockfileWarning = formatDeployLockfileSelectionWarning(lockfileSelection);
391
398
  if (lockfileWarning) warnImpl(lockfileWarning);
392
399
  const projectConfigSelection = detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl });
@@ -672,7 +679,9 @@ export function createDeployPackageRuntime({
672
679
  errorImpl(lines[0]);
673
680
  lines.slice(1).forEach((line) => logImpl(line));
674
681
  } else {
675
- const lines = renderDeployFailureDiagnosticLines(errData, deployRes.statusText);
682
+ const lines = renderDeployFailureDiagnosticLines(errData, deployRes.statusText, {
683
+ expectedAppName: targetAppId,
684
+ });
676
685
  errorImpl(lines[0]);
677
686
  lines
678
687
  .slice(1)
@@ -14,6 +14,7 @@ import {
14
14
  } from "./app-diagnostics.mjs";
15
15
  import { buildDeployClaimFromArchive } from "./deploy-package-claim.mjs";
16
16
  import { formatDeployCorrelationId } from "./deploy-correlation-diagnostic.mjs";
17
+ import { pollAcceptedDeploymentOperation } from "./deployment-operation-poll.mjs";
17
18
  import { hasProjectDatabaseSchemaSource } from "./project-migration-manifest.mjs";
18
19
  import {
19
20
  extractActionableMigrationDiagnostic,
@@ -295,11 +296,25 @@ export function createDevCloudCommand(deps) {
295
296
  ...protocolHeaders,
296
297
  },
297
298
  });
298
- const data = await res.json();
299
+ // A successful POST only means the control plane accepted the bundle.
300
+ // Backend deployment, migration, and activation run asynchronously; do
301
+ // not tell `gencow dev` users that deployment completed until the
302
+ // returned operation reaches a terminal state.
303
+ const completedRes =
304
+ res.status === 202
305
+ ? await pollAcceptedDeploymentOperation({
306
+ initialResponse: res,
307
+ creds,
308
+ expectedAppName: appName,
309
+ platformFetch: deps.platformFetchImpl,
310
+ setTimeoutImpl: deps.setTimeoutImpl,
311
+ })
312
+ : res;
313
+ const data = await completedRes.json();
299
314
  const elapsed = ((Date.now() - startMs) / 1000).toFixed(1);
300
315
  const sizeKB = (bundle.length / 1024).toFixed(1);
301
316
 
302
- if (!res.ok) {
317
+ if (!completedRes.ok) {
303
318
  const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
304
319
  deps.logImpl(`${deps.DIM}${ts2}${deps.RESET} ${deps.RED}[deploy]${deps.RESET} ✗ Deploy failed!`);
305
320
  const errMsg = data.error || "Unknown error";