gencow 0.1.258 → 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.
@@ -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
  [
@@ -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,
@@ -17,11 +17,8 @@ import {
17
17
  } from "./deploy-static-fullstack-runtime.mjs";
18
18
  import { detectStaticDeployBackend, preflightStaticDeployArtifact } from "./static-deploy-command.mjs";
19
19
  import { formatCommandError } from "./cli-command-runner.mjs";
20
- import { prodCreateFailureMessage } from "./prod-create-failure.mjs";
21
20
  import { readJsonObjectResponse } from "./http-response-json.mjs";
22
- import { reconcileProductionAppTarget } from "./deploy-app-target.mjs";
23
- import { CONFIRMATION_MODE, resolveConfirmationMode } from "./confirm-gate.mjs";
24
- import { confirmFirstProductionDeploy } from "./deploy-first-prod-confirm.mjs";
21
+ import { ensureProductionDeployTarget } from "./deploy-prod-target.mjs";
25
22
 
26
23
  export function parseDeployArgs(deployArgs) {
27
24
  const parsed = {
@@ -186,134 +183,12 @@ function showUnknownDeployArgument(arg, { errorImpl = error, infoImpl = info, lo
186
183
  infoImpl(" gencow static [dir] Deploy static files only");
187
184
  }
188
185
 
189
- async function ensureProductionDeployTarget({
190
- appId,
191
- displayName,
192
- createCloudAppIfNeeded,
193
- createInterfaceImpl,
194
- creds,
195
- deployArgs,
196
- gencowJsonPath,
197
- platformFetchImpl,
198
- processEnv = process.env,
199
- readFileSyncImpl = readFileSync,
200
- setTimeoutImpl = setTimeout,
201
- successImpl = success,
202
- errorImpl = error,
203
- infoImpl = info,
204
- logImpl = log,
205
- writeFileSyncImpl = writeFileSync,
206
- exitImpl = (code) => process.exit(code),
207
- prodAppId = null,
208
- region = null,
209
- }) {
210
- const confirmMode = resolveConfirmationMode({
211
- allowCiBypass: true,
212
- args: deployArgs,
213
- createInterfaceImpl,
214
- processEnv,
215
- });
216
- // Without a recorded prod app, the first create always needs confirmation, so a
217
- // non-interactive run fails before any platform lookup or create request.
218
- if (!prodAppId && confirmMode === CONFIRMATION_MODE.NON_INTERACTIVE) {
219
- await confirmFirstProductionDeploy({ appId, errorImpl, infoImpl, logImpl, mode: confirmMode, region });
220
- exitImpl(1);
221
- return null;
222
- }
223
- const reconciledTarget = await reconcileProductionAppTarget({
224
- appId,
225
- prodAppId,
226
- creds,
227
- displayName,
228
- gencowJsonPath,
229
- platformFetchImpl,
230
- createCloudAppIfNeeded,
231
- infoImpl,
232
- errorImpl,
233
- exitImpl,
234
- });
235
- if (!reconciledTarget) return null;
236
- const productionParentAppId = reconciledTarget.appId;
237
- const existingProdAppId = reconciledTarget.prodAppId;
238
- if (existingProdAppId) return existingProdAppId;
239
- if (!productionParentAppId) return null;
240
-
241
- const proceed = await confirmFirstProductionDeploy({
242
- appId: productionParentAppId,
243
- createInterfaceImpl,
244
- errorImpl,
245
- infoImpl,
246
- logImpl,
247
- mode: confirmMode,
248
- region,
249
- });
250
- if (!proceed) {
251
- exitImpl(1);
252
- return null;
253
- }
254
-
255
- infoImpl("Creating prod app...");
256
- const createProdRes = await platformFetchImpl(
257
- creds,
258
- `/platform/apps/${productionParentAppId}/create-prod`,
259
- {
260
- method: "POST",
261
- headers: { "Content-Type": "application/json" },
262
- body: JSON.stringify(region ? { region } : {}),
263
- },
264
- );
265
-
266
- if (!createProdRes.ok) {
267
- const errData = await readJsonObjectResponse(createProdRes);
268
- if (createProdRes.status === 403) {
269
- logImpl("");
270
- logImpl(` ${RED}⛔ Production Deploy — Startup or Enterprise required${RESET}`);
271
- logImpl("");
272
- logImpl(` ${DIM}gencow deploy --prod is for production deployment.${RESET}`);
273
- logImpl("");
274
- logImpl(` ${BOLD}💡 Dev environment deploy:${RESET}`);
275
- logImpl(` ${GREEN}gencow deploy${RESET} ${DIM}← Deploy backend to dev${RESET}`);
276
- logImpl(` ${GREEN}gencow dev${RESET} ${DIM}← Real-time backend + live logs${RESET}`);
277
- logImpl(` ${GREEN}gencow static dist/${RESET} ${DIM}← Static file (dist/) deploy${RESET}`);
278
- logImpl("");
279
- logImpl(` ${BOLD}🚀 Unlock production deploy:${RESET}`);
280
- logImpl(` ${GREEN}gencow upgrade${RESET} ${DIM}← Upgrade to Startup${RESET}`);
281
- logImpl("");
282
- } else {
283
- errorImpl(prodCreateFailureMessage(errData, createProdRes));
284
- }
285
- exitImpl(1);
286
- return null;
287
- }
288
-
289
- const createProdData = await readJsonObjectResponse(createProdRes);
290
- if (!createProdData) {
291
- errorImpl("Prod app creation failed: server returned an invalid JSON response");
292
- exitImpl(1);
293
- return null;
294
- }
295
- const nextProdAppId = createProdData.prodApp;
296
- const gencowJson = existsSync(gencowJsonPath) ? JSON.parse(readFileSyncImpl(gencowJsonPath, "utf8")) : {};
297
- gencowJson.prodApp = nextProdAppId;
298
- writeFileSyncImpl(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
299
-
300
- if (createProdData.alreadyExists) {
301
- infoImpl(`Prod app verified: ${nextProdAppId}`);
302
- if (region && createProdData.region && createProdData.region !== region) {
303
- infoImpl(
304
- `Region unchanged: existing prod app is in ${createProdData.region}; --region only applies to first prod creation.`,
305
- );
306
- }
307
- } else {
308
- successImpl(`Prod app created: ${nextProdAppId}`);
309
- infoImpl(`URL: ${createProdData.url}`);
310
- if (createProdData.region) {
311
- infoImpl(`Region: ${createProdData.region} (${createProdData.regionSource || "selected"})`);
312
- }
313
- await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, 3000));
314
- }
315
-
316
- 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 };
317
192
  }
318
193
 
319
194
  async function runRollbackDeploy({
@@ -618,28 +493,46 @@ export function createDeployCommand({
618
493
  return;
619
494
  }
620
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;
621
500
  if (parsed.envTarget === "prod" && appId && !parsed.existingBundleMode && !parsed.resumeRequested) {
622
- const prodTarget = await ensureProductionDeployTarget({
623
- appId: rootAppId,
624
- displayName,
625
- createCloudAppIfNeeded: backendDeployRuntime.createCloudAppIfNeeded,
626
- createInterfaceImpl,
627
- creds,
628
- deployArgs,
629
- gencowJsonPath,
630
- platformFetchImpl,
631
- processEnv,
632
- readFileSyncImpl,
633
- setTimeoutImpl,
634
- successImpl,
635
- errorImpl,
636
- infoImpl,
637
- logImpl,
638
- writeFileSyncImpl,
639
- exitImpl,
640
- prodAppId,
641
- region: parsed.region,
642
- });
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
+ }
643
536
  if (!prodTarget) return;
644
537
  appId = prodTarget;
645
538
  infoImpl(`Deploy target: ${CYAN}${appId}${RESET} (production)`);
@@ -653,6 +546,7 @@ export function createDeployCommand({
653
546
  if (parsed.staticDeploy) {
654
547
  await runStaticDeployFlow({
655
548
  appId,
549
+ preparedSource,
656
550
  backendRoot: backendProjectRoot,
657
551
  creds,
658
552
  displayName,
@@ -740,6 +634,7 @@ export function createDeployCommand({
740
634
 
741
635
  await deployBackendPackage({
742
636
  activePackageRuntime,
637
+ preparedSource,
743
638
  appId,
744
639
  creds,
745
640
  displayName,
@@ -72,6 +72,9 @@ export function createScopedDeployPackageRuntime({
72
72
 
73
73
  export async function deployBackendPackage({
74
74
  activePackageRuntime,
75
+ // Set when the backend source was prepared before the production app was created.
76
+ // Its runtime must be the one that packages (it holds the prepared codegen output).
77
+ preparedSource = null,
75
78
  appId,
76
79
  creds,
77
80
  displayName,
@@ -122,10 +125,12 @@ export async function deployBackendPackage({
122
125
  });
123
126
  if (!activeAppId) return null;
124
127
  } else {
125
- if ((await activePackageRuntime.preflightBackendCapability?.()) === false) return null;
126
- const preparedGeneratorVersion = await activePackageRuntime.prepareDeployProjectSource?.({
127
- existingBundleMode,
128
- });
128
+ if (!preparedSource) {
129
+ if ((await activePackageRuntime.preflightBackendCapability?.()) === false) return null;
130
+ }
131
+ const preparedGeneratorVersion = preparedSource
132
+ ? preparedSource.generatorVersion
133
+ : await activePackageRuntime.prepareDeployProjectSource?.({ existingBundleMode });
129
134
  packageResult = await activePackageRuntime.packageDeployProject({
130
135
  forceDeploy,
131
136
  existingBundleMode,
@@ -262,6 +267,7 @@ export async function deployBackendPackage({
262
267
 
263
268
  export async function runStaticDeployFlow({
264
269
  appId,
270
+ preparedSource = null,
265
271
  backendRoot,
266
272
  creds,
267
273
  displayName,
@@ -332,11 +338,13 @@ export async function runStaticDeployFlow({
332
338
  logImpl("");
333
339
 
334
340
  const runtime =
335
- backendRoot === frontendCwd
341
+ preparedSource?.runtime ??
342
+ (backendRoot === frontendCwd
336
343
  ? deployPackageRuntime
337
- : createDeployPackageRuntimeForCwd(() => backendRoot);
344
+ : createDeployPackageRuntimeForCwd(() => backendRoot));
338
345
  const deployed = await deployBackendPackage({
339
346
  activePackageRuntime: runtime,
347
+ preparedSource,
340
348
  appId: activeAppId,
341
349
  creds,
342
350
  displayName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.258",
3
+ "version": "0.1.259",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,10 +41,10 @@
41
41
  "better-auth": "1.7.11",
42
42
  "postgres": "^3.4.9",
43
43
  "@gencow/client": "0.2.8",
44
+ "@gencow/shared": "0.0.0",
44
45
  "@gencow/core": "0.1.47",
45
- "@gencow/react": "0.2.8",
46
46
  "@gencow/server": "0.1.3",
47
- "@gencow/shared": "0.0.0"
47
+ "@gencow/react": "0.2.8"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "node scripts/bundle-core.mjs && node scripts/bundle-shared.mjs && node scripts/bundle-server.mjs",