okengine 0.20.0 → 0.21.0
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/package.json +2 -1
- package/site/content/docs/client/calling.mdx +117 -29
- package/site/content/docs/client/index.mdx +3 -3
- package/site/content/docs/elements/flow/http.mdx +18 -15
- package/site/content/docs/elements/flow/index.mdx +26 -18
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/files.mdx +11 -5
- package/site/content/docs/elements/store/index.mdx +2 -3
- package/site/content/docs/elements/store/kv.mdx +8 -2
- package/site/content/docs/elements/store/sql.mdx +9 -6
- package/site/content/docs/elements/vault/index.mdx +11 -8
- package/site/content/docs/elements/vault/secrets.mdx +6 -3
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/recipes/rustfs.mdx +1 -1
- package/site/content/docs/reference/configuration.mdx +1 -1
- package/site/content/docs/reference/errors.mdx +199 -25
- package/site/content/docs/reference/fx.mdx +6 -1
- package/site/content/docs/understand/the-architecture.mdx +2 -2
- package/site/content/docs/understand/try-it.mdx +758 -25
- package/src/cli/dev-app-runner.ts +2 -1
- package/src/cli/start.ts +2 -1
- package/src/client/create.ts +3 -3
- package/src/client/explain.test.ts +252 -0
- package/src/client/explain.ts +272 -0
- package/src/client/live.test.ts +44 -0
- package/src/client/notes-contract.test.ts +10 -0
- package/src/client/sse.ts +6 -2
- package/src/client/transport.test.ts +67 -0
- package/src/client/transport.ts +24 -40
- package/src/client/types.ts +17 -6
- package/src/client-react/live-resource.ts +6 -2
- package/src/compiler/response.ts +17 -27
- package/src/console/server/invoke-user-flow.test.ts +8 -2
- package/src/console/server/invoke-user-flow.ts +12 -18
- package/src/console/server/security.gate.test.ts +1 -1
- package/src/console/ui-next/dist/assets/{access-page-DFLu0wTA.js → access-page-tIbsiphz.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-DGscxaF5.js → agent-disclosure-CSKumwS2.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-BGmRZk7d.js → cache-glyph-BCC-DxKT.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button--feUYxvG.js → call-pii-button-jOmYISdc.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-JWvpaiGY.js → collapsible-DC2xNaAb.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-D9yCJG4n.js → duration-tone-CwoV56jn.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-Bs6MD9GB.js → flows-page-gT1lsWQK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-xH8MrEnv.js → highlighted-json-C2GZEJNI.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-C4vB6ZIw.js → http-method-BdYjcIrD.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-yTCY4AcS.js → index-Cul17AcV.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-BxJ3R6dU.js → observability-page-oY9vdYBk.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-QRKB_IE8.js → replica-lag-C4QdAF7J.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-DqZ-fMu5.js → request-meta-C43DHyld.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-Dixb6L7a.js → store-page-CL0D9dOq.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-CazhjtiU.js → trace-detail-sheet-Dcpo4Us_.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-DlnqYKfr.js → tree-expand-toggle-CKJTuv43.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-BXTLjU2-.js → units-page-1PlT19ft.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-39KR__bc.js → vault-page-BwZ9YjTW.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/docker/docker.test.ts +3 -3
- package/src/docker/images-config.test.ts +4 -4
- package/src/docker/stack-id.test.ts +1 -1
- package/src/elements/store/files-errors.test.ts +149 -0
- package/src/elements/store/files-errors.ts +189 -0
- package/src/elements/store/kv-errors.test.ts +98 -0
- package/src/elements/store/kv-errors.ts +139 -0
- package/src/elements/store/resource.ts +11 -7
- package/src/elements/store/runtime.ts +18 -13
- package/src/elements/store/sql-errors.test.ts +197 -0
- package/src/elements/store/sql-errors.ts +294 -0
- package/src/elements/store/sql-session.test.ts +52 -0
- package/src/elements/store/sql-session.ts +6 -0
- package/src/elements/store/store-errors.ts +47 -0
- package/src/http.ts +9 -1
- package/src/i18n/catalogs/ar.ts +18 -0
- package/src/i18n/catalogs/en.ts +18 -0
- package/src/index.ts +9 -1
- package/src/kernel/app.ts +14 -2
- package/src/kernel/builtin-errors.test.ts +117 -0
- package/src/kernel/builtin-errors.ts +129 -0
- package/src/kernel/call.test.ts +182 -0
- package/src/kernel/errors-vault.ts +16 -0
- package/src/kernel/errors.registry.test.ts +7 -0
- package/src/kernel/errors.ts +94 -21
- package/src/kernel/fail-helpers.ts +34 -0
- package/src/kernel/fx.test.ts +8 -0
- package/src/kernel/fx.ts +12 -6
- package/src/kernel/index.ts +12 -1
- package/src/runtime/dev-request-log.ts +29 -11
- package/src/term.test.ts +76 -0
- package/src/term.ts +166 -3
package/src/kernel/fx.ts
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
recordWouldHaveFired,
|
|
59
59
|
touchDryRunStore,
|
|
60
60
|
} from "./dry-run.ts";
|
|
61
|
-
import
|
|
61
|
+
import type { FailFn } from "./errors.ts";
|
|
62
62
|
import { currentAbortSignal, linkAbort } from "./abort-scope.ts";
|
|
63
63
|
import {
|
|
64
64
|
fxAll,
|
|
@@ -82,6 +82,13 @@ import { okid } from "../okid.ts";
|
|
|
82
82
|
import { lazyRequire } from "./lazy-require.ts";
|
|
83
83
|
import type { AppMessageKey, MessageValues } from "../i18n/types.ts";
|
|
84
84
|
|
|
85
|
+
function loadFail(): FailFn {
|
|
86
|
+
return lazyRequire<typeof import("./fail-helpers.ts")>(
|
|
87
|
+
import.meta.dir,
|
|
88
|
+
["fail", "helpers"].join("-"),
|
|
89
|
+
).fail;
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
/** Lazy runs/window helpers — kept off the cold `oke` static graph. */
|
|
86
93
|
async function loadRunsWindow(): Promise<typeof import("../runs/window.ts")> {
|
|
87
94
|
return import("../runs/window.ts");
|
|
@@ -884,11 +891,10 @@ export interface Fx {
|
|
|
884
891
|
/**
|
|
885
892
|
* Flow-boundary failure value (does not throw).
|
|
886
893
|
*
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
* @param opts - Optional message
|
|
894
|
+
* Built-in helpers (`fx.fail.notFound`, `fx.fail.forbidden`, …) need no
|
|
895
|
+
* `errors:` declaration. Domain codes still use `fx.fail("OutOfStock", data)`.
|
|
890
896
|
*/
|
|
891
|
-
fail
|
|
897
|
+
readonly fail: FailFn;
|
|
892
898
|
/** JSON response helpers (status + Stripe-style envelope). */
|
|
893
899
|
readonly json: FxJson;
|
|
894
900
|
/**
|
|
@@ -2246,7 +2252,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
2246
2252
|
operator,
|
|
2247
2253
|
principal,
|
|
2248
2254
|
tenant,
|
|
2249
|
-
fail,
|
|
2255
|
+
fail: loadFail(),
|
|
2250
2256
|
json: {
|
|
2251
2257
|
ok<T>(value: T, opts?: { readonly meta?: Record<string, unknown> }): JsonResult<T> {
|
|
2252
2258
|
return {
|
package/src/kernel/index.ts
CHANGED
|
@@ -106,14 +106,15 @@ export {
|
|
|
106
106
|
type RunTelemetry,
|
|
107
107
|
} from "./run-telemetry.ts";
|
|
108
108
|
|
|
109
|
+
export { fail } from "./fail-helpers.ts";
|
|
109
110
|
export {
|
|
110
|
-
fail,
|
|
111
111
|
formatOkeMessage,
|
|
112
112
|
lookupOkeError,
|
|
113
113
|
OKE_ERROR_RANGES,
|
|
114
114
|
OKE_ERRORS,
|
|
115
115
|
OkeError,
|
|
116
116
|
throwOke,
|
|
117
|
+
type FailFn,
|
|
117
118
|
type FailOptions,
|
|
118
119
|
type FlowErrorValue,
|
|
119
120
|
type FlowFailure,
|
|
@@ -122,6 +123,16 @@ export {
|
|
|
122
123
|
type OkeErrorDomain,
|
|
123
124
|
type OkeErrorParams,
|
|
124
125
|
} from "./errors.ts";
|
|
126
|
+
export {
|
|
127
|
+
BUILTIN_ERROR_STATUS,
|
|
128
|
+
httpStatusForFailure,
|
|
129
|
+
statusForBuiltinError,
|
|
130
|
+
type BuiltinErrorBag,
|
|
131
|
+
type BuiltinErrorCode,
|
|
132
|
+
type BuiltinErrorMap,
|
|
133
|
+
type BuiltinValidationIssue,
|
|
134
|
+
type DatabaseErrorReason,
|
|
135
|
+
} from "./builtin-errors.ts";
|
|
125
136
|
|
|
126
137
|
export {
|
|
127
138
|
flow,
|
|
@@ -56,6 +56,8 @@ export type DevRequestLogInput = {
|
|
|
56
56
|
readonly ms: number;
|
|
57
57
|
/** Human failure detail (error.message / code) for 4xx/5xx. */
|
|
58
58
|
readonly detail?: string;
|
|
59
|
+
/** Failure `error.code` for the title chip. */
|
|
60
|
+
readonly errorCode?: string;
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
/**
|
|
@@ -85,16 +87,19 @@ export function isSilentDevRequest(method: string, path: string, surface?: DevLo
|
|
|
85
87
|
*
|
|
86
88
|
* @param response - HTTP response (cloned; original body stays readable)
|
|
87
89
|
*/
|
|
88
|
-
export async function
|
|
89
|
-
|
|
90
|
+
export async function failureEnvelopeFromResponse(
|
|
91
|
+
response: Response,
|
|
92
|
+
): Promise<{ readonly detail?: string; readonly code?: string }> {
|
|
93
|
+
if (response.status < 400) return {};
|
|
90
94
|
try {
|
|
91
95
|
const body: unknown = await response.clone().json();
|
|
92
|
-
if (body === null || typeof body !== "object" || !("error" in body)) return
|
|
96
|
+
if (body === null || typeof body !== "object" || !("error" in body)) return {};
|
|
93
97
|
const error = (body as { error: unknown }).error;
|
|
94
|
-
if (error === null || typeof error !== "object") return
|
|
98
|
+
if (error === null || typeof error !== "object") return {};
|
|
95
99
|
const rec = error as { message?: unknown; code?: unknown; data?: unknown };
|
|
100
|
+
const code = typeof rec.code === "string" ? rec.code : undefined;
|
|
96
101
|
if (typeof rec.message === "string" && rec.message.trim().length > 0) {
|
|
97
|
-
return rec.message.trim();
|
|
102
|
+
return { detail: rec.message.trim(), ...(code ? { code } : {}) };
|
|
98
103
|
}
|
|
99
104
|
if (
|
|
100
105
|
rec.data !== null &&
|
|
@@ -103,14 +108,25 @@ export async function failureDetailFromResponse(response: Response): Promise<str
|
|
|
103
108
|
typeof (rec.data as { reason: unknown }).reason === "string"
|
|
104
109
|
) {
|
|
105
110
|
const reason = (rec.data as { reason: string }).reason;
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
return {
|
|
112
|
+
detail: `${code ?? "Error"}: ${reason}`,
|
|
113
|
+
...(code ? { code } : {}),
|
|
114
|
+
};
|
|
108
115
|
}
|
|
109
|
-
if (
|
|
116
|
+
if (code) return { detail: code, code };
|
|
110
117
|
} catch {
|
|
111
118
|
// non-JSON error bodies stay status-only
|
|
112
119
|
}
|
|
113
|
-
return
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract a short failure detail from an OKE JSON envelope body.
|
|
125
|
+
*
|
|
126
|
+
* @param response - HTTP response (cloned; original body stays readable)
|
|
127
|
+
*/
|
|
128
|
+
export async function failureDetailFromResponse(response: Response): Promise<string | undefined> {
|
|
129
|
+
return (await failureEnvelopeFromResponse(response)).detail;
|
|
114
130
|
}
|
|
115
131
|
|
|
116
132
|
/**
|
|
@@ -135,6 +151,7 @@ export function logDevRequest(input: DevRequestLogInput): void {
|
|
|
135
151
|
status: input.status,
|
|
136
152
|
ms: input.ms,
|
|
137
153
|
detail: input.detail,
|
|
154
|
+
errorCode: input.errorCode,
|
|
138
155
|
}),
|
|
139
156
|
);
|
|
140
157
|
}
|
|
@@ -167,7 +184,7 @@ export async function timedDevFetch(
|
|
|
167
184
|
const url = new URL(request.url);
|
|
168
185
|
const method = request.method.toUpperCase();
|
|
169
186
|
const response = await handle(request);
|
|
170
|
-
const
|
|
187
|
+
const envelope = await failureEnvelopeFromResponse(response);
|
|
171
188
|
logDevRequest({
|
|
172
189
|
surface: options.surface ?? currentDevSurface(),
|
|
173
190
|
method,
|
|
@@ -176,7 +193,8 @@ export async function timedDevFetch(
|
|
|
176
193
|
runId: options.resolveRunId?.(request, response),
|
|
177
194
|
status: response.status,
|
|
178
195
|
ms: Math.round(performance.now() - started),
|
|
179
|
-
detail,
|
|
196
|
+
detail: envelope.detail,
|
|
197
|
+
errorCode: envelope.code,
|
|
180
198
|
});
|
|
181
199
|
return response;
|
|
182
200
|
}
|
package/src/term.test.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
formatOkeWordmark,
|
|
17
17
|
formatServiceLine,
|
|
18
18
|
formatSurfaceBox,
|
|
19
|
+
formatFatalError,
|
|
19
20
|
formatRequestLine,
|
|
20
21
|
formatStackSummary,
|
|
21
22
|
countTermLines,
|
|
@@ -296,6 +297,81 @@ describe("term", () => {
|
|
|
296
297
|
expect(out).not.toContain("↳");
|
|
297
298
|
});
|
|
298
299
|
|
|
300
|
+
test("formatRequestLine title chip includes error.code", () => {
|
|
301
|
+
const out = formatRequestLine({
|
|
302
|
+
surface: "Backend",
|
|
303
|
+
method: "POST",
|
|
304
|
+
path: "/links",
|
|
305
|
+
flow: "links.create",
|
|
306
|
+
status: 409,
|
|
307
|
+
ms: 3,
|
|
308
|
+
detail: "That value is already in use.",
|
|
309
|
+
errorCode: "Conflict",
|
|
310
|
+
color: false,
|
|
311
|
+
});
|
|
312
|
+
expect(out).toMatch(/◇ {2}409 Conflict\n/);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("formatFatalError prints VaultBootError as OKE1510 with cause and fix", () => {
|
|
316
|
+
const err = Object.assign(new Error("vault boot failed"), {
|
|
317
|
+
name: "VaultBootError",
|
|
318
|
+
gaps: [{ name: "STRIPE_SECRET_KEY" }, { name: "DATABASE_URL" }],
|
|
319
|
+
});
|
|
320
|
+
const out = formatFatalError(err, false);
|
|
321
|
+
expect(out).toContain("OKE1510");
|
|
322
|
+
expect(out).toContain("2 secrets have no value in any resolution layer.");
|
|
323
|
+
expect(out).toContain("- STRIPE_SECRET_KEY");
|
|
324
|
+
expect(out).toContain("oke vault set");
|
|
325
|
+
expect(out).toContain("https://oke.omqkhafi.dev/e/1510");
|
|
326
|
+
expect(out).not.toContain("Error:");
|
|
327
|
+
expect(out).not.toContain(" at ");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("formatFatalError prints GateBootError with posture gaps", () => {
|
|
331
|
+
const err = Object.assign(new Error("gate boot failed"), {
|
|
332
|
+
name: "GateBootError",
|
|
333
|
+
gaps: [{ flowId: "notes.get", method: "GET", path: "/notes/:id" }],
|
|
334
|
+
});
|
|
335
|
+
const out = formatFatalError(err, false);
|
|
336
|
+
expect(out).toContain("Gate");
|
|
337
|
+
expect(out).toContain("1 trigger(s) missing auth posture.");
|
|
338
|
+
expect(out).toContain("- notes.get GET /notes/:id");
|
|
339
|
+
expect(out).toContain("Attach a gate or `.public()`.");
|
|
340
|
+
expect(out).not.toContain(" at ");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("formatFatalError prints PluginNeedsError with unmet needs", () => {
|
|
344
|
+
const err = Object.assign(new Error("plugin needs failed"), {
|
|
345
|
+
name: "PluginNeedsError",
|
|
346
|
+
gaps: [{ plugin: "auth", need: "store.sql" }],
|
|
347
|
+
});
|
|
348
|
+
const out = formatFatalError(err, false);
|
|
349
|
+
expect(out).toContain("Plugin");
|
|
350
|
+
expect(out).toContain("1 unmet .needs() dependency.");
|
|
351
|
+
expect(out).toContain('- auth needs "store.sql"');
|
|
352
|
+
expect(out).toContain("Install the missing plugin");
|
|
353
|
+
expect(out).not.toContain(" at ");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("formatFatalError hides stack unless OKE_DEBUG=1", () => {
|
|
357
|
+
const prev = process.env["OKE_DEBUG"];
|
|
358
|
+
const boom = new Error("plain boom");
|
|
359
|
+
try {
|
|
360
|
+
delete process.env["OKE_DEBUG"];
|
|
361
|
+
const hidden = formatFatalError(boom, false);
|
|
362
|
+
expect(hidden).toContain("Error");
|
|
363
|
+
expect(hidden).toContain("plain boom");
|
|
364
|
+
expect(hidden).not.toContain(" at ");
|
|
365
|
+
|
|
366
|
+
process.env["OKE_DEBUG"] = "1";
|
|
367
|
+
const shown = formatFatalError(boom, false);
|
|
368
|
+
expect(shown).toContain("Error");
|
|
369
|
+
} finally {
|
|
370
|
+
if (prev === undefined) delete process.env["OKE_DEBUG"];
|
|
371
|
+
else process.env["OKE_DEBUG"] = prev;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
299
375
|
test("formatStackSummary is scannable", () => {
|
|
300
376
|
const out = formatStackSummary({
|
|
301
377
|
project: "oke-dev-a3f791",
|
package/src/term.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* Flip to monochrome with `NO_COLOR=1` or non-TTY stdout.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { docsUrl } from "./docs-origin.ts";
|
|
9
|
+
import { lookupOkeError } from "./kernel/errors.ts";
|
|
10
|
+
|
|
8
11
|
/** Whether ANSI color should be applied. */
|
|
9
12
|
export function termColorEnabled(stream: { readonly isTTY?: boolean } = process.stdout): boolean {
|
|
10
13
|
if (process.env["NO_COLOR"] !== undefined) return false;
|
|
@@ -701,15 +704,171 @@ export function formatDevNote(options: {
|
|
|
701
704
|
const ink = options.tone === "error" ? s.red : s.yellow;
|
|
702
705
|
const bar = `${s.dim}│${s.reset}`;
|
|
703
706
|
const width = Math.max(40, Math.min(72, (process.stdout.columns ?? 80) - 6));
|
|
704
|
-
const wrapped =
|
|
707
|
+
const wrapped = wrapBodyLines(options.body.trimEnd(), width);
|
|
705
708
|
const lines = [`${ink}◇${s.reset} ${s.bold}${ink}${options.title}${s.reset}`, bar];
|
|
706
709
|
for (const line of wrapped) {
|
|
707
|
-
lines.push(`${bar} ${ink}${line}${s.reset}`);
|
|
710
|
+
lines.push(line.length === 0 ? bar : `${bar} ${ink}${line}${s.reset}`);
|
|
708
711
|
}
|
|
709
712
|
lines.push(bar);
|
|
710
713
|
return `${lines.join("\n")}\n`;
|
|
711
714
|
}
|
|
712
715
|
|
|
716
|
+
function wrapBodyLines(body: string, width: number): string[] {
|
|
717
|
+
const out: string[] = [];
|
|
718
|
+
for (const raw of body.split("\n")) {
|
|
719
|
+
const line = raw.trimEnd();
|
|
720
|
+
if (line.length === 0) {
|
|
721
|
+
out.push("");
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
out.push(...wrapWords(line, width));
|
|
725
|
+
}
|
|
726
|
+
return out;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Boot / start fatal — cause, optional list, fix, docs URL. No stack unless
|
|
731
|
+
* `OKE_DEBUG=1`.
|
|
732
|
+
*
|
|
733
|
+
* @param err - Thrown boot value
|
|
734
|
+
* @param color - Color on/off
|
|
735
|
+
*/
|
|
736
|
+
export function formatFatalError(err: unknown, color: boolean = termColorEnabled()): string {
|
|
737
|
+
const debug = process.env["OKE_DEBUG"] === "1";
|
|
738
|
+
const formatted = describeFatalError(err, debug);
|
|
739
|
+
return formatDevNote({
|
|
740
|
+
title: formatted.title,
|
|
741
|
+
body: formatted.body,
|
|
742
|
+
tone: "error",
|
|
743
|
+
color,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function describeFatalError(
|
|
748
|
+
err: unknown,
|
|
749
|
+
debug: boolean,
|
|
750
|
+
): { readonly title: string; readonly body: string } {
|
|
751
|
+
const vault = vaultBootGaps(err);
|
|
752
|
+
if (vault) {
|
|
753
|
+
const def = lookupOkeError(1510);
|
|
754
|
+
const count = String(vault.length);
|
|
755
|
+
const cause = def
|
|
756
|
+
? interpolateTemplate(def.cause, { count })
|
|
757
|
+
: `${count} secrets have no value in any resolution layer.`;
|
|
758
|
+
const fix = def
|
|
759
|
+
? interpolateTemplate(def.fix, { count })
|
|
760
|
+
: "Set each name (`oke vault set <name>`, or `.env.local`).";
|
|
761
|
+
const url = def ? docsUrl(`/e/${def.code}`) : docsUrl("/e/1510");
|
|
762
|
+
const names = vault.map((g) =>
|
|
763
|
+
g.description ? `- ${g.name}: ${g.description}` : `- ${g.name}`,
|
|
764
|
+
);
|
|
765
|
+
return {
|
|
766
|
+
title: "OKE1510",
|
|
767
|
+
body: [cause, ...names, `→ ${fix}`, url].join("\n"),
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const gate = namedGaps(err, "GateBootError");
|
|
772
|
+
if (gate) {
|
|
773
|
+
const lines = gate.map((g) => {
|
|
774
|
+
const rec = g as Record<string, unknown>;
|
|
775
|
+
const flow = typeof rec.flowId === "string" ? rec.flowId : "flow";
|
|
776
|
+
const method = typeof rec.method === "string" ? rec.method : "";
|
|
777
|
+
const path = typeof rec.path === "string" ? rec.path : "";
|
|
778
|
+
return `- ${flow} ${method} ${path}`.trim();
|
|
779
|
+
});
|
|
780
|
+
return {
|
|
781
|
+
title: "Gate",
|
|
782
|
+
body: [
|
|
783
|
+
`${gate.length} trigger(s) missing auth posture.`,
|
|
784
|
+
...lines,
|
|
785
|
+
"→ Attach a gate or `.public()`.",
|
|
786
|
+
].join("\n"),
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const plugin = namedGaps(err, "PluginNeedsError");
|
|
791
|
+
if (plugin) {
|
|
792
|
+
const lines = plugin.map((g) => {
|
|
793
|
+
const rec = g as Record<string, unknown>;
|
|
794
|
+
const name = typeof rec.plugin === "string" ? rec.plugin : "plugin";
|
|
795
|
+
const need = typeof rec.need === "string" ? rec.need : "";
|
|
796
|
+
return `- ${name} needs "${need}"`;
|
|
797
|
+
});
|
|
798
|
+
return {
|
|
799
|
+
title: "Plugin",
|
|
800
|
+
body: [
|
|
801
|
+
`${plugin.length} unmet .needs() dependenc${plugin.length === 1 ? "y" : "ies"}.`,
|
|
802
|
+
...lines,
|
|
803
|
+
"→ Install the missing plugin or bind the element / driver.",
|
|
804
|
+
].join("\n"),
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (isOkeErrorShape(err)) {
|
|
809
|
+
return {
|
|
810
|
+
title: `OKE${err.code}`,
|
|
811
|
+
body: [err.causeText, `→ ${err.fix}`, err.docsUrl].join("\n"),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
816
|
+
const stack = debug && err instanceof Error && err.stack ? `\n${err.stack}` : "";
|
|
817
|
+
return { title: "Error", body: `${message}${stack}` };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function interpolateTemplate(template: string, params: Readonly<Record<string, string>>): string {
|
|
821
|
+
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_, key: string) => params[key] ?? `{${key}}`);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function vaultBootGaps(
|
|
825
|
+
err: unknown,
|
|
826
|
+
): readonly { readonly name: string; readonly description?: string }[] | undefined {
|
|
827
|
+
if (!err || typeof err !== "object") return undefined;
|
|
828
|
+
if ((err as { name?: unknown }).name !== "VaultBootError") return undefined;
|
|
829
|
+
const gaps = (err as { gaps?: unknown }).gaps;
|
|
830
|
+
if (!Array.isArray(gaps)) return undefined;
|
|
831
|
+
const out: { name: string; description?: string }[] = [];
|
|
832
|
+
for (const g of gaps) {
|
|
833
|
+
if (!g || typeof g !== "object") continue;
|
|
834
|
+
const name = (g as { name?: unknown }).name;
|
|
835
|
+
if (typeof name !== "string") continue;
|
|
836
|
+
const description = (g as { description?: unknown }).description;
|
|
837
|
+
out.push({
|
|
838
|
+
name,
|
|
839
|
+
...(typeof description === "string" ? { description } : {}),
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return out;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function namedGaps(err: unknown, name: string): readonly unknown[] | undefined {
|
|
846
|
+
if (!err || typeof err !== "object") return undefined;
|
|
847
|
+
if ((err as { name?: unknown }).name !== name) return undefined;
|
|
848
|
+
const gaps = (err as { gaps?: unknown }).gaps;
|
|
849
|
+
return Array.isArray(gaps) ? gaps : undefined;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function isOkeErrorShape(
|
|
853
|
+
err: unknown,
|
|
854
|
+
): err is { code: number; causeText: string; fix: string; docsUrl: string } {
|
|
855
|
+
if (!err || typeof err !== "object") return false;
|
|
856
|
+
const rec = err as {
|
|
857
|
+
name?: unknown;
|
|
858
|
+
code?: unknown;
|
|
859
|
+
causeText?: unknown;
|
|
860
|
+
fix?: unknown;
|
|
861
|
+
docsUrl?: unknown;
|
|
862
|
+
};
|
|
863
|
+
return (
|
|
864
|
+
rec.name === "OkeError" &&
|
|
865
|
+
typeof rec.code === "number" &&
|
|
866
|
+
typeof rec.causeText === "string" &&
|
|
867
|
+
typeof rec.fix === "string" &&
|
|
868
|
+
typeof rec.docsUrl === "string"
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
713
872
|
/**
|
|
714
873
|
* Boot honesty notice — Clack-like column under the `oke dev` hero.
|
|
715
874
|
*
|
|
@@ -808,6 +967,8 @@ export function formatRequestLine(options: {
|
|
|
808
967
|
readonly color?: boolean;
|
|
809
968
|
/** Failure detail printed on a follow-up line (4xx/5xx). */
|
|
810
969
|
readonly detail?: string;
|
|
970
|
+
/** Failure `error.code` for the title chip (`409 Conflict`). */
|
|
971
|
+
readonly errorCode?: string;
|
|
811
972
|
/** WideEvent / run id when the request executed a flow. */
|
|
812
973
|
readonly runId?: string;
|
|
813
974
|
}): string {
|
|
@@ -849,8 +1010,10 @@ export function formatRequestLine(options: {
|
|
|
849
1010
|
const detail = options.detail?.trim();
|
|
850
1011
|
if (!detail || options.status < 400) return main;
|
|
851
1012
|
const tone: DevNoteTone = options.status >= 500 ? "error" : "warn";
|
|
1013
|
+
const code = options.errorCode?.trim();
|
|
1014
|
+
const title = code && code.length > 0 ? `${options.status} ${code}` : String(options.status);
|
|
852
1015
|
const note = formatDevNote({
|
|
853
|
-
title
|
|
1016
|
+
title,
|
|
854
1017
|
body: detail,
|
|
855
1018
|
tone,
|
|
856
1019
|
color: options.color ?? termColorEnabled(),
|