gencow 0.1.259 → 0.1.261

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.
@@ -86,6 +86,11 @@ const SAFE_LOCAL_ERROR_CODES = new Map([
86
86
  message: "Deploy stopped before upload: a backend source file could not be loaded.",
87
87
  stage: "codegen",
88
88
  action: (caught) => {
89
+ if (safeMissingExportName(caught)) {
90
+ const name = safeMissingExportName(caught);
91
+ const where = safeSourceLocation(caught) ? "at the location above " : "";
92
+ return `Retryable: yes, after the import of "${name}" ${where}names an export its module does not have — use an exported name, or export it from that module, then run \`npx gencow@latest codegen\` to confirm it passes, then rerun your deploy command.`;
93
+ }
89
94
  const cause =
90
95
  "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
96
  if (safeSourceLocation(caught)) {
@@ -169,6 +174,13 @@ function safeDisplayDir(caught) {
169
174
  }
170
175
  const SAFE_SOURCE_LOCATION_PATH = /^(?![/\\])(?!.*(?:^|\/)\.\.(?:\/|$))[^\u0000-\u001f\u007f]{1,240}$/u;
171
176
 
177
+ const SAFE_IMPORT_NAME = /^[A-Za-z_$][\w$]{0,127}$/u;
178
+
179
+ function safeMissingExportName(caught) {
180
+ const { missingExport } = caught;
181
+ return typeof missingExport === "string" && SAFE_IMPORT_NAME.test(missingExport) ? missingExport : null;
182
+ }
183
+
172
184
  function safeSourceLocation(caught) {
173
185
  const { sourceFile, line, column } = caught;
174
186
  if (typeof sourceFile !== "string" || !SAFE_SOURCE_LOCATION_PATH.test(sourceFile)) return null;
@@ -47,6 +47,14 @@ export function resolveProdAppTarget({ explicitAppId = null, prodApp = null, acc
47
47
  return { appId: prodApp };
48
48
  }
49
49
 
50
+ // Single rule for the implicit prod environment: an explicit app id that is this project's
51
+ // prod app selects the prod environment even without --prod. Every command that picks a
52
+ // cloud app (cloud targets, deploy) must use this helper instead of repeating the condition.
53
+ export function resolveImplicitProdEnvironment({ explicitApp = false, envTarget = "dev", appId = null, prodApp = null }) {
54
+ if (explicitApp && envTarget === "dev" && prodApp && appId === prodApp) return "prod";
55
+ return envTarget;
56
+ }
57
+
50
58
  // Shared human message for a failed prod target; falls back to the call site's own text.
51
59
  export function describeProdTargetFailure(result, fallbackMessage) {
52
60
  if (result?.reason === "explicit_app_not_prod_app") {
@@ -81,10 +89,12 @@ export function resolveCloudAppTarget(restArgs, options = {}) {
81
89
  if (projectTargets.invalid) {
82
90
  return { error: "MIGRATION_PROJECT_CONFIG_INVALID" };
83
91
  }
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
- }
92
+ envTarget = resolveImplicitProdEnvironment({
93
+ explicitApp,
94
+ envTarget,
95
+ appId,
96
+ prodApp: projectTargets.prodApp,
97
+ });
88
98
  if (!appId) {
89
99
  appId = envTarget === "prod" ? projectTargets.prodApp : projectTargets.appId;
90
100
  }
@@ -17,22 +17,36 @@ 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
+ import {
21
+ describeProdTargetFailure,
22
+ resolveImplicitProdEnvironment,
23
+ resolveProdAppTarget,
24
+ } from "./cloud-targets.mjs";
21
25
  import { platformFetch, requireCreds } from "./platform-client.mjs";
22
26
  import { readProjectMetadata } from "./project-context.mjs";
23
27
 
24
28
  export function resolveDbCloudTarget(args, gencowJson = null) {
25
- const isProd = args.includes("--prod");
26
29
  let explicitAppId = null;
30
+ let hasExplicitApp = false;
27
31
  let platformUrl = null;
28
32
 
29
33
  for (let i = 0; i < args.length; i++) {
30
34
  if (args[i] === "--app" || args[i] === "-a") {
31
35
  explicitAppId = args[i + 1] ?? null;
36
+ hasExplicitApp = true;
32
37
  i++;
33
38
  }
34
39
  }
35
40
 
41
+ // Same implicit prod rule as deploy and static: an --app naming the project's prodApp is prod even without --prod.
42
+ const isProd =
43
+ resolveImplicitProdEnvironment({
44
+ explicitApp: hasExplicitApp,
45
+ envTarget: args.includes("--prod") ? "prod" : "dev",
46
+ appId: explicitAppId,
47
+ prodApp: gencowJson?.prodApp || null,
48
+ }) === "prod";
49
+
36
50
  if (gencowJson) {
37
51
  platformUrl = gencowJson.platformUrl || null;
38
52
  if (isProd) {
@@ -36,13 +36,24 @@ function toProjectRelativePath(rawPath, cwd, processCwd) {
36
36
  // esbuild prints one diagnostic per line (`<file>:<line>:<col>: ERROR: <text>`) and the
37
37
  // Bun loader prints `Could not resolve`/`Cannot find module` for an import path that does
38
38
  // not exist. Those are load failures: the source is well-formed, its import is not.
39
+ // `No matching export` is an import of a name its module does not export (esbuild, or the
40
+ // named-import check in codegen-named-import-check.ts); the module itself is well-formed.
39
41
  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;
42
+ const UNRESOLVED_MODULE_PATTERN = /Could not resolve "|Cannot find (?:module|package)\b|No matching export in "/u;
43
+ const MISSING_EXPORT_PATTERN = /No matching export in "[^"\n]*" for import "([A-Za-z_$][\w$]{0,127})"/u;
41
44
 
42
45
  // The first diagnostic decides the class, matching how the first location is reported.
46
+ function firstDiagnosticLine(message) {
47
+ return message.split("\n").find((line) => DIAGNOSTIC_LINE_PATTERN.test(line)) ?? message;
48
+ }
49
+
43
50
  function isUnresolvedModuleFailure(message) {
44
- const firstDiagnostic = message.split("\n").find((line) => DIAGNOSTIC_LINE_PATTERN.test(line)) ?? message;
45
- return UNRESOLVED_MODULE_PATTERN.test(firstDiagnostic);
51
+ return UNRESOLVED_MODULE_PATTERN.test(firstDiagnosticLine(message));
52
+ }
53
+
54
+ // Returns the imported name of a missing-export diagnostic (identifier only), or null.
55
+ function findMissingExportName(message) {
56
+ return MISSING_EXPORT_PATTERN.exec(firstDiagnosticLine(message))?.[1] ?? null;
46
57
  }
47
58
 
48
59
  // Returns the first project-relative source location found in a raw message, or null.
@@ -67,7 +78,12 @@ export function classifyDeployCodegenFailure(error, { cwd, backendRoot = "gencow
67
78
  const location = findProjectSourceLocation(message, { cwd, processCwd });
68
79
  const backendLocation = location && location.sourceFile.startsWith(`${backendRoot}/`) ? location : null;
69
80
  if (isUnresolvedModuleFailure(message)) {
70
- return { code: CODEGEN_FAILURE_CODES.SOURCE_LOAD_FAILED, ...(backendLocation ?? {}) };
81
+ const missingExport = findMissingExportName(message);
82
+ return {
83
+ code: CODEGEN_FAILURE_CODES.SOURCE_LOAD_FAILED,
84
+ ...(backendLocation ?? {}),
85
+ ...(missingExport ? { missingExport } : {}),
86
+ };
71
87
  }
72
88
  if (location) {
73
89
  return {
@@ -140,6 +156,7 @@ export function createDeployCodegenFailure(classification, { correlationId } = {
140
156
  error.line = classification.line;
141
157
  if (classification.column !== undefined) error.column = classification.column;
142
158
  }
159
+ if (classification.missingExport !== undefined) error.missingExport = classification.missingExport;
143
160
  // Local reference only (no Platform request exists for a codegen failure).
144
161
  if (correlationId !== undefined) error.correlationId = correlationId;
145
162
  return error;
@@ -0,0 +1,37 @@
1
+ import { unlinkSync } from "fs";
2
+
3
+ // Local-only backend source preparation for a first `deploy --prod`. Every step here
4
+ // reads the project on disk (capability check, migration generation, codegen, the
5
+ // packaging audits and the bundle with its round-trip check). It runs once, before the
6
+ // production app is created, so a rejection leaves no remote app behind.
7
+ export async function prepareBackendSourceForProdCreate(
8
+ runtime,
9
+ { forceDeploy = false, requestId = null } = {},
10
+ ) {
11
+ if ((await runtime.preflightBackendCapability?.()) === false) return null;
12
+ const generatorVersion = await runtime.prepareDeployProjectSource?.({ existingBundleMode: false });
13
+ const packageResult = await runtime.packageDeployProject({
14
+ forceDeploy,
15
+ existingBundleMode: false,
16
+ requestId,
17
+ sourcePrepared: true,
18
+ preparedGeneratorVersion: generatorVersion,
19
+ });
20
+ if (!packageResult) return null;
21
+ return { runtime, generatorVersion, packageResult };
22
+ }
23
+
24
+ // Drops the temporary bundle file and codegen output of a prepared source that did not
25
+ // reach the backend deploy (which otherwise releases them itself). Safe to call twice.
26
+ export function releasePreparedBackendSource(prepared, { unlinkSyncImpl = unlinkSync } = {}) {
27
+ if (!prepared) return;
28
+ const tmpBundle = prepared.packageResult?.tmpBundle;
29
+ if (tmpBundle) {
30
+ try {
31
+ unlinkSyncImpl(tmpBundle);
32
+ } catch {
33
+ /* already removed */
34
+ }
35
+ }
36
+ prepared.runtime?.cleanupPreparedDeploySource?.();
37
+ }
@@ -19,6 +19,11 @@ import { detectStaticDeployBackend, preflightStaticDeployArtifact } from "./stat
19
19
  import { formatCommandError } from "./cli-command-runner.mjs";
20
20
  import { readJsonObjectResponse } from "./http-response-json.mjs";
21
21
  import { ensureProductionDeployTarget } from "./deploy-prod-target.mjs";
22
+ import {
23
+ prepareBackendSourceForProdCreate,
24
+ releasePreparedBackendSource,
25
+ } from "./deploy-prepared-source.mjs";
26
+ import { resolveImplicitProdEnvironment } from "./cloud-targets.mjs";
22
27
 
23
28
  export function parseDeployArgs(deployArgs) {
24
29
  const parsed = {
@@ -183,14 +188,6 @@ function showUnknownDeployArgument(arg, { errorImpl = error, infoImpl = info, lo
183
188
  infoImpl(" gencow static [dir] Deploy static files only");
184
189
  }
185
190
 
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 };
192
- }
193
-
194
191
  async function runRollbackDeploy({
195
192
  appId,
196
193
  envTarget,
@@ -432,6 +429,14 @@ export function createDeployCommand({
432
429
  resolvePathImpl,
433
430
  });
434
431
  let { appId, displayName, gencowJsonPath, prodAppId, projectDir } = context;
432
+ // Same environment rule as cloud-targets.mjs: --app <project prod app> deploys to prod even without --prod.
433
+ // Every branch below (static, rollback, existing-bundle, backend) reads this promoted environment.
434
+ parsed.envTarget = resolveImplicitProdEnvironment({
435
+ explicitApp: Boolean(parsed.appId),
436
+ envTarget: parsed.envTarget,
437
+ appId: parsed.appId,
438
+ prodApp: prodAppId,
439
+ });
435
440
  const rootAppId = appId;
436
441
  appId = resolveEnvironmentDeployTarget({ appId, envTarget: parsed.envTarget, prodAppId });
437
442
  const backendProjectRoot = projectDir ?? projectRoot;
@@ -524,14 +529,20 @@ export function createDeployCommand({
524
529
  region: parsed.region,
525
530
  prepareLocalSource: preparesBackendSource
526
531
  ? async () => {
527
- preparedSource = await prepareBackendSourceForProdCreate(backendDeployRuntime);
532
+ preparedSource = await prepareBackendSourceForProdCreate(backendDeployRuntime, {
533
+ forceDeploy: parsed.forceDeploy,
534
+ requestId: parsed.requestId,
535
+ });
528
536
  return preparedSource !== null;
529
537
  }
530
538
  : null,
531
539
  });
532
540
  } finally {
533
- // Prepared codegen output is temporary; drop it when no production app was created.
534
- if (!prodTarget) backendDeployRuntime.cleanupPreparedDeploySource?.();
541
+ // Prepared codegen output and bundle are temporary; drop them when no production app was created.
542
+ if (!prodTarget) {
543
+ backendDeployRuntime.cleanupPreparedDeploySource?.();
544
+ releasePreparedBackendSource(preparedSource, { unlinkSyncImpl });
545
+ }
535
546
  }
536
547
  if (!prodTarget) return;
537
548
  appId = prodTarget;
@@ -544,64 +555,69 @@ export function createDeployCommand({
544
555
  }
545
556
 
546
557
  if (parsed.staticDeploy) {
547
- await runStaticDeployFlow({
548
- appId,
549
- preparedSource,
550
- backendRoot: backendProjectRoot,
551
- creds,
552
- displayName,
553
- envTarget: parsed.envTarget,
554
- forceDeploy: parsed.forceDeploy,
555
- requestId: parsed.requestId,
556
- resumeStaticOnly: parsed.resumeRequested,
557
- gencowJsonPath,
558
- hasDetectedBackend,
559
- selectedClient,
560
- staticDirArg: resolvedStaticDirArg,
561
- staticArtifactPreflight,
562
- createDeployPackageRuntimeForCwd,
563
- cwdImpl,
564
- deployPackageRuntime: backendDeployRuntime,
565
- errorImpl,
566
- existsSyncImpl,
567
- exitImpl,
568
- infoImpl,
569
- logImpl,
570
- resolvePathImpl,
571
- runStaticDeployRuntimeImpl,
572
- unlinkSyncImpl,
573
- writeFileSyncImpl,
574
- prepareReleaseAttempt: prepareReleaseAttemptImpl
575
- ? ({ appId: activeAppId, sourceRoot }) =>
576
- prepareReleaseAttemptImpl({
577
- root: sourceRoot || backendProjectRoot,
578
- sourceRootKind: "fullstack",
579
- components: ["backend", "static"],
580
- cliVersion,
581
- creds,
582
- appId: activeAppId,
583
- environment: parsed.envTarget,
584
- requestId: parsed.requestId,
585
- platformFetchImpl,
586
- capabilityTimeoutMs: 15_000,
587
- allowCapabilityTimeoutFallback: true,
588
- warnImpl,
589
- })
590
- : undefined,
591
- failReleaseAttempt: failReleaseAttemptImpl
592
- ? ({ appId: activeAppId, attemptId, failureStage, failureCode }) =>
593
- failReleaseAttemptImpl({
594
- creds,
595
- appId: activeAppId,
596
- attemptId,
597
- environment: parsed.envTarget,
598
- failureStage,
599
- failureCode,
600
- platformFetchImpl,
601
- })
602
- : undefined,
603
- successImpl,
604
- });
558
+ try {
559
+ await runStaticDeployFlow({
560
+ appId,
561
+ preparedSource,
562
+ backendRoot: backendProjectRoot,
563
+ creds,
564
+ displayName,
565
+ envTarget: parsed.envTarget,
566
+ forceDeploy: parsed.forceDeploy,
567
+ requestId: parsed.requestId,
568
+ resumeStaticOnly: parsed.resumeRequested,
569
+ gencowJsonPath,
570
+ hasDetectedBackend,
571
+ selectedClient,
572
+ staticDirArg: resolvedStaticDirArg,
573
+ staticArtifactPreflight,
574
+ createDeployPackageRuntimeForCwd,
575
+ cwdImpl,
576
+ deployPackageRuntime: backendDeployRuntime,
577
+ errorImpl,
578
+ existsSyncImpl,
579
+ exitImpl,
580
+ infoImpl,
581
+ logImpl,
582
+ resolvePathImpl,
583
+ runStaticDeployRuntimeImpl,
584
+ unlinkSyncImpl,
585
+ writeFileSyncImpl,
586
+ prepareReleaseAttempt: prepareReleaseAttemptImpl
587
+ ? ({ appId: activeAppId, sourceRoot }) =>
588
+ prepareReleaseAttemptImpl({
589
+ root: sourceRoot || backendProjectRoot,
590
+ sourceRootKind: "fullstack",
591
+ components: ["backend", "static"],
592
+ cliVersion,
593
+ creds,
594
+ appId: activeAppId,
595
+ environment: parsed.envTarget,
596
+ requestId: parsed.requestId,
597
+ platformFetchImpl,
598
+ capabilityTimeoutMs: 15_000,
599
+ allowCapabilityTimeoutFallback: true,
600
+ warnImpl,
601
+ })
602
+ : undefined,
603
+ failReleaseAttempt: failReleaseAttemptImpl
604
+ ? ({ appId: activeAppId, attemptId, failureStage, failureCode }) =>
605
+ failReleaseAttemptImpl({
606
+ creds,
607
+ appId: activeAppId,
608
+ attemptId,
609
+ environment: parsed.envTarget,
610
+ failureStage,
611
+ failureCode,
612
+ platformFetchImpl,
613
+ })
614
+ : undefined,
615
+ successImpl,
616
+ });
617
+ } finally {
618
+ // The static flow can return before the backend deploy consumes the prepared source.
619
+ releasePreparedBackendSource(preparedSource, { unlinkSyncImpl });
620
+ }
605
621
  return 0;
606
622
  }
607
623
 
@@ -125,22 +125,25 @@ export async function deployBackendPackage({
125
125
  });
126
126
  if (!activeAppId) return null;
127
127
  } else {
128
- if (!preparedSource) {
128
+ if (preparedSource) {
129
+ // Local preparation and packaging already ran once, before the production create.
130
+ packageResult = preparedSource.packageResult;
131
+ } else {
129
132
  if ((await activePackageRuntime.preflightBackendCapability?.()) === false) return null;
130
- }
131
- const preparedGeneratorVersion = preparedSource
132
- ? preparedSource.generatorVersion
133
- : await activePackageRuntime.prepareDeployProjectSource?.({ existingBundleMode });
134
- packageResult = await activePackageRuntime.packageDeployProject({
135
- forceDeploy,
136
- existingBundleMode,
137
- requestId,
138
- sourcePrepared: true,
139
- preparedGeneratorVersion,
140
- });
141
- if (!packageResult) {
142
- await markAttemptFailed("package", "PACKAGE_FAILED");
143
- return null;
133
+ const preparedGeneratorVersion = await activePackageRuntime.prepareDeployProjectSource?.({
134
+ existingBundleMode,
135
+ });
136
+ packageResult = await activePackageRuntime.packageDeployProject({
137
+ forceDeploy,
138
+ existingBundleMode,
139
+ requestId,
140
+ sourcePrepared: true,
141
+ preparedGeneratorVersion,
142
+ });
143
+ if (!packageResult) {
144
+ await markAttemptFailed("package", "PACKAGE_FAILED");
145
+ return null;
146
+ }
144
147
  }
145
148
 
146
149
  activeAppId = await activePackageRuntime.createCloudAppIfNeeded({
@@ -3,7 +3,11 @@ import { isAbsolute, resolve } from "path";
3
3
 
4
4
  import { readProjectDisplayName } from "./cli-project-runtime.mjs";
5
5
  import { formatCommandError } from "./cli-command-runner.mjs";
6
- import { describeProdTargetFailure, resolveProdAppTarget } from "./cloud-targets.mjs";
6
+ import {
7
+ describeProdTargetFailure,
8
+ resolveImplicitProdEnvironment,
9
+ resolveProdAppTarget,
10
+ } from "./cloud-targets.mjs";
7
11
  import {
8
12
  getCliInvocationSelection,
9
13
  resolveProjectMetadataPath,
@@ -291,8 +295,14 @@ export function createStaticCommand(options = {}) {
291
295
  return;
292
296
  }
293
297
  }
294
- let deployTarget = parsed.isProd ? prodAppId || appId : appId;
295
- const envTarget = parsed.isProd ? "prod" : "dev";
298
+ // Same environment rule as deploy (cloud-targets.mjs): --app <project prod app> is prod even without --prod.
299
+ const envTarget = resolveImplicitProdEnvironment({
300
+ explicitApp: Boolean(parsed.appId),
301
+ envTarget: parsed.isProd ? "prod" : "dev",
302
+ appId: parsed.appId,
303
+ prodApp: prodAppId,
304
+ });
305
+ let deployTarget = envTarget === "prod" ? prodAppId || appId : appId;
296
306
 
297
307
  if (parsed.isProd && !prodAppId) {
298
308
  deployTarget = await ensureStaticProductionTarget({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.259",
3
+ "version": "0.1.261",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,11 +40,11 @@
40
40
  "ai": "^6.0.314",
41
41
  "better-auth": "1.7.11",
42
42
  "postgres": "^3.4.9",
43
+ "@gencow/react": "0.2.8",
43
44
  "@gencow/client": "0.2.8",
44
- "@gencow/shared": "0.0.0",
45
- "@gencow/core": "0.1.47",
46
45
  "@gencow/server": "0.1.3",
47
- "@gencow/react": "0.2.8"
46
+ "@gencow/core": "0.1.47",
47
+ "@gencow/shared": "0.0.0"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "node scripts/bundle-core.mjs && node scripts/bundle-shared.mjs && node scripts/bundle-server.mjs",