okengine 0.6.1 → 0.7.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/AGENTS.md +2 -2
- package/README.md +73 -181
- package/package.json +2 -2
- package/site/content/docs/console/clock.mdx +25 -5
- package/site/content/docs/console/signals.mdx +13 -4
- package/site/content/docs/elements/ai.mdx +29 -2
- package/site/content/docs/elements/channel.mdx +19 -13
- package/site/content/docs/elements/clock.mdx +56 -24
- package/site/content/docs/elements/flow.mdx +2 -0
- package/site/content/docs/elements/gate.mdx +8 -1
- package/site/content/docs/elements/signal.mdx +173 -29
- package/site/content/docs/elements/store.mdx +464 -73
- package/site/content/docs/elements/vault.mdx +15 -5
- package/site/content/docs/get-started/basic-usage.mdx +13 -6
- package/site/content/docs/get-started/installation.mdx +49 -13
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/reference/cli.md +30 -2
- package/site/content/docs/reference/configuration.mdx +13 -13
- package/site/content/docs/reference/environment-variables.mdx +12 -10
- package/site/content/docs/reference/errors.mdx +12 -11
- package/site/content/docs/reference/fx.mdx +15 -13
- package/site/content/docs/reference/security.md +3 -1
- package/src/cli/ai-setup/ai-setup.test.ts +144 -0
- package/src/cli/ai-setup/apply.ts +214 -0
- package/src/cli/ai-setup/catalog.ts +263 -0
- package/src/cli/ai-setup/detect-ollama.ts +166 -0
- package/src/cli/ai-setup/index.ts +228 -0
- package/src/cli/ai-setup/prompts.ts +649 -0
- package/src/cli/ai-setup/recommend.test.ts +100 -0
- package/src/cli/ai-setup/recommend.ts +203 -0
- package/src/cli/ai.ts +38 -0
- package/src/cli/db-auto-push.test.ts +6 -1
- package/src/cli/db-auto-push.ts +8 -3
- package/src/cli/dev-db-push.test.ts +59 -0
- package/src/cli/dev.test.ts +40 -0
- package/src/cli/dev.ts +55 -1
- package/src/cli/docker-clean.test.ts +179 -0
- package/src/cli/docker-clean.ts +406 -0
- package/src/cli/docker.ts +16 -4
- package/src/cli/ensure-drizzle-config.ts +1 -1
- package/src/cli/hero-meta.test.ts +6 -5
- package/src/cli/hero-meta.ts +7 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.ts +4 -4
- package/src/cli/openbao-bootstrap.test.ts +1 -0
- package/src/cli/openbao-bootstrap.ts +9 -2
- package/src/cli/registry.ts +79 -1
- package/src/client/live-gap.test.ts +35 -0
- package/src/client/transport.test.ts +21 -0
- package/src/client/transport.ts +27 -5
- package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
- package/src/config/define-config.test.ts +6 -6
- package/src/config/index.ts +1 -1
- package/src/console/server/console.test.ts +45 -0
- package/src/console/server/flows.ts +39 -9
- package/src/console/server/i18n.ts +41 -0
- package/src/console/server/vault.ts +12 -38
- package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
- package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
- package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
- package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
- package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
- package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
- package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
- package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
- package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
- package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
- package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
- package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/shell/components/ui.tsx +5 -1
- package/src/console/ui/shell/setup/Wizard.tsx +26 -5
- package/src/docker/cleanup.test.ts +193 -0
- package/src/docker/cleanup.ts +355 -0
- package/src/docker/docker.test.ts +4 -0
- package/src/docker/index.ts +19 -0
- package/src/docker/recipes/ollama.ts +10 -5
- package/src/drivers/ai-ollama.ts +45 -2
- package/src/drivers/ai-providers.test.ts +31 -0
- package/src/drivers/index.ts +3 -0
- package/src/drivers/signal-engine.ts +98 -25
- package/src/drivers/signal-nats.ts +4 -4
- package/src/drivers/signal-postgres.ts +158 -64
- package/src/drivers/signal-redis.ts +4 -4
- package/src/drivers/signal-types.ts +61 -2
- package/src/drivers/vault-driver-removal.test.ts +5 -1
- package/src/elements/clock/chaos-child.ts +150 -0
- package/src/elements/clock/chaos.test.ts +204 -0
- package/src/elements/clock/reconcile.ts +110 -0
- package/src/elements/clock/runtime.ts +11 -0
- package/src/elements/clock.test.ts +164 -2
- package/src/elements/clock.ts +1 -0
- package/src/elements/gate/runtime.ts +6 -0
- package/src/elements/signal/chaos-child.ts +41 -4
- package/src/elements/signal/declare.ts +4 -1
- package/src/elements/signal/delivery-modes.test.ts +179 -0
- package/src/elements/signal/key-ordering.test.ts +306 -0
- package/src/elements/signal/lease-reclaim.test.ts +123 -0
- package/src/elements/signal/optional-emit.test.ts +76 -0
- package/src/elements/signal/order-lifecycle.test.ts +130 -0
- package/src/elements/signal/orphan-messages.test.ts +152 -0
- package/src/elements/signal/runtime.ts +11 -4
- package/src/elements/signal/schema-emit.test.ts +107 -0
- package/src/elements/signal.test.ts +89 -5
- package/src/elements/store/files-fx.ts +104 -0
- package/src/elements/store/files-image.test.ts +251 -0
- package/src/elements/store/files-image.ts +494 -0
- package/src/elements/store/runtime.ts +60 -0
- package/src/elements/store.ts +24 -0
- package/src/elements/vault/boot-chain.ts +150 -0
- package/src/elements/vault/runtime.ts +8 -0
- package/src/kernel/app.ts +3 -1
- package/src/kernel/boot-bind/clock.ts +52 -7
- package/src/kernel/boot-bind/gate.ts +71 -3
- package/src/kernel/boot-bind/honor-config.test.ts +247 -0
- package/src/kernel/boot-bind/signal.ts +64 -7
- package/src/kernel/boot-bind/vault.ts +27 -8
- package/src/kernel/boot.ts +10 -2
- package/src/kernel/errors.ts +8 -2
- package/src/kernel/fx.test.ts +13 -0
- package/src/kernel/fx.ts +27 -4
- package/src/runtime/dev-request-log.test.ts +20 -1
- package/src/runtime/dev-request-log.ts +38 -0
- package/src/term.test.ts +15 -0
- package/src/term.ts +8 -3
- package/src/console/ui/dist/assets/index-CjxwRGVv.js +0 -10
|
@@ -2,29 +2,86 @@
|
|
|
2
2
|
* Lazy signal binder — loaded only when Signal is declared.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
5
6
|
import { memorySignalDriver } from "../../drivers/signal-memory.ts";
|
|
7
|
+
import { createBunSignalRedisClient, redisSignalDriver } from "../../drivers/signal-redis.ts";
|
|
8
|
+
import type { SignalRedisClientLike } from "../../drivers/signal-types.ts";
|
|
6
9
|
import { createSignalRuntime, type SignalRuntime } from "../../elements/signal.ts";
|
|
7
|
-
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
8
10
|
import type { BootOptions } from "../boot.ts";
|
|
9
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Resolve `drivers.signal` for the active env (default `memory`).
|
|
14
|
+
*
|
|
15
|
+
* @param options - Boot options
|
|
16
|
+
* @param env - Active environment
|
|
17
|
+
*/
|
|
18
|
+
export function resolveSignalDriverId(options: BootOptions, env: ConfigEnv): string {
|
|
19
|
+
return resolveDriverId(options.config?.drivers?.signal, env) ?? "memory";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function redisUrlFor(docker: boolean): string | undefined {
|
|
23
|
+
const url = process.env.REDIS_URL ?? process.env.OKE_STORE_KV_URL ?? undefined;
|
|
24
|
+
if (!url && docker) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"oke boot: signal redis driver needs REDIS_URL (did `oke dev -d` write docker/.env.docker?)",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
31
|
+
|
|
10
32
|
/**
|
|
11
33
|
* Construct a Signal runtime, register decls / binding names, start the bus.
|
|
12
34
|
*
|
|
35
|
+
* Supported ids: `memory` · `redis`. `postgres` / `nats` fail loud until a real
|
|
36
|
+
* native client can be constructed (never silently bind memory).
|
|
37
|
+
*
|
|
13
38
|
* @param options - Boot options
|
|
14
39
|
* @param env - Active environment
|
|
15
40
|
* @param now - Clock
|
|
41
|
+
* @param docker - Docker mode
|
|
16
42
|
*/
|
|
17
43
|
export async function bindSignal(
|
|
18
44
|
options: BootOptions,
|
|
19
45
|
env: ConfigEnv,
|
|
20
46
|
now: () => number,
|
|
47
|
+
docker = false,
|
|
21
48
|
): Promise<SignalRuntime> {
|
|
22
|
-
const signalId =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
49
|
+
const signalId = resolveSignalDriverId(options, env);
|
|
50
|
+
const injectedRedis = options.clients?.signalRedis as SignalRedisClientLike | undefined;
|
|
51
|
+
|
|
52
|
+
let signal: SignalRuntime;
|
|
53
|
+
switch (signalId) {
|
|
54
|
+
case "memory":
|
|
55
|
+
signal = createSignalRuntime({
|
|
56
|
+
driver: memorySignalDriver,
|
|
57
|
+
now,
|
|
58
|
+
});
|
|
59
|
+
break;
|
|
60
|
+
case "redis": {
|
|
61
|
+
const redis = injectedRedis ?? createBunSignalRedisClient(redisUrlFor(docker));
|
|
62
|
+
signal = createSignalRuntime({
|
|
63
|
+
driver: redisSignalDriver,
|
|
64
|
+
now,
|
|
65
|
+
redis,
|
|
66
|
+
});
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "postgres":
|
|
70
|
+
throw new Error(
|
|
71
|
+
'oke boot: signal driver "postgres" needs a LISTEN/NOTIFY-capable SQL client — ' +
|
|
72
|
+
'not available via Bun.SQL yet. Use "redis" or "memory", or inject elements.signal.',
|
|
73
|
+
);
|
|
74
|
+
case "nats":
|
|
75
|
+
throw new Error(
|
|
76
|
+
'oke boot: signal driver "nats" has no production client bind yet — ' +
|
|
77
|
+
'use "redis" or "memory", or inject elements.signal.',
|
|
78
|
+
);
|
|
79
|
+
default:
|
|
80
|
+
throw new Error(
|
|
81
|
+
`oke boot: unknown signal driver "${signalId}" (expected memory · redis · postgres · nats)`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
28
85
|
for (const decl of options.signals ?? []) {
|
|
29
86
|
signal.register(decl);
|
|
30
87
|
}
|
|
@@ -2,28 +2,47 @@
|
|
|
2
2
|
* Lazy vault binder — loaded only when secrets are declared.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
6
6
|
import { createVaultRuntime, type VaultRuntime } from "../../elements/vault.ts";
|
|
7
|
-
import
|
|
7
|
+
import { buildVaultBootChain, normalizeVaultDriverId } from "../../elements/vault/boot-chain.ts";
|
|
8
8
|
import type { BootOptions } from "../boot.ts";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Resolve `drivers.vault` for the active env (default `env` locally, `memory` in test).
|
|
12
|
+
*
|
|
13
|
+
* @param options - Boot options
|
|
14
|
+
* @param env - Active environment
|
|
15
|
+
*/
|
|
16
|
+
export function resolveVaultDriverId(options: BootOptions, env: ConfigEnv): string {
|
|
17
|
+
const resolved = resolveDriverId(options.config?.drivers?.vault, env);
|
|
18
|
+
if (resolved) return resolved;
|
|
19
|
+
return env === "test" ? "memory" : "env";
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
/**
|
|
11
23
|
* Construct and boot a Vault runtime from BootOptions.
|
|
12
24
|
*
|
|
25
|
+
* Uses `drivers.vault` via the shared {@link buildVaultBootChain} helper
|
|
26
|
+
* (same chain Console uses). Injected `options.vault.chain` still wins for tests.
|
|
27
|
+
*
|
|
13
28
|
* @param options - Boot options
|
|
14
29
|
* @param env - Active environment
|
|
15
30
|
*/
|
|
16
31
|
export async function bindVault(options: BootOptions, env: ConfigEnv): Promise<VaultRuntime> {
|
|
17
32
|
const vaultSecrets = options.vault?.secrets ?? options.secrets ?? [];
|
|
33
|
+
const chain =
|
|
34
|
+
options.vault?.chain ??
|
|
35
|
+
buildVaultBootChain({
|
|
36
|
+
driverId: normalizeVaultDriverId(resolveVaultDriverId(options, env)),
|
|
37
|
+
env,
|
|
38
|
+
cwd: process.cwd(),
|
|
39
|
+
seed: {},
|
|
40
|
+
});
|
|
18
41
|
const vault = createVaultRuntime({
|
|
19
42
|
secrets: vaultSecrets,
|
|
20
|
-
chain
|
|
21
|
-
{
|
|
22
|
-
driver: memoryVaultDriver,
|
|
23
|
-
options: { secrets: {} },
|
|
24
|
-
},
|
|
25
|
-
],
|
|
43
|
+
chain,
|
|
26
44
|
allowDevFallbacks: options.vault?.allowDevFallbacks ?? env !== "prod",
|
|
45
|
+
now: options.vault?.now,
|
|
27
46
|
});
|
|
28
47
|
await vault.boot();
|
|
29
48
|
return vault;
|
package/src/kernel/boot.ts
CHANGED
|
@@ -114,6 +114,14 @@ export interface BootOptions {
|
|
|
114
114
|
readonly onSignal?: (signal: string, payload: unknown) => void | Promise<void>;
|
|
115
115
|
/** Injectable clock for test / frozen harnesses. */
|
|
116
116
|
readonly now?: () => number;
|
|
117
|
+
/**
|
|
118
|
+
* Test-only client injection for binders that open redis / signal redis.
|
|
119
|
+
* Production apps leave this unset.
|
|
120
|
+
*/
|
|
121
|
+
readonly clients?: {
|
|
122
|
+
readonly kv?: import("../drivers/types.ts").KvClientLike;
|
|
123
|
+
readonly signalRedis?: import("../drivers/signal-types.ts").SignalRedisClientLike;
|
|
124
|
+
};
|
|
117
125
|
/** Instance id for leader election. */
|
|
118
126
|
readonly instanceId?: string;
|
|
119
127
|
/**
|
|
@@ -344,7 +352,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
344
352
|
let signal = pre.signal;
|
|
345
353
|
if (needs.signal) {
|
|
346
354
|
if (!signal) {
|
|
347
|
-
signal = await signalBind!.bindSignal(options, env, now);
|
|
355
|
+
signal = await signalBind!.bindSignal(options, env, now, docker);
|
|
348
356
|
} else {
|
|
349
357
|
// Pre-built: still register decls / start if needed.
|
|
350
358
|
for (const decl of options.signals ?? []) {
|
|
@@ -376,7 +384,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
376
384
|
// Gate (before AI)
|
|
377
385
|
let gate = pre.gate;
|
|
378
386
|
if (needs.gate && !gate) {
|
|
379
|
-
gate = await gateBind!.bindGate(options, now);
|
|
387
|
+
gate = await gateBind!.bindGate(options, now, env, docker);
|
|
380
388
|
}
|
|
381
389
|
|
|
382
390
|
// 5. Channel
|
package/src/kernel/errors.ts
CHANGED
|
@@ -143,14 +143,20 @@ export const OKE_ERRORS = {
|
|
|
143
143
|
fix: 'Add "{resource}" to this flow\'s effects.calls.',
|
|
144
144
|
},
|
|
145
145
|
/**
|
|
146
|
-
* Emit target has no subscriber (unified-theory §21
|
|
147
|
-
*
|
|
146
|
+
* Emit target has no subscriber (unified-theory §21).
|
|
147
|
+
* Thrown at emit when `optional` is false and nobody is subscribed.
|
|
148
148
|
*/
|
|
149
149
|
ORPHAN_EMIT: {
|
|
150
150
|
code: 1042,
|
|
151
151
|
cause: 'Flow "{flow}" emits signal "{resource}" with no subscriber.',
|
|
152
152
|
fix: "Add `on({resource}, …)` or mark the signal `{ optional: true }`.",
|
|
153
153
|
},
|
|
154
|
+
/** Emit payload failed the signal's declared Standard Schema. */
|
|
155
|
+
SIGNAL_SCHEMA: {
|
|
156
|
+
code: 1043,
|
|
157
|
+
cause: '"{resource}": {detail}',
|
|
158
|
+
fix: "Fix schema payload.",
|
|
159
|
+
},
|
|
154
160
|
/**
|
|
155
161
|
* Domain table/column missing under docker/prod (migrations not applied).
|
|
156
162
|
* Store/DDL band starts at 1100.
|
package/src/kernel/fx.test.ts
CHANGED
|
@@ -258,6 +258,19 @@ describe("errors — registry", () => {
|
|
|
258
258
|
expect(err.message).toContain("→");
|
|
259
259
|
});
|
|
260
260
|
|
|
261
|
+
test("OKE1043 is reserved for signal schema emit failures", () => {
|
|
262
|
+
const def = OKE_ERRORS.SIGNAL_SCHEMA;
|
|
263
|
+
expect(def.code).toBe(1043);
|
|
264
|
+
expect(lookupOkeError(1043)).toEqual(def);
|
|
265
|
+
const err = new OkeError(def, {
|
|
266
|
+
resource: "order-placed",
|
|
267
|
+
detail: "total: Expected number, received string",
|
|
268
|
+
});
|
|
269
|
+
expect(err.message).toContain("OKE1043");
|
|
270
|
+
expect(err.message).toContain("order-placed");
|
|
271
|
+
expect(err.message).toContain("https://oke.omqkhafi.dev/e/1043");
|
|
272
|
+
});
|
|
273
|
+
|
|
261
274
|
test("fx.fail returns a value, not an exception", () => {
|
|
262
275
|
const fx = createFx({ flow: "x", effects: {} });
|
|
263
276
|
const result = fx.fail(
|
package/src/kernel/fx.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from "../elements/store.ts";
|
|
20
20
|
import type { SqlRow } from "../drivers/types.ts";
|
|
21
21
|
import type { SignalRuntime } from "../elements/signal.ts";
|
|
22
|
+
import type { SignalEmitOptions } from "../drivers/signal-types.ts";
|
|
22
23
|
import type { VaultRuntime } from "../elements/vault.ts";
|
|
23
24
|
import type { ChannelRuntime } from "../elements/channel.ts";
|
|
24
25
|
import type { AiRuntime } from "../elements/ai.ts";
|
|
@@ -295,8 +296,9 @@ export interface Fx {
|
|
|
295
296
|
*
|
|
296
297
|
* @param signal - Signal name or handle
|
|
297
298
|
* @param payload - Payload
|
|
299
|
+
* @param options - Optional emit options (`key` for per-key once ordering)
|
|
298
300
|
*/
|
|
299
|
-
emit(signal: NamedRef, payload?: unknown): Promise<void>;
|
|
301
|
+
emit(signal: NamedRef, payload?: unknown, options?: SignalEmitOptions): Promise<void>;
|
|
300
302
|
/**
|
|
301
303
|
* Call another flow (records `call`). Stub returns `undefined`.
|
|
302
304
|
*
|
|
@@ -892,7 +894,28 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
892
894
|
});
|
|
893
895
|
}
|
|
894
896
|
|
|
895
|
-
|
|
897
|
+
if (decl.facet === "files") {
|
|
898
|
+
const ref = decl.ref;
|
|
899
|
+
return runtime.openFilesFx(
|
|
900
|
+
decl as Extract<StoreDecl, { facet: "files" }>,
|
|
901
|
+
{
|
|
902
|
+
effects: options.effects ?? {},
|
|
903
|
+
revealPii: options.revealPii,
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
gate: gated,
|
|
907
|
+
refuseDryRunWrite: () => {
|
|
908
|
+
if (isDryRun()) {
|
|
909
|
+
throw new DryRunWriteIsolationError(
|
|
910
|
+
`Driver-backed store "${ref}" cannot isolate writes during dry-run; dry-run refused rather than risk a double-write.`,
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// KV / index — thin gated wrappers preserving driver methods.
|
|
896
919
|
const baseRef = decl.ref;
|
|
897
920
|
return new Proxy({} as StoreHandle, {
|
|
898
921
|
get(_t, prop) {
|
|
@@ -1010,11 +1033,11 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1010
1033
|
store(ref) {
|
|
1011
1034
|
return storeHandle(ref);
|
|
1012
1035
|
},
|
|
1013
|
-
emit(signal, payload) {
|
|
1036
|
+
emit(signal, payload, emitOptions) {
|
|
1014
1037
|
const name = resolveName(signal);
|
|
1015
1038
|
return gated("emit", name, async () => {
|
|
1016
1039
|
if (options.signalRuntime) {
|
|
1017
|
-
await options.signalRuntime.emit(name, payload);
|
|
1040
|
+
await options.signalRuntime.emit(name, payload, emitOptions);
|
|
1018
1041
|
}
|
|
1019
1042
|
});
|
|
1020
1043
|
},
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
failureDetailFromResponse,
|
|
8
|
+
isSilentDevRequest,
|
|
9
|
+
shouldLogDevRequests,
|
|
10
|
+
} from "./dev-request-log.ts";
|
|
7
11
|
|
|
8
12
|
describe("dev-request-log", () => {
|
|
9
13
|
const prev = process.env.OKE_DEV_REQUEST_LOG;
|
|
@@ -27,4 +31,19 @@ describe("dev-request-log", () => {
|
|
|
27
31
|
expect(isSilentDevRequest("GET", "/_oke/client.json")).toBe(true);
|
|
28
32
|
expect(isSilentDevRequest("POST", "/console/flows")).toBe(false);
|
|
29
33
|
});
|
|
34
|
+
|
|
35
|
+
test("failureDetailFromResponse reads error.message from envelope", async () => {
|
|
36
|
+
const res = Response.json(
|
|
37
|
+
{
|
|
38
|
+
data: null,
|
|
39
|
+
error: {
|
|
40
|
+
code: "ClaimFailed",
|
|
41
|
+
data: { reason: "password_policy" },
|
|
42
|
+
message: "Password needs at least 12 characters, including a letter and a number.",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{ status: 400 },
|
|
46
|
+
);
|
|
47
|
+
expect(await failureDetailFromResponse(res)).toMatch(/12 characters/);
|
|
48
|
+
});
|
|
30
49
|
});
|
|
@@ -52,6 +52,8 @@ export type DevRequestLogInput = {
|
|
|
52
52
|
readonly flow?: string;
|
|
53
53
|
readonly status: number;
|
|
54
54
|
readonly ms: number;
|
|
55
|
+
/** Human failure detail (error.message / code) for 4xx/5xx. */
|
|
56
|
+
readonly detail?: string;
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
/**
|
|
@@ -70,6 +72,39 @@ export function isSilentDevRequest(method: string, path: string): boolean {
|
|
|
70
72
|
return /\.(?:js|css|map|svg|png|ico|woff2?|ttf|webp)$/i.test(path);
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Extract a short failure detail from an OKE JSON envelope body.
|
|
77
|
+
*
|
|
78
|
+
* @param response - HTTP response (cloned; original body stays readable)
|
|
79
|
+
*/
|
|
80
|
+
export async function failureDetailFromResponse(response: Response): Promise<string | undefined> {
|
|
81
|
+
if (response.status < 400) return undefined;
|
|
82
|
+
try {
|
|
83
|
+
const body: unknown = await response.clone().json();
|
|
84
|
+
if (body === null || typeof body !== "object" || !("error" in body)) return undefined;
|
|
85
|
+
const error = (body as { error: unknown }).error;
|
|
86
|
+
if (error === null || typeof error !== "object") return undefined;
|
|
87
|
+
const rec = error as { message?: unknown; code?: unknown; data?: unknown };
|
|
88
|
+
if (typeof rec.message === "string" && rec.message.trim().length > 0) {
|
|
89
|
+
return rec.message.trim();
|
|
90
|
+
}
|
|
91
|
+
if (
|
|
92
|
+
rec.data !== null &&
|
|
93
|
+
typeof rec.data === "object" &&
|
|
94
|
+
"reason" in rec.data &&
|
|
95
|
+
typeof (rec.data as { reason: unknown }).reason === "string"
|
|
96
|
+
) {
|
|
97
|
+
const reason = (rec.data as { reason: string }).reason;
|
|
98
|
+
const code = typeof rec.code === "string" ? rec.code : "Error";
|
|
99
|
+
return `${code}: ${reason}`;
|
|
100
|
+
}
|
|
101
|
+
if (typeof rec.code === "string") return rec.code;
|
|
102
|
+
} catch {
|
|
103
|
+
// non-JSON error bodies stay status-only
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
73
108
|
/**
|
|
74
109
|
* Print one request line when {@link shouldLogDevRequests} is on.
|
|
75
110
|
*
|
|
@@ -86,6 +121,7 @@ export function logDevRequest(input: DevRequestLogInput): void {
|
|
|
86
121
|
flow: input.flow,
|
|
87
122
|
status: input.status,
|
|
88
123
|
ms: input.ms,
|
|
124
|
+
detail: input.detail,
|
|
89
125
|
}),
|
|
90
126
|
);
|
|
91
127
|
}
|
|
@@ -115,6 +151,7 @@ export async function timedDevFetch(
|
|
|
115
151
|
const url = new URL(request.url);
|
|
116
152
|
const method = request.method.toUpperCase();
|
|
117
153
|
const response = await handle(request);
|
|
154
|
+
const detail = await failureDetailFromResponse(response);
|
|
118
155
|
logDevRequest({
|
|
119
156
|
surface: options.surface ?? currentDevSurface(),
|
|
120
157
|
method,
|
|
@@ -122,6 +159,7 @@ export async function timedDevFetch(
|
|
|
122
159
|
flow: options.resolveFlow?.(request, response),
|
|
123
160
|
status: response.status,
|
|
124
161
|
ms: Math.round(performance.now() - started),
|
|
162
|
+
detail,
|
|
125
163
|
});
|
|
126
164
|
return response;
|
|
127
165
|
}
|
package/src/term.test.ts
CHANGED
|
@@ -125,6 +125,21 @@ describe("term", () => {
|
|
|
125
125
|
expect(out).not.toMatch(/\u001b\[/);
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
test("formatRequestLine prints failure detail under 4xx/5xx", () => {
|
|
129
|
+
const out = formatRequestLine({
|
|
130
|
+
surface: "Console",
|
|
131
|
+
method: "POST",
|
|
132
|
+
path: "/console/setup/claim",
|
|
133
|
+
flow: "console.setup.claim",
|
|
134
|
+
status: 400,
|
|
135
|
+
ms: 1,
|
|
136
|
+
detail: "Password needs at least 12 characters, including a letter and a number.",
|
|
137
|
+
color: false,
|
|
138
|
+
});
|
|
139
|
+
expect(out).toContain("400");
|
|
140
|
+
expect(out).toContain("↳ Password needs at least 12 characters");
|
|
141
|
+
});
|
|
142
|
+
|
|
128
143
|
test("formatStackSummary is scannable", () => {
|
|
129
144
|
const out = formatStackSummary({
|
|
130
145
|
project: "oke-dev-a3f791",
|
package/src/term.ts
CHANGED
|
@@ -300,6 +300,8 @@ export function formatRequestLine(options: {
|
|
|
300
300
|
/** Instant for date/time columns (default now). */
|
|
301
301
|
readonly at?: Date;
|
|
302
302
|
readonly color?: boolean;
|
|
303
|
+
/** Failure detail printed on a follow-up line (4xx/5xx). */
|
|
304
|
+
readonly detail?: string;
|
|
303
305
|
}): string {
|
|
304
306
|
const s = termStyle(options.color ?? termColorEnabled());
|
|
305
307
|
const at = options.at ?? new Date();
|
|
@@ -323,7 +325,7 @@ export function formatRequestLine(options: {
|
|
|
323
325
|
const path = options.path.length > 28 ? `${options.path.slice(0, 27)}…` : options.path.padEnd(28);
|
|
324
326
|
const flow = (options.flow ?? "—").padEnd(22);
|
|
325
327
|
const ms = `${options.ms}ms`.padStart(6);
|
|
326
|
-
|
|
328
|
+
const main =
|
|
327
329
|
`${surfaceColor}●${s.reset} ` +
|
|
328
330
|
`${surfaceColor}${options.surface.padEnd(7)}${s.reset} ` +
|
|
329
331
|
`${methodColor}${method}${s.reset} ` +
|
|
@@ -332,8 +334,11 @@ export function formatRequestLine(options: {
|
|
|
332
334
|
`${s.dim}${ms}${s.reset} ` +
|
|
333
335
|
`${statusColor}${options.status}${s.reset} ` +
|
|
334
336
|
`${s.dim}${date}${s.reset} ` +
|
|
335
|
-
`${s.dim}${time}${s.reset}\n
|
|
336
|
-
);
|
|
337
|
+
`${s.dim}${time}${s.reset}\n`;
|
|
338
|
+
const detail = options.detail?.trim();
|
|
339
|
+
if (!detail || options.status < 400) return main;
|
|
340
|
+
const clipped = detail.length > 120 ? `${detail.slice(0, 119)}…` : detail;
|
|
341
|
+
return `${main}${s.dim} ↳ ${clipped}${s.reset}\n`;
|
|
337
342
|
}
|
|
338
343
|
|
|
339
344
|
/**
|