gencow 0.1.260 → 0.1.262

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.
@@ -24,6 +24,7 @@ import {
24
24
 
25
25
  export { APP_DELETE_OPERATION_PATTERN } from "./app-delete-diagnostic.mjs";
26
26
 
27
+ const APP_DELETE_DIAGNOSTIC_STAGES = new Set(["admission", "runtime-stop", "tenant-database", "catalog-finalize"]);
27
28
  const RETRY_CLASSES = new Set(["operator-action", "platform-action", "retryable-infra", "terminal-client"]);
28
29
  const USER_ACTIONS = new Set([
29
30
  "CONTACT_SUPPORT",
@@ -102,8 +103,13 @@ export function appResponseError(body, fallback, response = null, options = {})
102
103
  if (/^[A-Za-z0-9_-]{8,128}$/u.test(body.correlationId ?? "")) {
103
104
  lines.push(`Correlation ID: ${body.correlationId}`);
104
105
  }
105
- if (body.stage === "admission") lines.push("Stage: admission");
106
- if (body.stateImpact === "This deletion request made no ownership or resource change.") {
106
+ // Delete-stage diagnoses from the Platform name the failed stage and its state impact.
107
+ if (APP_DELETE_DIAGNOSTIC_STAGES.has(body.stage)) lines.push(`Stage: ${body.stage}`);
108
+ if (
109
+ APP_DELETE_DIAGNOSTIC_STAGES.has(body.stage) &&
110
+ typeof body.stateImpact === "string" &&
111
+ /^[A-Za-z0-9 ,.;:-]{1,200}$/u.test(body.stateImpact)
112
+ ) {
107
113
  lines.push(`State impact: ${body.stateImpact}`);
108
114
  }
109
115
  if (typeof body.retryPrecondition === "string" &&
@@ -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;
@@ -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;
@@ -10,6 +10,56 @@ import { resolveCloudAppTarget } from "./cloud-targets.mjs";
10
10
  import { BOLD, CYAN, DIM, GREEN, RESET, error, info, log, success, warn } from "./output.mjs";
11
11
  import { platformFetch, requireCreds } from "./platform-client.mjs";
12
12
 
13
+ const ENV_RESPONSE_UNREADABLE_CODE = "ENV_RESPONSE_UNREADABLE";
14
+
15
+ // Reads a failed platform response. A JSON `null`, an empty body, or a non-object body
16
+ // normalizes to `message: null`, so callers never dereference the parsed value.
17
+ async function readEnvErrorResponse(response) {
18
+ const parsed = await response.json().catch(() => null);
19
+ const body = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
20
+ const message = typeof body?.error === "string" && body.error.trim() ? body.error.trim() : null;
21
+ return { ...envResponseFacts(response), message };
22
+ }
23
+
24
+ function envResponseFacts(response) {
25
+ const correlationId = response.headers?.get?.("x-correlation-id") ?? null;
26
+ return {
27
+ status: response.status,
28
+ correlationId: /^[A-Za-z0-9_-]{8,128}$/u.test(correlationId ?? "") ? correlationId : null,
29
+ };
30
+ }
31
+
32
+ // A typed `{error}` text is kept verbatim. Without one, the CLI uses the same labels as
33
+ // renderProjectionResult, with state reported as unknown because the CLI cannot observe it.
34
+ function formatEnvErrorMessage(failure, { stage, checkCommand, retryable = isRetryableEnvStatus(failure.status) }) {
35
+ if (failure.message) return failure.message;
36
+ return [
37
+ "Platform returned an empty or unreadable error response",
38
+ `Code: ${ENV_RESPONSE_UNREADABLE_CODE}`,
39
+ `Stage: ${stage}`,
40
+ `HTTP ${failure.status}`,
41
+ failure.correlationId ? `Correlation ID: ${failure.correlationId}` : null,
42
+ "Catalog changed: unknown",
43
+ "Serving process changed: unknown",
44
+ `Retryable: ${retryable ? "yes" : "no"}`,
45
+ `Next action: run ${checkCommand}, then retry only if the change is not applied.`,
46
+ ]
47
+ .filter(Boolean)
48
+ .join("\n");
49
+ }
50
+
51
+ function isRetryableEnvStatus(status) {
52
+ return status === 408 || status === 429 || status >= 500;
53
+ }
54
+
55
+ function envTargetFlags(appId, envTarget) {
56
+ return envTarget === "prod" ? "--prod" : `--app ${appId}`;
57
+ }
58
+
59
+ function envListCommand(appId, envTarget) {
60
+ return `gencow env list ${envTargetFlags(appId, envTarget)}`;
61
+ }
62
+
13
63
  export function sanitizeCloudEnvKey(rawKey) {
14
64
  return rawKey
15
65
  .trim()
@@ -86,29 +136,36 @@ export function createEnvCommand(deps = {}) {
86
136
  const sleepImpl = deps.sleep ?? ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)));
87
137
  const projectionPollAttempts = deps.projectionPollAttempts ?? 40;
88
138
 
89
- async function loadProjectionStatus(creds, appId, operationId) {
139
+ async function loadProjectionStatus(creds, appId, operationId, envTarget) {
90
140
  const response = await platformFetchImpl(
91
141
  creds,
92
142
  `/platform/apps/${appId}/env/operations/${encodeURIComponent(operationId)}`,
93
143
  { method: "GET" },
94
144
  );
95
- const body = await response.json().catch(() => ({}));
96
- if (!response.ok) throw new Error(body.error || `HTTP ${response.status}`);
97
- return body;
145
+ if (!response.ok) {
146
+ throw new Error(
147
+ formatEnvErrorMessage(await readEnvErrorResponse(response), {
148
+ stage: "env_status",
149
+ checkCommand: `gencow env status --operation ${operationId} ${envTargetFlags(appId, envTarget)}`,
150
+ }),
151
+ );
152
+ }
153
+ const body = await response.json().catch(() => null);
154
+ return body && typeof body === "object" ? body : {};
98
155
  }
99
156
 
100
- async function waitForProjection(creds, appId, initial) {
157
+ async function waitForProjection(creds, appId, initial, envTarget) {
101
158
  if (!initial?.operationId || initial.runtimeState !== "pending") return initial ?? {};
102
159
  let current = initial;
103
160
  for (let attempt = 0; attempt < projectionPollAttempts; attempt += 1) {
104
161
  await sleepImpl(Number(current.retryAfterMs) || 500);
105
162
  try {
106
- current = await loadProjectionStatus(creds, appId, initial.operationId);
163
+ current = await loadProjectionStatus(creds, appId, initial.operationId, envTarget);
107
164
  } catch {
108
165
  return {
109
166
  ...current,
110
167
  statusQueryFailed: true,
111
- nextAction: `Run gencow env status --operation ${initial.operationId}`,
168
+ nextAction: `Run gencow env status --operation ${initial.operationId} ${envTargetFlags(appId, envTarget)}`,
112
169
  };
113
170
  }
114
171
  if (current.runtimeState !== "pending") return current;
@@ -205,7 +262,7 @@ export function createEnvCommand(deps = {}) {
205
262
  return;
206
263
  }
207
264
  try {
208
- return renderProjectionResult(await loadProjectionStatus(creds, appId, operationId));
265
+ return renderProjectionResult(await loadProjectionStatus(creds, appId, operationId, envTarget));
209
266
  } catch (e) {
210
267
  error(`Failed to fetch env operation: ${e.message}`);
211
268
  return 1;
@@ -223,7 +280,20 @@ export function createEnvCommand(deps = {}) {
223
280
  const reason = typeof body?.error === "string" && body.error ? body.error : null;
224
281
  throw new Error(reason ?? `HTTP ${res.status}`);
225
282
  }
226
- const vars = await res.json();
283
+ const vars = await res.json().catch(() => null);
284
+ if (!Array.isArray(vars)) {
285
+ throw new Error(
286
+ formatEnvErrorMessage(
287
+ { ...envResponseFacts(res), message: null },
288
+ {
289
+ stage: "env_list",
290
+ checkCommand: envListCommand(appId, envTarget),
291
+ // A successful list read is side-effect free, so retrying it is safe.
292
+ retryable: true,
293
+ },
294
+ ),
295
+ );
296
+ }
227
297
 
228
298
  log(
229
299
  `\n${BOLD}Environment Variables${RESET} ${DIM}(cloud: ${appId}, ${envTarget}, ${vars.length} total)${RESET}\n`,
@@ -297,11 +367,15 @@ export function createEnvCommand(deps = {}) {
297
367
  body: JSON.stringify({ key: name, value, env: envTarget }),
298
368
  });
299
369
  if (!res.ok) {
300
- const body = await res.json().catch(() => ({}));
301
- throw new Error(body.error || `HTTP ${res.status}`);
370
+ throw new Error(
371
+ formatEnvErrorMessage(await readEnvErrorResponse(res), {
372
+ stage: "env_set",
373
+ checkCommand: envListCommand(appId, envTarget),
374
+ }),
375
+ );
302
376
  }
303
377
  const mutation = await res.json().catch(() => ({}));
304
- const projection = await waitForProjection(creds, appId, mutation);
378
+ const projection = await waitForProjection(creds, appId, mutation, envTarget);
305
379
  const projectionExitCode = renderProjectionResult(projection);
306
380
  if (projectionExitCode === 0) {
307
381
  success(`Stored ${BOLD}${name}${RESET} ${DIM}(cloud: ${appId}, ${envTarget})${RESET}`);
@@ -346,11 +420,15 @@ export function createEnvCommand(deps = {}) {
346
420
  },
347
421
  );
348
422
  if (!res.ok) {
349
- const body = await res.json().catch(() => ({}));
350
- throw new Error(body.error || `HTTP ${res.status}`);
423
+ throw new Error(
424
+ formatEnvErrorMessage(await readEnvErrorResponse(res), {
425
+ stage: "env_unset",
426
+ checkCommand: envListCommand(appId, envTarget),
427
+ }),
428
+ );
351
429
  }
352
430
  const mutation = await res.json().catch(() => ({}));
353
- const projection = await waitForProjection(creds, appId, mutation);
431
+ const projection = await waitForProjection(creds, appId, mutation, envTarget);
354
432
  const projectionExitCode = renderProjectionResult(projection);
355
433
  if (projectionExitCode === 0) {
356
434
  success(`Removed ${BOLD}${key}${RESET} ${DIM}(cloud: ${appId}, ${envTarget})${RESET}`);
@@ -435,15 +513,19 @@ export function createEnvCommand(deps = {}) {
435
513
  body: JSON.stringify({ vars, env: envTarget }),
436
514
  });
437
515
  if (!res.ok) {
438
- const body = await res.json().catch(() => ({}));
439
- throw new Error(body.error || `HTTP ${res.status}`);
516
+ throw new Error(
517
+ formatEnvErrorMessage(await readEnvErrorResponse(res), {
518
+ stage: "env_push",
519
+ checkCommand: envListCommand(appId, envTarget),
520
+ }),
521
+ );
440
522
  }
441
- const data = await res.json();
523
+ const data = (await res.json().catch(() => null)) ?? {};
442
524
  for (const key of Object.keys(vars)) {
443
525
  success(`${key}`);
444
526
  }
445
527
  log(`\n ${GREEN}${data.count || count} set${RESET} ${DIM}(cloud: ${appId}, ${envTarget})${RESET}`);
446
- commandFailed = renderProjectionResult(await waitForProjection(creds, appId, data)) !== 0;
528
+ commandFailed = renderProjectionResult(await waitForProjection(creds, appId, data, envTarget)) !== 0;
447
529
  } catch (e) {
448
530
  error(`Failed to push env vars: ${e.message}`);
449
531
  commandFailed = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.260",
3
+ "version": "0.1.262",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,9 +41,9 @@
41
41
  "better-auth": "1.7.11",
42
42
  "postgres": "^3.4.9",
43
43
  "@gencow/client": "0.2.8",
44
- "@gencow/react": "0.2.8",
45
44
  "@gencow/core": "0.1.47",
46
45
  "@gencow/shared": "0.0.0",
46
+ "@gencow/react": "0.2.8",
47
47
  "@gencow/server": "0.1.3"
48
48
  },
49
49
  "scripts": {