gencow 0.1.257 → 0.1.259

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.
@@ -488,6 +488,8 @@ ${dashboardLine}
488
488
  if (delData.stopErrorCode) {
489
489
  warnImpl(`App stop needs administrator review. Code: ${delData.stopErrorCode}. Handoff ID: ${delData.handoffId}`);
490
490
  }
491
+ // The handoff changes ownership only; the app keeps serving until an administrator processes it.
492
+ warnImpl(`Until an administrator processes the deletion, the app's public URL can keep responding. Request ID: ${requestId}. This request is already recorded; do not repeat the delete.`);
491
493
  return 0;
492
494
  }
493
495
 
@@ -83,10 +83,16 @@ const SAFE_LOCAL_ERROR_CODES = new Map([
83
83
  [
84
84
  "CODEGEN_SOURCE_LOAD_FAILED",
85
85
  {
86
- message: "Deploy stopped before upload: a backend source file throws while it is loaded.",
86
+ message: "Deploy stopped before upload: a backend source file could not be loaded.",
87
87
  stage: "codegen",
88
- action:
89
- "Retryable: yes, after the top-level code that throws during module load is fixed — run `npx gencow@latest codegen` to confirm it passes, then rerun your deploy command. The throw location is not available; check the backend files that run code at import time.",
88
+ action: (caught) => {
89
+ const cause =
90
+ "the top-level code that throws during module load, or an import path that does not resolve (check the path and that the imported package is installed)";
91
+ if (safeSourceLocation(caught)) {
92
+ return `Retryable: yes, after the source at the location above is fixed (${cause}) — run \`npx gencow@latest codegen\` to confirm it passes, then rerun your deploy command.`;
93
+ }
94
+ return `Retryable: yes, after ${cause} is fixed — run \`npx gencow@latest codegen\` to confirm it passes, then rerun your deploy command. The failing location is not available; check the backend files that run code at import time and the paths they import.`;
95
+ },
90
96
  },
91
97
  ],
92
98
  [
@@ -111,16 +117,35 @@ const SAFE_LOCAL_ERROR_CODES = new Map([
111
117
  "Retryable: no, until `npx gencow@latest codegen` passes locally — run it, fix what it reports, then rerun your deploy command.",
112
118
  },
113
119
  ],
120
+ [
121
+ "STATIC_DIR_NOT_FOUND",
122
+ {
123
+ message: (caught) => {
124
+ const dir = safeDisplayDir(caught);
125
+ return dir
126
+ ? `Static deploy stopped before upload: the static directory "${dir}" does not exist or is not a directory.`
127
+ : "Static deploy stopped before upload: the static directory does not exist or is not a directory.";
128
+ },
129
+ stage: "preflight",
130
+ action: (caught) => {
131
+ const dir = safeDisplayDir(caught) ?? "<static-dir>";
132
+ if (caught.staticDirSource === "config") {
133
+ return `Retryable: yes, after ${dir} exists as a directory — run \`ls -ld ${dir}\` to confirm, or correct the client static.dir setting in your Gencow config, then rerun the same command.`;
134
+ }
135
+ return `Retryable: yes, after ${dir} exists as a directory — run \`ls -ld ${dir}\` to confirm, then rerun the same command with a correct path, or omit the directory argument to auto-detect (dist, out, build, .next/out).`;
136
+ },
137
+ },
138
+ ],
114
139
  [
115
140
  "STATIC_ARTIFACT_INDEX_EMPTY",
116
141
  {
117
142
  message:
118
- "Static deploy stopped before upload: the build output's index.html is empty or contains only whitespace.",
143
+ "Static deploy stopped before upload: the build output's index.html is empty or contains only whitespace, a BOM, or HTML comments.",
119
144
  stage: "artifact",
120
145
  action: (caught) => {
121
146
  const dir = safeDisplayDir(caught);
122
147
  const target = dir ?? "<build-output-dir>";
123
- return `Retryable: yes, after the build writes an index.html with real content into ${target} — run \`ls -l ${target}/index.html\` to confirm its size is above 0 bytes and that it is not only whitespace, then rerun \`gencow static ${target}\`.`;
148
+ return `Retryable: yes, after the build writes an index.html with real content into ${target} — run \`ls -l ${target}/index.html\` to confirm its size is above 0 bytes and that it holds more than whitespace or HTML comments, then rerun \`gencow static ${target}\`.`;
124
149
  },
125
150
  },
126
151
  ],
@@ -33,6 +33,28 @@ export function readProjectAppTargets(cwd = process.cwd()) {
33
33
  }
34
34
  }
35
35
 
36
+ // Single decision for "which prod app does --prod (+ optional --app) target".
37
+ // acceptExplicitWithoutProdApp preserves call sites that trust an explicit app
38
+ // when the project has no prodApp binding (legacy metadata); others fail closed.
39
+ export function resolveProdAppTarget({ explicitAppId = null, prodApp = null, acceptExplicitWithoutProdApp = false }) {
40
+ if (!prodApp) {
41
+ if (explicitAppId && acceptExplicitWithoutProdApp) return { appId: explicitAppId };
42
+ return { appId: null, reason: "no_prod_app" };
43
+ }
44
+ if (explicitAppId && explicitAppId !== prodApp) {
45
+ return { appId: null, reason: "explicit_app_not_prod_app", prodApp };
46
+ }
47
+ return { appId: prodApp };
48
+ }
49
+
50
+ // Shared human message for a failed prod target; falls back to the call site's own text.
51
+ export function describeProdTargetFailure(result, fallbackMessage) {
52
+ if (result?.reason === "explicit_app_not_prod_app") {
53
+ return `\`--app\` with \`--prod\` must name this project's prod app: ${result.prodApp}. Run again with --app ${result.prodApp} or without --app.`;
54
+ }
55
+ return fallbackMessage;
56
+ }
57
+
36
58
  export function resolveCloudAppTarget(restArgs, options = {}) {
37
59
  const {
38
60
  cwd = process.cwd(),
@@ -59,17 +81,24 @@ export function resolveCloudAppTarget(restArgs, options = {}) {
59
81
  if (projectTargets.invalid) {
60
82
  return { error: "MIGRATION_PROJECT_CONFIG_INVALID" };
61
83
  }
84
+ // The project's own prod app id names the prod environment even without --prod.
85
+ if (explicitApp && envTarget === "dev" && projectTargets.prodApp && appId === projectTargets.prodApp) {
86
+ envTarget = "prod";
87
+ }
62
88
  if (!appId) {
63
89
  appId = envTarget === "prod" ? projectTargets.prodApp : projectTargets.appId;
64
90
  }
65
91
 
66
92
  if (envTarget === "prod") {
67
- if (!projectTargets.prodApp && !explicitApp) {
68
- return { error: missingProdMessage };
69
- }
70
- if (explicitApp && projectTargets.prodApp && appId !== projectTargets.prodApp) {
71
- return { error: missingProdMessage };
93
+ const prodTarget = resolveProdAppTarget({
94
+ explicitAppId: explicitApp ? appId : null,
95
+ prodApp: projectTargets.prodApp,
96
+ acceptExplicitWithoutProdApp: true,
97
+ });
98
+ if (!prodTarget.appId) {
99
+ return { error: describeProdTargetFailure(prodTarget, missingProdMessage) };
72
100
  }
101
+ appId = prodTarget.appId;
73
102
  }
74
103
 
75
104
  if (!appId) {
@@ -17,6 +17,7 @@ import { createDbCheckCommand, createDbPushCommand } from "./db-push-command.mjs
17
17
  import { logEnvFileOperationWithType } from "./backend-env-resolver.mjs";
18
18
  import { runDrizzleGenerateWithFallback } from "./drizzle-generate.mjs";
19
19
  import { BOLD, CYAN, DIM, GREEN, RED, RESET, error, info, log, success, warn } from "./output.mjs";
20
+ import { describeProdTargetFailure, resolveProdAppTarget } from "./cloud-targets.mjs";
20
21
  import { platformFetch, requireCreds } from "./platform-client.mjs";
21
22
  import { readProjectMetadata } from "./project-context.mjs";
22
23
 
@@ -35,9 +36,17 @@ export function resolveDbCloudTarget(args, gencowJson = null) {
35
36
  if (gencowJson) {
36
37
  platformUrl = gencowJson.platformUrl || null;
37
38
  if (isProd) {
38
- const prodApp = gencowJson.prodApp || null;
39
- const appId = prodApp ? (!explicitAppId || explicitAppId === prodApp ? prodApp : null) : explicitAppId;
40
- return { appId, isProd, platformUrl };
39
+ const prodTarget = resolveProdAppTarget({
40
+ explicitAppId,
41
+ prodApp: gencowJson.prodApp || null,
42
+ acceptExplicitWithoutProdApp: true,
43
+ });
44
+ return {
45
+ appId: prodTarget.appId,
46
+ isProd,
47
+ platformUrl,
48
+ prodTargetFailure: prodTarget.appId ? undefined : prodTarget,
49
+ };
41
50
  }
42
51
  return { appId: explicitAppId || gencowJson.appId || gencowJson.appName || null, isProd, platformUrl };
43
52
  }
@@ -369,10 +378,16 @@ export function createDbCommands({
369
378
  readFileSyncImpl,
370
379
  resolvePathImpl,
371
380
  });
372
- const { appId, isProd, platformUrl } = resolveDbCloudTarget(args, gencowJson);
381
+ const target = resolveDbCloudTarget(args, gencowJson);
382
+ const { appId, isProd, platformUrl } = target;
373
383
 
374
384
  if (isProd && !appId) {
375
- errorImpl("Prod app not found. Run 'gencow deploy' first to create a prod app.");
385
+ errorImpl(
386
+ describeProdTargetFailure(
387
+ target.prodTargetFailure,
388
+ "Prod app not found. Run 'gencow deploy' first to create a prod app.",
389
+ ),
390
+ );
376
391
  return 1;
377
392
  }
378
393
 
@@ -53,6 +53,7 @@ import {
53
53
  resolveConfirmationMode,
54
54
  } from "./confirm-gate.mjs";
55
55
  import { emitMigrationResponsibilityNotice } from "./migration-advisory.mjs";
56
+ import { describeProdTargetFailure } from "./cloud-targets.mjs";
56
57
  import { readProjectMetadata } from "./project-context.mjs";
57
58
 
58
59
  function clientCorrelationId() {
@@ -246,7 +247,12 @@ function createDbMigrationCommand(deps, command) {
246
247
  buildClientUserFixableDiagnostic("MIGRATION_APP_TARGET_MISSING", clientCorrelationId()),
247
248
  );
248
249
  } else if (isProd) {
249
- deps.errorImpl("Prod app not found. Run 'gencow deploy' first to create a prod app.");
250
+ deps.errorImpl(
251
+ describeProdTargetFailure(
252
+ target.prodTargetFailure,
253
+ "Prod app not found. Run 'gencow deploy' first to create a prod app.",
254
+ ),
255
+ );
250
256
  } else {
251
257
  deps.errorImpl("Cloud app not found. Make sure gencow.json exists with an appId.");
252
258
  deps.infoImpl(`${DIM}Run 'gencow dev' first to create your app.${RESET}`);
@@ -33,6 +33,18 @@ function toProjectRelativePath(rawPath, cwd, processCwd) {
33
33
  return normalized;
34
34
  }
35
35
 
36
+ // esbuild prints one diagnostic per line (`<file>:<line>:<col>: ERROR: <text>`) and the
37
+ // Bun loader prints `Could not resolve`/`Cannot find module` for an import path that does
38
+ // not exist. Those are load failures: the source is well-formed, its import is not.
39
+ const DIAGNOSTIC_LINE_PATTERN = /ERROR: |Could not resolve "|Cannot find (?:module|package)\b/u;
40
+ const UNRESOLVED_MODULE_PATTERN = /Could not resolve "|Cannot find (?:module|package)\b/u;
41
+
42
+ // The first diagnostic decides the class, matching how the first location is reported.
43
+ function isUnresolvedModuleFailure(message) {
44
+ const firstDiagnostic = message.split("\n").find((line) => DIAGNOSTIC_LINE_PATTERN.test(line)) ?? message;
45
+ return UNRESOLVED_MODULE_PATTERN.test(firstDiagnostic);
46
+ }
47
+
36
48
  // Returns the first project-relative source location found in a raw message, or null.
37
49
  // Absolute paths, stacks and the raw text are never returned.
38
50
  function findProjectSourceLocation(message, { cwd, processCwd }) {
@@ -53,11 +65,14 @@ export function classifyDeployCodegenFailure(error, { cwd, backendRoot = "gencow
53
65
  const message = (error instanceof Error ? error.message : String(error ?? "")).slice(0, MAX_CLASSIFIED_MESSAGE_LENGTH);
54
66
  if (message.includes("Transpile error:")) {
55
67
  const location = findProjectSourceLocation(message, { cwd, processCwd });
68
+ const backendLocation = location && location.sourceFile.startsWith(`${backendRoot}/`) ? location : null;
69
+ if (isUnresolvedModuleFailure(message)) {
70
+ return { code: CODEGEN_FAILURE_CODES.SOURCE_LOAD_FAILED, ...(backendLocation ?? {}) };
71
+ }
56
72
  if (location) {
57
- const inBackend = location.sourceFile.startsWith(`${backendRoot}/`);
58
73
  return {
59
74
  code: CODEGEN_FAILURE_CODES.SOURCE_SYNTAX_ERROR,
60
- ...(inBackend ? location : {}),
75
+ ...(backendLocation ?? {}),
61
76
  };
62
77
  }
63
78
  }
@@ -0,0 +1,144 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2
+
3
+ import { CONFIRMATION_MODE, resolveConfirmationMode } from "./confirm-gate.mjs";
4
+ import { confirmFirstProductionDeploy } from "./deploy-first-prod-confirm.mjs";
5
+ import { reconcileProductionAppTarget } from "./deploy-app-target.mjs";
6
+ import { readJsonObjectResponse } from "./http-response-json.mjs";
7
+ import { BOLD, DIM, GREEN, RED, RESET, error, info, log, success } from "./output.mjs";
8
+ import { prodCreateFailureMessage } from "./prod-create-failure.mjs";
9
+
10
+ export async function ensureProductionDeployTarget({
11
+ appId,
12
+ displayName,
13
+ createCloudAppIfNeeded,
14
+ createInterfaceImpl,
15
+ creds,
16
+ deployArgs,
17
+ gencowJsonPath,
18
+ platformFetchImpl,
19
+ processEnv = process.env,
20
+ readFileSyncImpl = readFileSync,
21
+ setTimeoutImpl = setTimeout,
22
+ successImpl = success,
23
+ errorImpl = error,
24
+ infoImpl = info,
25
+ logImpl = log,
26
+ writeFileSyncImpl = writeFileSync,
27
+ exitImpl = (code) => process.exit(code),
28
+ prodAppId = null,
29
+ region = null,
30
+ prepareLocalSource = null,
31
+ }) {
32
+ const confirmMode = resolveConfirmationMode({
33
+ allowCiBypass: true,
34
+ args: deployArgs,
35
+ createInterfaceImpl,
36
+ processEnv,
37
+ });
38
+ // Without a recorded prod app, the first create always needs confirmation, so a
39
+ // non-interactive run fails before any platform lookup or create request.
40
+ if (!prodAppId && confirmMode === CONFIRMATION_MODE.NON_INTERACTIVE) {
41
+ await confirmFirstProductionDeploy({ appId, errorImpl, infoImpl, logImpl, mode: confirmMode, region });
42
+ exitImpl(1);
43
+ return null;
44
+ }
45
+ const reconciledTarget = await reconcileProductionAppTarget({
46
+ appId,
47
+ prodAppId,
48
+ creds,
49
+ displayName,
50
+ gencowJsonPath,
51
+ platformFetchImpl,
52
+ createCloudAppIfNeeded,
53
+ infoImpl,
54
+ errorImpl,
55
+ exitImpl,
56
+ });
57
+ if (!reconciledTarget) return null;
58
+ const productionParentAppId = reconciledTarget.appId;
59
+ const existingProdAppId = reconciledTarget.prodAppId;
60
+ if (existingProdAppId) return existingProdAppId;
61
+ if (!productionParentAppId) return null;
62
+
63
+ const proceed = await confirmFirstProductionDeploy({
64
+ appId: productionParentAppId,
65
+ createInterfaceImpl,
66
+ errorImpl,
67
+ infoImpl,
68
+ logImpl,
69
+ mode: confirmMode,
70
+ region,
71
+ });
72
+ if (!proceed) {
73
+ exitImpl(1);
74
+ return null;
75
+ }
76
+
77
+ // Local source validation must pass before any remote create: a rejected source
78
+ // (for example a codegen failure) must not leave an empty production app behind.
79
+ // The callback renders its own diagnosis and returns false when it aborts.
80
+ if (prepareLocalSource && !(await prepareLocalSource())) return null;
81
+
82
+ infoImpl("Creating prod app...");
83
+ const createProdRes = await platformFetchImpl(
84
+ creds,
85
+ `/platform/apps/${productionParentAppId}/create-prod`,
86
+ {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify(region ? { region } : {}),
90
+ },
91
+ );
92
+
93
+ if (!createProdRes.ok) {
94
+ const errData = await readJsonObjectResponse(createProdRes);
95
+ if (createProdRes.status === 403) {
96
+ logImpl("");
97
+ logImpl(` ${RED}⛔ Production Deploy — Startup or Enterprise required${RESET}`);
98
+ logImpl("");
99
+ logImpl(` ${DIM}gencow deploy --prod is for production deployment.${RESET}`);
100
+ logImpl("");
101
+ logImpl(` ${BOLD}💡 Dev environment deploy:${RESET}`);
102
+ logImpl(` ${GREEN}gencow deploy${RESET} ${DIM}← Deploy backend to dev${RESET}`);
103
+ logImpl(` ${GREEN}gencow dev${RESET} ${DIM}← Real-time backend + live logs${RESET}`);
104
+ logImpl(` ${GREEN}gencow static dist/${RESET} ${DIM}← Static file (dist/) deploy${RESET}`);
105
+ logImpl("");
106
+ logImpl(` ${BOLD}🚀 Unlock production deploy:${RESET}`);
107
+ logImpl(` ${GREEN}gencow upgrade${RESET} ${DIM}← Upgrade to Startup${RESET}`);
108
+ logImpl("");
109
+ } else {
110
+ errorImpl(prodCreateFailureMessage(errData, createProdRes));
111
+ }
112
+ exitImpl(1);
113
+ return null;
114
+ }
115
+
116
+ const createProdData = await readJsonObjectResponse(createProdRes);
117
+ if (!createProdData) {
118
+ errorImpl("Prod app creation failed: server returned an invalid JSON response");
119
+ exitImpl(1);
120
+ return null;
121
+ }
122
+ const nextProdAppId = createProdData.prodApp;
123
+ const gencowJson = existsSync(gencowJsonPath) ? JSON.parse(readFileSyncImpl(gencowJsonPath, "utf8")) : {};
124
+ gencowJson.prodApp = nextProdAppId;
125
+ writeFileSyncImpl(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
126
+
127
+ if (createProdData.alreadyExists) {
128
+ infoImpl(`Prod app verified: ${nextProdAppId}`);
129
+ if (region && createProdData.region && createProdData.region !== region) {
130
+ infoImpl(
131
+ `Region unchanged: existing prod app is in ${createProdData.region}; --region only applies to first prod creation.`,
132
+ );
133
+ }
134
+ } else {
135
+ successImpl(`Prod app created: ${nextProdAppId}`);
136
+ infoImpl(`URL: ${createProdData.url}`);
137
+ if (createProdData.region) {
138
+ infoImpl(`Region: ${createProdData.region} (${createProdData.regionSource || "selected"})`);
139
+ }
140
+ await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, 3000));
141
+ }
142
+
143
+ return nextProdAppId;
144
+ }
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
2
2
  import { basename, resolve } from "path";
3
3
  import { readProjectDisplayName } from "./cli-project-runtime.mjs";
4
- import { BOLD, CYAN, DIM, GREEN, RED, RESET, YELLOW, error, info, log, success, warn } from "./output.mjs";
4
+ import { BOLD, CYAN, DIM, RESET, YELLOW, error, info, log, success, warn } from "./output.mjs";
5
5
  import { createDeployPackageRuntime } from "./deploy-package-runtime.mjs";
6
6
  import {
7
7
  getCliInvocationSelection,
@@ -16,11 +16,9 @@ import {
16
16
  runStaticDeployFlow,
17
17
  } from "./deploy-static-fullstack-runtime.mjs";
18
18
  import { detectStaticDeployBackend, preflightStaticDeployArtifact } from "./static-deploy-command.mjs";
19
- import { prodCreateFailureMessage } from "./prod-create-failure.mjs";
19
+ import { formatCommandError } from "./cli-command-runner.mjs";
20
20
  import { readJsonObjectResponse } from "./http-response-json.mjs";
21
- import { reconcileProductionAppTarget } from "./deploy-app-target.mjs";
22
- import { CONFIRMATION_MODE, resolveConfirmationMode } from "./confirm-gate.mjs";
23
- import { confirmFirstProductionDeploy } from "./deploy-first-prod-confirm.mjs";
21
+ import { ensureProductionDeployTarget } from "./deploy-prod-target.mjs";
24
22
 
25
23
  export function parseDeployArgs(deployArgs) {
26
24
  const parsed = {
@@ -185,134 +183,12 @@ function showUnknownDeployArgument(arg, { errorImpl = error, infoImpl = info, lo
185
183
  infoImpl(" gencow static [dir] Deploy static files only");
186
184
  }
187
185
 
188
- async function ensureProductionDeployTarget({
189
- appId,
190
- displayName,
191
- createCloudAppIfNeeded,
192
- createInterfaceImpl,
193
- creds,
194
- deployArgs,
195
- gencowJsonPath,
196
- platformFetchImpl,
197
- processEnv = process.env,
198
- readFileSyncImpl = readFileSync,
199
- setTimeoutImpl = setTimeout,
200
- successImpl = success,
201
- errorImpl = error,
202
- infoImpl = info,
203
- logImpl = log,
204
- writeFileSyncImpl = writeFileSync,
205
- exitImpl = (code) => process.exit(code),
206
- prodAppId = null,
207
- region = null,
208
- }) {
209
- const confirmMode = resolveConfirmationMode({
210
- allowCiBypass: true,
211
- args: deployArgs,
212
- createInterfaceImpl,
213
- processEnv,
214
- });
215
- // Without a recorded prod app, the first create always needs confirmation, so a
216
- // non-interactive run fails before any platform lookup or create request.
217
- if (!prodAppId && confirmMode === CONFIRMATION_MODE.NON_INTERACTIVE) {
218
- await confirmFirstProductionDeploy({ appId, errorImpl, infoImpl, logImpl, mode: confirmMode, region });
219
- exitImpl(1);
220
- return null;
221
- }
222
- const reconciledTarget = await reconcileProductionAppTarget({
223
- appId,
224
- prodAppId,
225
- creds,
226
- displayName,
227
- gencowJsonPath,
228
- platformFetchImpl,
229
- createCloudAppIfNeeded,
230
- infoImpl,
231
- errorImpl,
232
- exitImpl,
233
- });
234
- if (!reconciledTarget) return null;
235
- const productionParentAppId = reconciledTarget.appId;
236
- const existingProdAppId = reconciledTarget.prodAppId;
237
- if (existingProdAppId) return existingProdAppId;
238
- if (!productionParentAppId) return null;
239
-
240
- const proceed = await confirmFirstProductionDeploy({
241
- appId: productionParentAppId,
242
- createInterfaceImpl,
243
- errorImpl,
244
- infoImpl,
245
- logImpl,
246
- mode: confirmMode,
247
- region,
248
- });
249
- if (!proceed) {
250
- exitImpl(1);
251
- return null;
252
- }
253
-
254
- infoImpl("Creating prod app...");
255
- const createProdRes = await platformFetchImpl(
256
- creds,
257
- `/platform/apps/${productionParentAppId}/create-prod`,
258
- {
259
- method: "POST",
260
- headers: { "Content-Type": "application/json" },
261
- body: JSON.stringify(region ? { region } : {}),
262
- },
263
- );
264
-
265
- if (!createProdRes.ok) {
266
- const errData = await readJsonObjectResponse(createProdRes);
267
- if (createProdRes.status === 403) {
268
- logImpl("");
269
- logImpl(` ${RED}⛔ Production Deploy — Startup or Enterprise required${RESET}`);
270
- logImpl("");
271
- logImpl(` ${DIM}gencow deploy --prod is for production deployment.${RESET}`);
272
- logImpl("");
273
- logImpl(` ${BOLD}💡 Dev environment deploy:${RESET}`);
274
- logImpl(` ${GREEN}gencow deploy${RESET} ${DIM}← Deploy backend to dev${RESET}`);
275
- logImpl(` ${GREEN}gencow dev${RESET} ${DIM}← Real-time backend + live logs${RESET}`);
276
- logImpl(` ${GREEN}gencow static dist/${RESET} ${DIM}← Static file (dist/) deploy${RESET}`);
277
- logImpl("");
278
- logImpl(` ${BOLD}🚀 Unlock production deploy:${RESET}`);
279
- logImpl(` ${GREEN}gencow upgrade${RESET} ${DIM}← Upgrade to Startup${RESET}`);
280
- logImpl("");
281
- } else {
282
- errorImpl(prodCreateFailureMessage(errData, createProdRes));
283
- }
284
- exitImpl(1);
285
- return null;
286
- }
287
-
288
- const createProdData = await readJsonObjectResponse(createProdRes);
289
- if (!createProdData) {
290
- errorImpl("Prod app creation failed: server returned an invalid JSON response");
291
- exitImpl(1);
292
- return null;
293
- }
294
- const nextProdAppId = createProdData.prodApp;
295
- const gencowJson = existsSync(gencowJsonPath) ? JSON.parse(readFileSyncImpl(gencowJsonPath, "utf8")) : {};
296
- gencowJson.prodApp = nextProdAppId;
297
- writeFileSyncImpl(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
298
-
299
- if (createProdData.alreadyExists) {
300
- infoImpl(`Prod app verified: ${nextProdAppId}`);
301
- if (region && createProdData.region && createProdData.region !== region) {
302
- infoImpl(
303
- `Region unchanged: existing prod app is in ${createProdData.region}; --region only applies to first prod creation.`,
304
- );
305
- }
306
- } else {
307
- successImpl(`Prod app created: ${nextProdAppId}`);
308
- infoImpl(`URL: ${createProdData.url}`);
309
- if (createProdData.region) {
310
- infoImpl(`Region: ${createProdData.region} (${createProdData.regionSource || "selected"})`);
311
- }
312
- await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, 3000));
313
- }
314
-
315
- return nextProdAppId;
186
+ // Returns the prepared backend source, or null when the capability check rejected it
187
+ // (the rejection is already rendered and exit is already requested).
188
+ async function prepareBackendSourceForProdCreate(runtime) {
189
+ if ((await runtime.preflightBackendCapability?.()) === false) return null;
190
+ const generatorVersion = await runtime.prepareDeployProjectSource?.({ existingBundleMode: false });
191
+ return { runtime, generatorVersion };
316
192
  }
317
193
 
318
194
  async function runRollbackDeploy({
@@ -528,12 +404,15 @@ export function createDeployCommand({
528
404
  try {
529
405
  staticArtifactPreflight = preflightStaticDeployArtifactImpl({
530
406
  staticDirArg: resolvedStaticDirArg,
407
+ // Display text is what the user typed or the client config says, never the resolved path.
408
+ staticDirDisplay: parsed.staticDirArg ?? selectedClient?.static?.dir ?? null,
409
+ staticDirSource: parsed.staticDirArg ? "argument" : "config",
531
410
  cwd: staticArtifactCwd,
532
411
  existsSyncImpl,
533
412
  resolvePathImpl,
534
413
  });
535
414
  } catch (manifestError) {
536
- errorImpl(manifestError instanceof Error ? manifestError.message : String(manifestError));
415
+ errorImpl(formatCommandError(manifestError));
537
416
  exitImpl(1);
538
417
  return;
539
418
  }
@@ -614,28 +493,46 @@ export function createDeployCommand({
614
493
  return;
615
494
  }
616
495
 
496
+ // Backend source preparation (capability check, migration generation, codegen) runs
497
+ // before the production app is created, so a rejected source creates no remote app.
498
+ // It is prepared once here and handed to deployBackendPackage/runStaticDeployFlow.
499
+ let preparedSource = null;
617
500
  if (parsed.envTarget === "prod" && appId && !parsed.existingBundleMode && !parsed.resumeRequested) {
618
- const prodTarget = await ensureProductionDeployTarget({
619
- appId: rootAppId,
620
- displayName,
621
- createCloudAppIfNeeded: backendDeployRuntime.createCloudAppIfNeeded,
622
- createInterfaceImpl,
623
- creds,
624
- deployArgs,
625
- gencowJsonPath,
626
- platformFetchImpl,
627
- processEnv,
628
- readFileSyncImpl,
629
- setTimeoutImpl,
630
- successImpl,
631
- errorImpl,
632
- infoImpl,
633
- logImpl,
634
- writeFileSyncImpl,
635
- exitImpl,
636
- prodAppId,
637
- region: parsed.region,
638
- });
501
+ // A static-only deploy has no backend source to prepare.
502
+ const preparesBackendSource = !parsed.staticDeploy || hasDetectedBackend;
503
+ let prodTarget = null;
504
+ try {
505
+ prodTarget = await ensureProductionDeployTarget({
506
+ appId: rootAppId,
507
+ displayName,
508
+ createCloudAppIfNeeded: backendDeployRuntime.createCloudAppIfNeeded,
509
+ createInterfaceImpl,
510
+ creds,
511
+ deployArgs,
512
+ gencowJsonPath,
513
+ platformFetchImpl,
514
+ processEnv,
515
+ readFileSyncImpl,
516
+ setTimeoutImpl,
517
+ successImpl,
518
+ errorImpl,
519
+ infoImpl,
520
+ logImpl,
521
+ writeFileSyncImpl,
522
+ exitImpl,
523
+ prodAppId,
524
+ region: parsed.region,
525
+ prepareLocalSource: preparesBackendSource
526
+ ? async () => {
527
+ preparedSource = await prepareBackendSourceForProdCreate(backendDeployRuntime);
528
+ return preparedSource !== null;
529
+ }
530
+ : null,
531
+ });
532
+ } finally {
533
+ // Prepared codegen output is temporary; drop it when no production app was created.
534
+ if (!prodTarget) backendDeployRuntime.cleanupPreparedDeploySource?.();
535
+ }
639
536
  if (!prodTarget) return;
640
537
  appId = prodTarget;
641
538
  infoImpl(`Deploy target: ${CYAN}${appId}${RESET} (production)`);
@@ -649,6 +546,7 @@ export function createDeployCommand({
649
546
  if (parsed.staticDeploy) {
650
547
  await runStaticDeployFlow({
651
548
  appId,
549
+ preparedSource,
652
550
  backendRoot: backendProjectRoot,
653
551
  creds,
654
552
  displayName,
@@ -736,6 +634,7 @@ export function createDeployCommand({
736
634
 
737
635
  await deployBackendPackage({
738
636
  activePackageRuntime,
637
+ preparedSource,
739
638
  appId,
740
639
  creds,
741
640
  displayName,