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.
Files changed (36) hide show
  1. package/core/index.js +129 -108
  2. package/docs/packaging.md +7 -3
  3. package/docs/troubleshooting.md +1 -1
  4. package/lib/app-command.mjs +64 -9
  5. package/lib/backup-command.mjs +98 -16
  6. package/lib/backup-restore-diagnostic.mjs +185 -0
  7. package/lib/cli-command-runner.mjs +65 -3
  8. package/lib/cli-project-runtime.mjs +1 -1
  9. package/lib/confirm-gate.mjs +101 -0
  10. package/lib/db-command.mjs +3 -0
  11. package/lib/db-migration-command-steps.mjs +44 -11
  12. package/lib/db-push-command.mjs +61 -21
  13. package/lib/deploy-codegen-failure.mjs +80 -0
  14. package/lib/deploy-codegen-preflight.mjs +3 -4
  15. package/lib/deploy-command.mjs +10 -4
  16. package/lib/deploy-failure-diagnostic.mjs +36 -14
  17. package/lib/deploy-first-prod-confirm.mjs +56 -0
  18. package/lib/deploy-logs-diagnostic.mjs +67 -0
  19. package/lib/deploy-runtime.mjs +30 -29
  20. package/lib/deployment-admission-retry.mjs +25 -1
  21. package/lib/migration-diagnostic.mjs +92 -1
  22. package/lib/migration-operation-client.mjs +15 -10
  23. package/lib/runtime-mode.mjs +1 -1
  24. package/lib/static-artifact-manifest.mjs +8 -2
  25. package/lib/static-command.mjs +2 -1
  26. package/lib/static-deploy-command.mjs +1 -0
  27. package/package.json +5 -3
  28. package/runtime/config.mjs +1 -1
  29. package/runtime/dependency-version-report.mjs +1 -1
  30. package/runtime/server.mjs +424 -424
  31. package/runtime/tooling.mjs +91 -127
  32. package/scripts/bundle-server.mjs +3 -3
  33. package/scripts/{bundle-internal.mjs → bundle-shared.mjs} +1 -1
  34. package/scripts/pre-publish-check.mjs +2 -2
  35. package/scripts/tooling-codegen-runtime.ts +13 -3
  36. 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 isSuccessStatus = result?.status === void 0 || result.status < 400;
69
- if (!isSuccessStatus) return result;
70
- const validatedBody = await validateOutput(outputSchema, result?.body);
71
- return { status: result?.status, headers: result?.headers, body: validatedBody };
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 runRoute = composeRouteMiddlewares(
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 runRoute(ctx, normalizedInput, req);
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
- return { status: 500, body: { error: `Output validation failed: ${err.message}` } };
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: wrappedHandler,
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("procedure.query / procedure.mutation / procedure.internal only supports Standard Schema v1 validators");
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
- The build script runs the prerequisite workspace builds declared in the CLI
47
- package scripts, then produces the package runtime artifacts. After the build,
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
 
@@ -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.
@@ -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 " Recovery: no deployment is available. Run `gencow deploy` from this project.\n Retryable: no";
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 name = rest[0];
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
- const configPath = projectSelection.configPath ?? resolve(projectDir, "gencow.config.ts");
358
- if (existsSync(configPath)) {
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(`gencow.config.ts — added deploy.app = "${appId}"`);
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);
@@ -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 legacy/raw
205
- // errorMessage contents (which may contain SQL or credentials).
206
- if (
207
- backup.status === "failed" &&
208
- typeof backup.errorMessage === "string" &&
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.");