gencow 0.1.252 → 0.1.254
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/core/index.js +129 -108
- package/docs/packaging.md +7 -3
- package/docs/troubleshooting.md +1 -1
- package/lib/app-command.mjs +64 -9
- package/lib/backup-command.mjs +98 -16
- package/lib/backup-restore-diagnostic.mjs +185 -0
- package/lib/cli-command-runner.mjs +65 -3
- package/lib/cli-project-runtime.mjs +1 -1
- package/lib/confirm-gate.mjs +101 -0
- package/lib/db-command.mjs +3 -0
- package/lib/db-migration-command-steps.mjs +44 -11
- package/lib/db-push-command.mjs +61 -21
- package/lib/deploy-codegen-failure.mjs +80 -0
- package/lib/deploy-codegen-preflight.mjs +3 -4
- package/lib/deploy-command.mjs +10 -4
- package/lib/deploy-failure-diagnostic.mjs +36 -14
- package/lib/deploy-first-prod-confirm.mjs +56 -0
- package/lib/deploy-logs-diagnostic.mjs +67 -0
- package/lib/deploy-runtime.mjs +30 -29
- package/lib/deployment-admission-retry.mjs +25 -1
- package/lib/migration-diagnostic.mjs +92 -1
- package/lib/migration-operation-client.mjs +15 -10
- package/lib/runtime-mode.mjs +1 -1
- package/lib/static-artifact-manifest.mjs +8 -2
- package/lib/static-command.mjs +2 -1
- package/lib/static-deploy-command.mjs +1 -0
- package/package.json +5 -3
- package/runtime/config.mjs +1 -1
- package/runtime/dependency-version-report.mjs +1 -1
- package/runtime/server.mjs +424 -424
- package/runtime/tooling.mjs +91 -127
- package/scripts/bundle-server.mjs +3 -3
- package/scripts/{bundle-internal.mjs → bundle-shared.mjs} +1 -1
- package/scripts/pre-publish-check.mjs +2 -2
- package/scripts/tooling-codegen-runtime.ts +13 -3
- package/scripts/tooling-entry.ts +6 -6
package/core/index.js
CHANGED
|
@@ -4,6 +4,78 @@ function defineApi(api) {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
// ../core/src/http-route.ts
|
|
7
|
+
var discardedResponseSymbol = /* @__PURE__ */ Symbol.for("@gencow/core/http-route/discarded-response/v1");
|
|
8
|
+
var discardedResponseCauseSymbol = /* @__PURE__ */ Symbol.for("@gencow/core/http-route/discarded-response-cause/v1");
|
|
9
|
+
var outputValidationFailureSymbol = /* @__PURE__ */ Symbol.for("@gencow/core/http-route/output-validation-failure/v1");
|
|
10
|
+
function getHttpRouteDiscardedResponse(value) {
|
|
11
|
+
if (!value || typeof value !== "object" && typeof value !== "function") return void 0;
|
|
12
|
+
try {
|
|
13
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, discardedResponseSymbol);
|
|
14
|
+
const response = descriptor && "value" in descriptor ? descriptor.value : void 0;
|
|
15
|
+
return response instanceof Response ? response : void 0;
|
|
16
|
+
} catch {
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function getHttpRouteDiscardedResponseCause(value) {
|
|
21
|
+
if (!value || typeof value !== "object" && typeof value !== "function") return void 0;
|
|
22
|
+
try {
|
|
23
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, discardedResponseCauseSymbol);
|
|
24
|
+
return descriptor && "value" in descriptor ? descriptor.value : void 0;
|
|
25
|
+
} catch {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function isHttpRouteOutputValidationFailure(value) {
|
|
30
|
+
if (!value || typeof value !== "object" && typeof value !== "function") return false;
|
|
31
|
+
try {
|
|
32
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, outputValidationFailureSymbol);
|
|
33
|
+
return !!descriptor && "value" in descriptor && descriptor.value === true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function addDiscardedResponse(value, response) {
|
|
39
|
+
try {
|
|
40
|
+
Object.defineProperty(value, discardedResponseSymbol, {
|
|
41
|
+
configurable: false,
|
|
42
|
+
enumerable: false,
|
|
43
|
+
value: response,
|
|
44
|
+
writable: false
|
|
45
|
+
});
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function routeResultResponse(value) {
|
|
52
|
+
if (value instanceof Response) return value;
|
|
53
|
+
return value?.body instanceof Response ? value.body : void 0;
|
|
54
|
+
}
|
|
55
|
+
function markDroppedResponse(value, response) {
|
|
56
|
+
if (!response || routeResultResponse(value) === response) return value;
|
|
57
|
+
if (value && (typeof value === "object" || typeof value === "function")) {
|
|
58
|
+
addDiscardedResponse(value, response);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function markDiscardedResponseError(error, response) {
|
|
63
|
+
if (error && (typeof error === "object" || typeof error === "function")) {
|
|
64
|
+
if (addDiscardedResponse(error, response)) return error;
|
|
65
|
+
}
|
|
66
|
+
const wrapped = error instanceof HttpRouteOutputValidationError ? new HttpRouteOutputValidationError(error.message) : new Error("httpRoute output validation failed", { cause: error });
|
|
67
|
+
addDiscardedResponse(wrapped, response);
|
|
68
|
+
try {
|
|
69
|
+
Object.defineProperty(wrapped, discardedResponseCauseSymbol, {
|
|
70
|
+
configurable: false,
|
|
71
|
+
enumerable: false,
|
|
72
|
+
value: error,
|
|
73
|
+
writable: false
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
return wrapped;
|
|
78
|
+
}
|
|
7
79
|
function hasStandardSchema(schema) {
|
|
8
80
|
return !!schema && typeof schema === "object" && "~standard" in schema && !!schema["~standard"] && typeof schema["~standard"].validate === "function";
|
|
9
81
|
}
|
|
@@ -65,10 +137,28 @@ async function buildInputFromRequest(method, req) {
|
|
|
65
137
|
}
|
|
66
138
|
async function maybeValidateRouteOutput(result, outputSchema) {
|
|
67
139
|
if (!outputSchema) return result;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
140
|
+
const source = routeResultResponse(result);
|
|
141
|
+
try {
|
|
142
|
+
const isSuccessStatus = result instanceof Response ? result.status < 400 : result?.status === void 0 || result.status < 400;
|
|
143
|
+
if (!isSuccessStatus) return result;
|
|
144
|
+
const response = result instanceof Response ? result : void 0;
|
|
145
|
+
const validatedBody = await validateOutput(
|
|
146
|
+
outputSchema,
|
|
147
|
+
response ? response.body : result?.body
|
|
148
|
+
);
|
|
149
|
+
if (response) {
|
|
150
|
+
if (validatedBody === response.body) return result;
|
|
151
|
+
return markDroppedResponse(
|
|
152
|
+
{ status: response.status, headers: response.headers, body: validatedBody },
|
|
153
|
+
source
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const normalized = { status: result?.status, headers: result?.headers, body: validatedBody };
|
|
157
|
+
return markDroppedResponse(normalized, source);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (source) throw markDiscardedResponseError(error, source);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
72
162
|
}
|
|
73
163
|
function composeRouteMiddlewares(middlewares, handler, inputSchema, outputSchema, inputValidationIndex, outputValidationIndex) {
|
|
74
164
|
return async (initialContext, initialInput, request) => {
|
|
@@ -210,15 +300,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
210
300
|
const inputSchema = this.inputSchema;
|
|
211
301
|
const outputSchema = this.outputSchema;
|
|
212
302
|
const inputErrorMapper = this.inputErrorMapper;
|
|
213
|
-
const
|
|
214
|
-
this.middlewares,
|
|
215
|
-
handler,
|
|
216
|
-
inputSchema,
|
|
217
|
-
outputSchema,
|
|
218
|
-
this.inputValidationIndex,
|
|
219
|
-
this.outputValidationIndex
|
|
220
|
-
);
|
|
221
|
-
const wrappedHandler = async (ctx, req) => {
|
|
303
|
+
const invokeRoute = async (ctx, req) => {
|
|
222
304
|
let normalizedInput;
|
|
223
305
|
try {
|
|
224
306
|
normalizedInput = inputSchema ? await buildInputFromRequest(method, req) : void 0;
|
|
@@ -231,14 +313,37 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
231
313
|
}
|
|
232
314
|
let result;
|
|
233
315
|
try {
|
|
234
|
-
result = await
|
|
316
|
+
result = await composeRouteMiddlewares(
|
|
317
|
+
this.middlewares,
|
|
318
|
+
handler,
|
|
319
|
+
inputSchema,
|
|
320
|
+
outputSchema,
|
|
321
|
+
this.inputValidationIndex,
|
|
322
|
+
this.outputValidationIndex
|
|
323
|
+
)(ctx, normalizedInput, req);
|
|
235
324
|
} catch (err) {
|
|
236
325
|
if (err instanceof HttpRouteInputValidationError) {
|
|
237
326
|
if (inputErrorMapper) return await inputErrorMapper(err.issues);
|
|
238
327
|
return { status: 400, body: { error: err.message } };
|
|
239
328
|
}
|
|
240
329
|
if (err instanceof HttpRouteOutputValidationError) {
|
|
241
|
-
|
|
330
|
+
const discardedResponse = getHttpRouteDiscardedResponse(err);
|
|
331
|
+
const failure = {
|
|
332
|
+
status: 500,
|
|
333
|
+
body: {
|
|
334
|
+
error: discardedResponse ? "Output validation failed" : `Output validation failed: ${err.message}`
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
if (discardedResponse) {
|
|
338
|
+
addDiscardedResponse(failure, discardedResponse);
|
|
339
|
+
Object.defineProperty(failure, outputValidationFailureSymbol, {
|
|
340
|
+
configurable: false,
|
|
341
|
+
enumerable: false,
|
|
342
|
+
value: true,
|
|
343
|
+
writable: false
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return failure;
|
|
242
347
|
}
|
|
243
348
|
throw err;
|
|
244
349
|
}
|
|
@@ -248,7 +353,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
248
353
|
method,
|
|
249
354
|
path: routePath,
|
|
250
355
|
isPublic,
|
|
251
|
-
handler:
|
|
356
|
+
handler: invokeRoute,
|
|
252
357
|
middlewares: [...this.middlewares],
|
|
253
358
|
inputValidationIndex: this.inputValidationIndex,
|
|
254
359
|
outputValidationIndex: this.outputValidationIndex,
|
|
@@ -341,90 +446,6 @@ function isWakeAppDeferredResult(result) {
|
|
|
341
446
|
return !result.ok && (result.status === "capacity_rejected" || result.status === "queue_timeout" || result.status === "convergence_pending");
|
|
342
447
|
}
|
|
343
448
|
|
|
344
|
-
// ../core/src/platform-capacity-profile.ts
|
|
345
|
-
var PLATFORM_CAPACITY_PROFILE_ENV = "GENCOW_CAPACITY_PROFILE";
|
|
346
|
-
var PLATFORM_CAPACITY_ENV_KEYS = [
|
|
347
|
-
PLATFORM_CAPACITY_PROFILE_ENV,
|
|
348
|
-
"MAX_CONCURRENT_RUNNING",
|
|
349
|
-
"MAX_CONCURRENT_WAKE",
|
|
350
|
-
"MAX_WAKE_QUEUE_MS",
|
|
351
|
-
"MIN_AVAILABLE_RAM_MB",
|
|
352
|
-
"WAKE_RETRY_AFTER_SEC",
|
|
353
|
-
"EVICTION_THRESHOLD_MB",
|
|
354
|
-
"GENCOW_SLEEP_TIMEOUT_MINUTES",
|
|
355
|
-
"GENCOW_SLEEP_MAX_PER_CYCLE",
|
|
356
|
-
"WARM_POOL_MIN_IDLE",
|
|
357
|
-
"WARM_POOL_MAX",
|
|
358
|
-
"DEPLOY_CANDIDATE_RESERVE",
|
|
359
|
-
"GENCOW_APP_PORT_RANGE",
|
|
360
|
-
"COWBOX_WARM_POOL_PORT_RANGE"
|
|
361
|
-
];
|
|
362
|
-
var PLATFORM_CAPACITY_PRESETS = Object.freeze({
|
|
363
|
-
prod: Object.freeze({
|
|
364
|
-
profile: "prod",
|
|
365
|
-
maxConcurrentRunning: 600,
|
|
366
|
-
maxConcurrentWake: 20,
|
|
367
|
-
maxWakeQueueMs: 5e3,
|
|
368
|
-
minAvailableRamMB: 8192,
|
|
369
|
-
evictionThresholdMB: 16384,
|
|
370
|
-
sleepTimeoutMinutes: 15,
|
|
371
|
-
sleepMaxPerCycle: 50,
|
|
372
|
-
warmPoolMinIdle: 10,
|
|
373
|
-
warmPoolMax: 650,
|
|
374
|
-
deployCandidateReserve: 40
|
|
375
|
-
}),
|
|
376
|
-
dev: Object.freeze({
|
|
377
|
-
profile: "dev",
|
|
378
|
-
maxConcurrentRunning: 60,
|
|
379
|
-
maxConcurrentWake: 5,
|
|
380
|
-
maxWakeQueueMs: 3e3,
|
|
381
|
-
minAvailableRamMB: 2048,
|
|
382
|
-
evictionThresholdMB: 4096,
|
|
383
|
-
sleepTimeoutMinutes: 10,
|
|
384
|
-
sleepMaxPerCycle: 20,
|
|
385
|
-
warmPoolMinIdle: 3,
|
|
386
|
-
warmPoolMax: 70,
|
|
387
|
-
deployCandidateReserve: 5
|
|
388
|
-
}),
|
|
389
|
-
local: Object.freeze({
|
|
390
|
-
profile: "local",
|
|
391
|
-
maxConcurrentRunning: null,
|
|
392
|
-
maxConcurrentWake: null,
|
|
393
|
-
maxWakeQueueMs: 5e3,
|
|
394
|
-
minAvailableRamMB: null,
|
|
395
|
-
evictionThresholdMB: null,
|
|
396
|
-
sleepTimeoutMinutes: 30,
|
|
397
|
-
sleepMaxPerCycle: 10,
|
|
398
|
-
warmPoolMinIdle: 3,
|
|
399
|
-
warmPoolMax: 10,
|
|
400
|
-
deployCandidateReserve: 0
|
|
401
|
-
})
|
|
402
|
-
});
|
|
403
|
-
function normalizeDomain(value) {
|
|
404
|
-
return (value || "").trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
405
|
-
}
|
|
406
|
-
function detectPlatformCapacityProfile(env = process.env) {
|
|
407
|
-
const explicit = env[PLATFORM_CAPACITY_PROFILE_ENV]?.trim().toLowerCase();
|
|
408
|
-
if (explicit) {
|
|
409
|
-
if (["prod", "production", "128gb"].includes(explicit)) return "prod";
|
|
410
|
-
if (["dev", "development", "16gb"].includes(explicit)) return "dev";
|
|
411
|
-
if (["local", "test", "disabled"].includes(explicit)) return "local";
|
|
412
|
-
throw new Error(`Invalid ${PLATFORM_CAPACITY_PROFILE_ENV}: ${explicit}. Expected prod, dev, or local.`);
|
|
413
|
-
}
|
|
414
|
-
const baseDomain = normalizeDomain(env.BASE_DOMAIN);
|
|
415
|
-
if (baseDomain === "gencow.dev") return "dev";
|
|
416
|
-
if (baseDomain === "gencow.app") return "prod";
|
|
417
|
-
const platformUrl = normalizeDomain(env.GENCOW_PLATFORM_URL);
|
|
418
|
-
if (platformUrl === "gencow.dev") return "dev";
|
|
419
|
-
if (platformUrl === "gencow.app") return "prod";
|
|
420
|
-
if (env.NODE_ENV === "production" && env.IS_PLATFORM === "true") return "prod";
|
|
421
|
-
return "local";
|
|
422
|
-
}
|
|
423
|
-
function resolvePlatformCapacityPreset(env = process.env) {
|
|
424
|
-
const profile = detectPlatformCapacityProfile(env);
|
|
425
|
-
return { ...PLATFORM_CAPACITY_PRESETS[profile] };
|
|
426
|
-
}
|
|
427
|
-
|
|
428
449
|
// ../core/src/grounded-answer-types.ts
|
|
429
450
|
var DEFAULT_GROUNDING_BUDGET = {
|
|
430
451
|
maxVerifyLoops: 2,
|
|
@@ -1668,7 +1689,9 @@ async function validateWithSchema(schema, value, options) {
|
|
|
1668
1689
|
}
|
|
1669
1690
|
throw new GencowValidationError("Validation failed");
|
|
1670
1691
|
}
|
|
1671
|
-
throw new Error(
|
|
1692
|
+
throw new Error(
|
|
1693
|
+
"procedure.query / procedure.mutation / procedure.internal only supports Standard Schema v1 validators"
|
|
1694
|
+
);
|
|
1672
1695
|
}
|
|
1673
1696
|
function composeMiddlewares(middlewares, handler, inputSchema, outputSchema, inputValidationIndex, outputValidationIndex) {
|
|
1674
1697
|
return async (initialContext, initialInput) => {
|
|
@@ -1798,14 +1821,14 @@ var GencowProcedureBuilderImpl = class _GencowProcedureBuilderImpl {
|
|
|
1798
1821
|
if (!this.procedureName) {
|
|
1799
1822
|
throw new Error("Procedure name is required. Call .name(...) before .handler(...)");
|
|
1800
1823
|
}
|
|
1801
|
-
const run = composeMiddlewares(
|
|
1824
|
+
const run = (context, input) => composeMiddlewares(
|
|
1802
1825
|
this.middlewares,
|
|
1803
1826
|
handler,
|
|
1804
1827
|
this.inputSchema,
|
|
1805
1828
|
this.outputSchema,
|
|
1806
1829
|
this.inputValidationIndex,
|
|
1807
1830
|
this.outputValidationIndex
|
|
1808
|
-
);
|
|
1831
|
+
)(context, input);
|
|
1809
1832
|
return {
|
|
1810
1833
|
kind: this.kind,
|
|
1811
1834
|
name: this.procedureName,
|
|
@@ -6082,9 +6105,6 @@ export {
|
|
|
6082
6105
|
GencowValidationError,
|
|
6083
6106
|
JobAdmissionError,
|
|
6084
6107
|
MAX_FILE_SIZE,
|
|
6085
|
-
PLATFORM_CAPACITY_ENV_KEYS,
|
|
6086
|
-
PLATFORM_CAPACITY_PRESETS,
|
|
6087
|
-
PLATFORM_CAPACITY_PROFILE_ENV,
|
|
6088
6108
|
RLS_SESSION_KEY_RE,
|
|
6089
6109
|
RealtimeChannelEventValidationError,
|
|
6090
6110
|
STORAGE_LIST_DEFAULT_LIMIT,
|
|
@@ -6144,10 +6164,11 @@ export {
|
|
|
6144
6164
|
defineRealtimeSubscriptions,
|
|
6145
6165
|
deriveWorkflowStatus,
|
|
6146
6166
|
deserializeWorkflowValue,
|
|
6147
|
-
detectPlatformCapacityProfile,
|
|
6148
6167
|
eqRlsContextColumn,
|
|
6149
6168
|
filterTenantRuntimeEnvVars,
|
|
6150
6169
|
formatBytes,
|
|
6170
|
+
getHttpRouteDiscardedResponse,
|
|
6171
|
+
getHttpRouteDiscardedResponseCause,
|
|
6151
6172
|
getJobAdmissionStatus,
|
|
6152
6173
|
getMinuteBucketStart,
|
|
6153
6174
|
getMsUntilNextMinute,
|
|
@@ -6167,6 +6188,7 @@ export {
|
|
|
6167
6188
|
hasMapSessionToContext,
|
|
6168
6189
|
inferTableNameFromColumn,
|
|
6169
6190
|
isCronJobsBuilder,
|
|
6191
|
+
isHttpRouteOutputValidationFailure,
|
|
6170
6192
|
isJobAdmissionError,
|
|
6171
6193
|
isJobAdmissionKillSwitchActive,
|
|
6172
6194
|
isReservedTenantRuntimeEnvKey,
|
|
@@ -6195,7 +6217,6 @@ export {
|
|
|
6195
6217
|
resetRealtimeChannelRegistryForTests,
|
|
6196
6218
|
resetRealtimeSubscriptionRegistryForTests,
|
|
6197
6219
|
resolveExplicitRealtimeChannelKey,
|
|
6198
|
-
resolvePlatformCapacityPreset,
|
|
6199
6220
|
resolveRealtimeChannelKey,
|
|
6200
6221
|
resolveRealtimeQueryScopeHash,
|
|
6201
6222
|
resolveRealtimeQuerySubscriptionKey,
|
package/docs/packaging.md
CHANGED
|
@@ -40,11 +40,15 @@ CLI workspace source tree.
|
|
|
40
40
|
Build from a complete repository checkout; workspace dependencies are required:
|
|
41
41
|
|
|
42
42
|
```sh
|
|
43
|
-
pnpm --dir packages/cli run build
|
|
43
|
+
pnpm --dir packages/cli run build:with-deps
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
`build:with-deps` builds the CLI's workspace dependencies (`@gencow/server`,
|
|
47
|
+
`@gencow/internal`, `@gencow/core`, and the rest of the CLI closure) once each in
|
|
48
|
+
topological order, then produces the package runtime artifacts. It selects packages
|
|
49
|
+
with the path selector `{.}...` rather than the name `gencow...`, because the root
|
|
50
|
+
package is also named `gencow` and a name selector would build the whole workspace. The plain `build`
|
|
51
|
+
script does not build dependencies; it expects their `dist/` output to exist. After the build,
|
|
48
52
|
from `packages/cli`, create a local tarball without running package lifecycle
|
|
49
53
|
hooks, inspect its file list, and run the package integrity check:
|
|
50
54
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -40,7 +40,7 @@ other work before handling arguments.
|
|
|
40
40
|
selected client target. Use the command reference for documented output options;
|
|
41
41
|
do not use subcommand `--help` as a syntax probe.
|
|
42
42
|
- If the CLI package build reports a missing or incompatible artifact, rebuild
|
|
43
|
-
from the repository root with `pnpm --dir packages/cli run build`, then rerun
|
|
43
|
+
from the repository root with `pnpm --dir packages/cli run build:with-deps`, then rerun
|
|
44
44
|
`node scripts/pre-publish-check.mjs` from `packages/cli`.
|
|
45
45
|
- For dependency resolution problems, run `pnpm install` at the workspace root
|
|
46
46
|
and use the package manager declared by the project lockfile.
|
package/lib/app-command.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
-
import { resolve } from "path";
|
|
3
|
+
import { basename, resolve } from "path";
|
|
4
4
|
import {
|
|
5
5
|
fetchAppDiagnosticLogsViaRpc,
|
|
6
6
|
formatAppDiagnosticLogBlock,
|
|
@@ -22,6 +22,7 @@ import { resolveProjectMetadataPath, resolveProjectSelection } from "./project-c
|
|
|
22
22
|
import { updateEnvLocalUrl } from "./cli-project-runtime.mjs";
|
|
23
23
|
|
|
24
24
|
const DEFAULT_APP_CREATE_TIMEOUT_MS = 60_000;
|
|
25
|
+
const SAFE_CLI_OPTION_PATTERN = /^--?[A-Za-z][A-Za-z0-9-]{0,40}$/u;
|
|
25
26
|
|
|
26
27
|
function resolveAppCreateTimeoutMs(value) {
|
|
27
28
|
const parsed = Number(value);
|
|
@@ -38,6 +39,11 @@ function invalidAppResponse(operation, response, kind) {
|
|
|
38
39
|
return `CLI_APP_RESPONSE_INVALID: ${operation} returned an invalid ${kind} response (${appResponseStatus(response)})`;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
function isTerminalAppDeleteResponse(body) {
|
|
43
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
44
|
+
return body.retryable === false || body.retryClass === "terminal-client";
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
export function formatAppDeployedAgo(dateStr, now = Date.now()) {
|
|
42
48
|
if (!dateStr) return `${DIM}never${RESET}`;
|
|
43
49
|
const diff = now - new Date(dateStr).getTime();
|
|
@@ -88,6 +94,32 @@ function formatPendingReasonLines(appRow) {
|
|
|
88
94
|
};
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
// A legacy-owner wake failure keeps the incumbent serving state and carries platform-authored guidance
|
|
98
|
+
// (retry condition, exact command). Render it instead of the generic "no deployment" text, which is wrong
|
|
99
|
+
// for an app that already has a legacy deployment.
|
|
100
|
+
function formatLegacyRedeployDetails(appRow) {
|
|
101
|
+
const pending = formatPendingReasonLines(appRow);
|
|
102
|
+
if (!pending || appRow?.runtimeRecoveryReason !== "legacy_runtime_preparation_required") return null;
|
|
103
|
+
const correlationId = appRow?.runtimeRecoveryCorrelationId;
|
|
104
|
+
return [
|
|
105
|
+
" Recovery: the app runtime is a legacy build the Platform cannot re-verify; the incumbent serving state was preserved.",
|
|
106
|
+
" Data/Deployment: unchanged; no data or serving state was changed.",
|
|
107
|
+
` Code: ${appRow?.runtimeRecoveryErrorCode || "RUNTIME_CONVERGENCE_LEGACY_REDEPLOY_REQUIRED"}`,
|
|
108
|
+
pending.reason,
|
|
109
|
+
...(correlationId ? [` Correlation: ${correlationId}`] : []),
|
|
110
|
+
pending.retryable,
|
|
111
|
+
pending.next,
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Platform marks an idle legacy-owner app (no wake failure recorded yet) with disposition.legacyRuntimeOwner.
|
|
116
|
+
const LEGACY_OWNER_REDEPLOY_DETAILS = [
|
|
117
|
+
" Recovery: the app runtime is a legacy build the Platform cannot re-verify; the incumbent serving state was preserved.",
|
|
118
|
+
" Data/Deployment: unchanged; no data or serving state was changed.",
|
|
119
|
+
" Retryable: no; deploy a new revision to move the app to the supported runtime.",
|
|
120
|
+
" Next command: gencow deploy --prod",
|
|
121
|
+
].join("\n");
|
|
122
|
+
|
|
91
123
|
export function formatRecoveryDispositionDetails(appRow) {
|
|
92
124
|
// The accepted delete receipt remains the lifecycle authority while the app
|
|
93
125
|
// is being removed. Do not replace it with unrelated recovery or support
|
|
@@ -95,7 +127,11 @@ export function formatRecoveryDispositionDetails(appRow) {
|
|
|
95
127
|
if (appRow?.status === "deleting") return "";
|
|
96
128
|
const disposition = appRow?.recoveryDisposition;
|
|
97
129
|
if (disposition?.kind === "deploy_required") {
|
|
98
|
-
return
|
|
130
|
+
return (
|
|
131
|
+
formatLegacyRedeployDetails(appRow) ??
|
|
132
|
+
(disposition.legacyRuntimeOwner === true ? LEGACY_OWNER_REDEPLOY_DETAILS : null) ??
|
|
133
|
+
" Recovery: no deployment is available. Run `gencow deploy` from this project.\n Retryable: no"
|
|
134
|
+
);
|
|
99
135
|
}
|
|
100
136
|
if (disposition?.kind === "owner_memory_action_required") {
|
|
101
137
|
return " Recovery: inspect logs and memory usage, then deploy a fix or explicitly Resume.\n Retryable: no automatic retry";
|
|
@@ -286,9 +322,17 @@ export function createAppCommand({
|
|
|
286
322
|
}
|
|
287
323
|
|
|
288
324
|
if (subcmd === "create") {
|
|
289
|
-
const
|
|
325
|
+
const unsupportedOption = rest.find((value) => value.startsWith("-"));
|
|
326
|
+
const name = rest.find((value) => !value.startsWith("-"));
|
|
327
|
+
if (unsupportedOption !== undefined) {
|
|
328
|
+
const optionLabel = SAFE_CLI_OPTION_PATTERN.test(unsupportedOption) ? `${unsupportedOption} ` : "";
|
|
329
|
+
errorImpl(
|
|
330
|
+
`Unknown option for app create: ${optionLabel}is not supported. The app name is the only argument.\nCode: APP_CREATE_USAGE_INVALID\nUsage: gencow app create <name>`,
|
|
331
|
+
);
|
|
332
|
+
return 1;
|
|
333
|
+
}
|
|
290
334
|
if (!name) {
|
|
291
|
-
errorImpl("Usage: gencow app create <name
|
|
335
|
+
errorImpl("Usage: gencow app create <name>\nCode: APP_CREATE_USAGE_INVALID");
|
|
292
336
|
return 1;
|
|
293
337
|
}
|
|
294
338
|
|
|
@@ -354,13 +398,15 @@ export function createAppCommand({
|
|
|
354
398
|
successImpl(`${config.rootDir}/index.ts — created (starter)`);
|
|
355
399
|
}
|
|
356
400
|
|
|
357
|
-
|
|
358
|
-
|
|
401
|
+
// The same resolved config file is used for the write and for the message, so the
|
|
402
|
+
// reported name always matches the file that was actually changed.
|
|
403
|
+
const configPath = projectSelection.configPath ?? null;
|
|
404
|
+
if (configPath && existsSync(configPath)) {
|
|
359
405
|
const src = readFileSync(configPath, "utf8");
|
|
360
406
|
const nextSrc = addDeployAppToConfigSource(src, appId);
|
|
361
407
|
if (nextSrc !== src) {
|
|
362
408
|
writeFileSync(configPath, nextSrc);
|
|
363
|
-
successImpl(
|
|
409
|
+
successImpl(`${basename(configPath)} — added deploy.app = "${appId}"`);
|
|
364
410
|
}
|
|
365
411
|
}
|
|
366
412
|
|
|
@@ -370,8 +416,11 @@ export function createAppCommand({
|
|
|
370
416
|
${GREEN}▸${RESET} App ID: ${appId}
|
|
371
417
|
${GREEN}▸${RESET} API: ${appUrl}
|
|
372
418
|
${dashboardLine}
|
|
373
|
-
${GREEN}▸${RESET} .env: VITE_API_URL set
|
|
374
|
-
|
|
419
|
+
${GREEN}▸${RESET} .env: VITE_API_URL set (dev app)
|
|
420
|
+
${GREEN}▸${RESET} Prod app: not created yet. Your first ${GREEN}gencow deploy --prod${RESET} creates it.
|
|
421
|
+
`);
|
|
422
|
+
logImpl(`
|
|
423
|
+
${DIM}gencow deploy --prod — create the prod app and deploy to it${RESET}
|
|
375
424
|
${DIM}pnpm gencow dev — watch & auto-deploy to cloud + live logs${RESET}
|
|
376
425
|
`);
|
|
377
426
|
return 0;
|
|
@@ -418,6 +467,12 @@ ${dashboardLine}
|
|
|
418
467
|
delRes,
|
|
419
468
|
{ expectedAppId: name },
|
|
420
469
|
);
|
|
470
|
+
// A typed terminal failure (for example APP_NOT_FOUND) is not retryable, whatever its HTTP
|
|
471
|
+
// status. Telling the user to replay the same request contradicts the server classification.
|
|
472
|
+
if (isTerminalAppDeleteResponse(delData)) {
|
|
473
|
+
errorImpl(`${diagnosis}\nNext command: gencow app list`);
|
|
474
|
+
return 1;
|
|
475
|
+
}
|
|
421
476
|
errorImpl(delRes.ok || delRes.status >= 500
|
|
422
477
|
? `${diagnosis}\nRequest ID: ${requestId}. Retry the same request: ${retryCommand}`
|
|
423
478
|
: diagnosis);
|
package/lib/backup-command.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { resolve } from "path";
|
|
|
3
3
|
import { BOLD, CYAN, DIM, GREEN, RED, RESET, YELLOW, error, info, log, success } from "./output.mjs";
|
|
4
4
|
import { platformFetch, requireCreds, rpcMutation, rpcQuery } from "./platform-client.mjs";
|
|
5
5
|
import { readProjectMetadata } from "./project-context.mjs";
|
|
6
|
+
import { formatRestoreFailure } from "./backup-restore-diagnostic.mjs";
|
|
6
7
|
|
|
7
8
|
export function resolveBackupAppId(restArgs, gencowJson = null) {
|
|
8
9
|
let appId = null;
|
|
@@ -85,6 +86,92 @@ export function parseBackupListOptions(restArgs) {
|
|
|
85
86
|
return options;
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
// Public codes this renderer knows. Legacy BACKUP_CAPABILITY_NOT_READY rows are handled separately.
|
|
90
|
+
const BACKUP_TYPED_CODES = new Set([
|
|
91
|
+
"BACKUP_ACCESS_RECONCILING",
|
|
92
|
+
"BACKUP_RLS_RESTRICTIVE_POLICY",
|
|
93
|
+
"BACKUP_RLS_SCOPE_UNAVAILABLE",
|
|
94
|
+
"BACKUP_CAPABILITY_ATTESTATION_INVALID",
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const BACKUP_PLATFORM_WAIT =
|
|
98
|
+
"No action is needed from you. Retry only after the backup access check reports ready; do not retry in a loop before then.";
|
|
99
|
+
|
|
100
|
+
// One entry per preflight reason the platform can store. A reason missing here is dropped from the rendered
|
|
101
|
+
// list, so it cannot reach the terminal under an unexplained code. Stored prose is never printed.
|
|
102
|
+
const BACKUP_REASON_PRESENTATION = {
|
|
103
|
+
attestation_metadata_invalid: { owner: "platform", nextAction: `Platform must correct the backup reader attestation. ${BACKUP_PLATFORM_WAIT}` },
|
|
104
|
+
role_attestation_missing: { owner: "platform", nextAction: `Platform must correct the backup reader attestation. ${BACKUP_PLATFORM_WAIT}` },
|
|
105
|
+
backup_identity_mismatch: { owner: "platform", nextAction: `Platform must correct the backup reader attestation. ${BACKUP_PLATFORM_WAIT}` },
|
|
106
|
+
role_has_broad_cluster_privilege: { owner: "platform", nextAction: `Platform must correct the backup reader attestation. ${BACKUP_PLATFORM_WAIT}` },
|
|
107
|
+
rls_bypass_not_approved: { owner: "platform", nextAction: `Platform must correct the backup reader attestation. ${BACKUP_PLATFORM_WAIT}` },
|
|
108
|
+
relation_select_missing: { owner: "platform", nextAction: `Platform is restoring backup access. ${BACKUP_PLATFORM_WAIT}` },
|
|
109
|
+
relation_ownership_forbidden: { owner: "platform", nextAction: `Platform is restoring backup access. ${BACKUP_PLATFORM_WAIT}` },
|
|
110
|
+
sequence_defaults_mutation_forbidden: { owner: "platform", nextAction: `Platform is restoring backup access. ${BACKUP_PLATFORM_WAIT}` },
|
|
111
|
+
complete_rls_dump_not_proven: { owner: "platform", nextAction: `Platform is restoring backup access. ${BACKUP_PLATFORM_WAIT}` },
|
|
112
|
+
rls_restrictive_policy_applies: {
|
|
113
|
+
owner: "app",
|
|
114
|
+
nextAction:
|
|
115
|
+
"Add a new migration that limits the restrictive RLS policy so it no longer applies to the Gencow backup reader (do not edit applied migrations), run `gencow deploy` from your app workspace, then retry the backup.",
|
|
116
|
+
},
|
|
117
|
+
rls_dump_scope_unbound: { owner: "platform", nextAction: `Platform must bind the backup scope for this app. ${BACKUP_PLATFORM_WAIT}` },
|
|
118
|
+
rls_scope_changed: {
|
|
119
|
+
owner: "platform",
|
|
120
|
+
nextAction:
|
|
121
|
+
"This is a transient race. A single retry after the current backup attempt finishes is expected to succeed. If it fails again, Platform is restoring access; do not retry in a loop, and cite the Reference below.",
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Stored shape written by the platform: "CODE[ [reasons=a,b]]: prose". Only the code and reason tokens are read.
|
|
126
|
+
const BACKUP_DIAGNOSIS_PATTERN = /^(BACKUP_[A-Z_]+)(?: \[reasons=([a-z_,]+)\])?: /u;
|
|
127
|
+
|
|
128
|
+
function parseBackupDiagnosis(backup) {
|
|
129
|
+
if (backup.status !== "failed" || typeof backup.errorMessage !== "string") return null;
|
|
130
|
+
const match = BACKUP_DIAGNOSIS_PATTERN.exec(backup.errorMessage);
|
|
131
|
+
if (!match) return null;
|
|
132
|
+
const code = match[1];
|
|
133
|
+
// Legacy rows stored before the typed codes existed keep their original fixed rendering.
|
|
134
|
+
if (code === "BACKUP_CAPABILITY_NOT_READY") return { code, legacy: true };
|
|
135
|
+
if (!BACKUP_TYPED_CODES.has(code)) return null;
|
|
136
|
+
const reasons = [
|
|
137
|
+
...new Set(
|
|
138
|
+
(match[2] ?? "")
|
|
139
|
+
.split(",")
|
|
140
|
+
.filter((token) => Object.prototype.hasOwnProperty.call(BACKUP_REASON_PRESENTATION, token)),
|
|
141
|
+
),
|
|
142
|
+
];
|
|
143
|
+
return reasons.length > 0 ? { code, reasons } : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderBackupDiagnosis(diagnosis, backupId) {
|
|
147
|
+
if (diagnosis.legacy) {
|
|
148
|
+
return [
|
|
149
|
+
" BACKUP_CAPABILITY_NOT_READY: Backup preflight is not ready. The dump did not start; no data or serving state changed. Retry only after Platform confirms complete backup visibility and protected snapshot and authorization readiness for this app. Check the failed backup with `gencow backup list` from its app workspace.",
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
const presentations = diagnosis.reasons.map((reason) => ({ reason, ...BACKUP_REASON_PRESENTATION[reason] }));
|
|
153
|
+
const appSteps = [...new Set(presentations.filter((p) => p.owner === "app").map((p) => p.nextAction))];
|
|
154
|
+
const platformReasons = presentations.filter((p) => p.owner === "platform").map((p) => p.reason);
|
|
155
|
+
const platformSteps = [...new Set(presentations.filter((p) => p.owner === "platform").map((p) => p.nextAction))];
|
|
156
|
+
const owners = [appSteps.length > 0 ? "App" : null, platformSteps.length > 0 ? "Platform" : null].filter(Boolean);
|
|
157
|
+
const lines = [
|
|
158
|
+
` ${diagnosis.code}`,
|
|
159
|
+
` Stage: preflight | State: unchanged (no data or serving state changed) | Owner: ${owners.join(" and ")}`,
|
|
160
|
+
` Reasons: ${diagnosis.reasons.join(", ")}`,
|
|
161
|
+
];
|
|
162
|
+
if (appSteps.length > 0 && platformSteps.length > 0) {
|
|
163
|
+
// Mixed refusal: the user acts on the app part only, and the Platform part is named as already in progress.
|
|
164
|
+
lines.push(` Next (you): ${appSteps.join(" ")}`);
|
|
165
|
+
lines.push(
|
|
166
|
+
` Platform part: Platform is restoring the Platform-owned reasons (${platformReasons.join(", ")}); no action is needed from you for them. ${platformSteps.join(" ")}`,
|
|
167
|
+
);
|
|
168
|
+
} else {
|
|
169
|
+
lines.push(` Next: ${[...appSteps, ...platformSteps].join(" ")}`);
|
|
170
|
+
}
|
|
171
|
+
lines.push(` Reference: backup #${backupId}`);
|
|
172
|
+
return lines;
|
|
173
|
+
}
|
|
174
|
+
|
|
88
175
|
function renderBackupHelp(logImpl = log, errorImpl = error, subCmd = null) {
|
|
89
176
|
if (subCmd) errorImpl(`Unknown sub-command: ${subCmd}`);
|
|
90
177
|
logImpl(`\n ${BOLD}Usage:${RESET}`);
|
|
@@ -98,6 +185,10 @@ function renderBackupHelp(logImpl = log, errorImpl = error, subCmd = null) {
|
|
|
98
185
|
logImpl(` gencow backup download <id> Download backup file (plan-dependent)\n`);
|
|
99
186
|
}
|
|
100
187
|
|
|
188
|
+
async function readErrorBody(response) {
|
|
189
|
+
return (await response.json?.().catch(() => ({}))) ?? {};
|
|
190
|
+
}
|
|
191
|
+
|
|
101
192
|
export function buildRestoreConfirmPrompt(sourceLabel, { includeStorageRisk = false } = {}) {
|
|
102
193
|
const storageRisk = includeStorageRisk
|
|
103
194
|
? `\n ${YELLOW}⚠ 백업 파일 복원은 DB만 교체하며 storage object/blob는 함께 이동하지 않습니다.${RESET}` +
|
|
@@ -201,16 +292,11 @@ export function createBackupCommand({
|
|
|
201
292
|
logImpl(
|
|
202
293
|
` ${DIM}#${backup.id}${RESET} ${statusColor}${backup.status.padEnd(10)}${RESET} ${backup.type.padEnd(11)} ${formatBackupRetentionClass(backup).padEnd(14)} ${formatBackupSize(backup.fileSize).padStart(10)} ${formatBackupDate(backup.createdAt)} ${DIM}expires ${formatBackupExpiry(backup.expiresAt)}${RESET}${backup.note ? ` ${DIM}${backup.note}${RESET}` : ""}`,
|
|
203
294
|
);
|
|
204
|
-
// Recognize the stable producer code, but never print
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
backup.errorMessage.startsWith("BACKUP_CAPABILITY_NOT_READY:")
|
|
210
|
-
) {
|
|
211
|
-
logImpl(
|
|
212
|
-
" BACKUP_CAPABILITY_NOT_READY: Backup preflight is not ready. The dump did not start; no data or serving state changed. Retry only after Platform confirms complete backup visibility and protected snapshot and authorization readiness for this app. Check the failed backup with `gencow backup list` from its app workspace.",
|
|
213
|
-
);
|
|
295
|
+
// Recognize the stable producer code, but never print raw errorMessage contents
|
|
296
|
+
// (which may contain SQL or credentials).
|
|
297
|
+
const diagnosis = parseBackupDiagnosis(backup);
|
|
298
|
+
if (diagnosis) {
|
|
299
|
+
for (const line of renderBackupDiagnosis(diagnosis, backup.id)) logImpl(line);
|
|
214
300
|
}
|
|
215
301
|
}
|
|
216
302
|
}
|
|
@@ -267,9 +353,7 @@ export function createBackupCommand({
|
|
|
267
353
|
logImpl(`\n${BOLD}Restoring from backup #${backupId}...${RESET}\n`);
|
|
268
354
|
const response = await rpcMutationImpl(creds, "backup.restore", { appName: appId, backupId });
|
|
269
355
|
if (!response.ok) {
|
|
270
|
-
errorImpl(
|
|
271
|
-
`Restore failed: ${(await response.json().catch(() => ({}))).error || response.statusText}`,
|
|
272
|
-
);
|
|
356
|
+
errorImpl(formatRestoreFailure(await readErrorBody(response), response.statusText));
|
|
273
357
|
return;
|
|
274
358
|
}
|
|
275
359
|
successImpl("Database restored. App restarted.");
|
|
@@ -306,9 +390,7 @@ export function createBackupCommand({
|
|
|
306
390
|
body: form,
|
|
307
391
|
});
|
|
308
392
|
if (!response.ok) {
|
|
309
|
-
errorImpl(
|
|
310
|
-
`Restore failed: ${(await response.json?.().catch(() => ({}))).error || response.statusText}`,
|
|
311
|
-
);
|
|
393
|
+
errorImpl(formatRestoreFailure(await readErrorBody(response), response.statusText));
|
|
312
394
|
return;
|
|
313
395
|
}
|
|
314
396
|
successImpl("Database restored from file. App restarted.");
|