gencow 0.1.258 → 0.1.260

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
  [
@@ -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) {
@@ -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,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
+ }
@@ -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
+ }