gencow 0.1.261 → 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" &&
@@ -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.261",
3
+ "version": "0.1.262",
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",
44
43
  "@gencow/client": "0.2.8",
45
- "@gencow/server": "0.1.3",
46
44
  "@gencow/core": "0.1.47",
47
- "@gencow/shared": "0.0.0"
45
+ "@gencow/shared": "0.0.0",
46
+ "@gencow/react": "0.2.8",
47
+ "@gencow/server": "0.1.3"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "node scripts/bundle-core.mjs && node scripts/bundle-shared.mjs && node scripts/bundle-server.mjs",