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.
- package/lib/app-response-error.mjs +8 -2
- package/lib/env-command.mjs +101 -19
- package/package.json +4 -4
- package/runtime/server.mjs +3 -6
- package/runtime/tooling.mjs +102 -50
- package/scripts/codegen-named-import-check.ts +8 -2
- package/scripts/tooling-codegen-runtime.ts +132 -55
|
@@ -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
|
-
|
|
106
|
-
if (body.
|
|
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" &&
|
package/lib/env-command.mjs
CHANGED
|
@@ -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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
439
|
-
|
|
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.
|
|
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",
|