okengine 0.6.0 → 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 +98 -71
- package/package.json +4 -4
- 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 +90 -20
- 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 +18 -17
- package/site/content/docs/reference/environment-variables.mdx +44 -18
- 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/openbao-restart.integration.test.ts +106 -97
- 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/dockerfile.integration.test.ts +126 -119
- package/src/docker/index.ts +19 -0
- package/src/docker/recipes/ollama.ts +10 -5
- package/src/docker/stack.integration.test.ts +118 -102
- package/src/drivers/ai-ollama-tools.integration.test.ts +8 -6
- package/src/drivers/ai-ollama.integration.test.ts +3 -19
- package/src/drivers/ai-ollama.ts +45 -2
- package/src/drivers/ai-providers.test.ts +31 -0
- package/src/drivers/channel-fcm.ts +49 -53
- package/src/drivers/channel-msegat.ts +61 -0
- package/src/drivers/channel-sently-map.ts +57 -0
- package/src/drivers/channel-sently.test.ts +99 -0
- package/src/drivers/channel-sndr.ts +28 -0
- package/src/drivers/channel-taqnyat.ts +57 -0
- package/src/drivers/channel-types.ts +79 -2
- package/src/drivers/channel-unifonic.ts +26 -43
- package/src/drivers/channel-wa-cloud.ts +33 -47
- package/src/drivers/channel-webpush.ts +39 -239
- package/src/drivers/index.ts +7 -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/channel/costs.test.ts +2 -2
- package/src/elements/channel/costs.ts +14 -2
- package/src/elements/channel/mime.ts +11 -0
- package/src/elements/channel/runtime.ts +94 -0
- package/src/elements/channel/sndr-webhooks.test.ts +26 -0
- package/src/elements/channel.ts +10 -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/index.ts +9 -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/channel.test.ts +68 -3
- package/src/kernel/boot-bind/channel.ts +93 -2
- 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/plugins/auth-delivery.mailpit.integration.test.ts +10 -4
- package/src/release/exports.test.ts +26 -0
- package/src/release/exports.ts +64 -5
- package/src/release/index.ts +5 -0
- package/src/release/measure.exports.test.ts +13 -1
- package/src/release/measure.ts +76 -13
- package/src/release/official-plugins.ts +46 -0
- package/src/release/readme.test.ts +30 -2
- 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
package/src/release/exports.ts
CHANGED
|
@@ -2,22 +2,37 @@
|
|
|
2
2
|
* Resolve published `package.json` exports into measurable bundle entries.
|
|
3
3
|
*
|
|
4
4
|
* Expands `./drivers/*` to concrete driver modules (tree-shaken subpaths).
|
|
5
|
+
* Official `okengine/plugins` named modules are sampled separately (not published
|
|
6
|
+
* as `./plugins/*` subpaths).
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import { readdir } from "node:fs/promises";
|
|
8
10
|
import { basename, join, resolve } from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
OFFICIAL_PLUGIN_BUDGETS,
|
|
13
|
+
type OfficialPluginBudget,
|
|
14
|
+
type PluginBudgetCategory,
|
|
15
|
+
} from "./official-plugins.ts";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
OFFICIAL_PLUGIN_BUDGETS,
|
|
19
|
+
PLUGIN_BUDGET_CATEGORIES,
|
|
20
|
+
type OfficialPluginBudget,
|
|
21
|
+
type PluginBudgetCategory,
|
|
22
|
+
} from "./official-plugins.ts";
|
|
9
23
|
|
|
10
24
|
const ROOT = resolve(import.meta.dir, "../..");
|
|
11
25
|
const DRIVERS_DIR = join(ROOT, "src/drivers");
|
|
26
|
+
const PLUGINS_DIR = join(ROOT, "src/plugins");
|
|
12
27
|
|
|
13
28
|
/** Report / snapshot group for a budget sample. */
|
|
14
|
-
export type BudgetGroup = "core" | "exports" | "drivers";
|
|
29
|
+
export type BudgetGroup = "core" | "exports" | "plugins" | "drivers";
|
|
15
30
|
|
|
16
31
|
/** One published subpath ready for gzip measurement. */
|
|
17
32
|
export interface ExportBudgetTarget {
|
|
18
33
|
/** Sample id — `export:<subpath>`. */
|
|
19
34
|
readonly id: string;
|
|
20
|
-
/** Package subpath (`.` or `./store`, `./drivers/postgres`, …). */
|
|
35
|
+
/** Package subpath (`.` or `./store`, `./drivers/postgres`, `./plugins/cors`, …). */
|
|
21
36
|
readonly subpath: string;
|
|
22
37
|
/** Short human label (`channel`, `postgres`, `okengine`, …). */
|
|
23
38
|
readonly label: string;
|
|
@@ -25,18 +40,23 @@ export interface ExportBudgetTarget {
|
|
|
25
40
|
readonly group: BudgetGroup;
|
|
26
41
|
/** Absolute path to the TypeScript entry. */
|
|
27
42
|
readonly entry: string;
|
|
43
|
+
/** Docs category when `group` is `plugins`. */
|
|
44
|
+
readonly category?: PluginBudgetCategory;
|
|
28
45
|
}
|
|
29
46
|
|
|
30
47
|
/**
|
|
31
48
|
* Short display name for a package subpath.
|
|
32
49
|
*
|
|
33
|
-
* @param subpath - `.` / `./channel` / `./drivers/postgres`
|
|
50
|
+
* @param subpath - `.` / `./channel` / `./drivers/postgres` / `./plugins/cors`
|
|
34
51
|
*/
|
|
35
52
|
export function exportBudgetLabel(subpath: string): string {
|
|
36
53
|
if (subpath === ".") return "okengine";
|
|
37
54
|
if (subpath.startsWith("./drivers/")) {
|
|
38
55
|
return subpath.slice("./drivers/".length);
|
|
39
56
|
}
|
|
57
|
+
if (subpath.startsWith("./plugins/")) {
|
|
58
|
+
return subpath.slice("./plugins/".length);
|
|
59
|
+
}
|
|
40
60
|
if (subpath.startsWith("./")) return subpath.slice(2);
|
|
41
61
|
return subpath;
|
|
42
62
|
}
|
|
@@ -50,6 +70,10 @@ export function exportBudgetGroup(subpath: string): BudgetGroup {
|
|
|
50
70
|
if (subpath === "./drivers" || subpath.startsWith("./drivers/")) {
|
|
51
71
|
return "drivers";
|
|
52
72
|
}
|
|
73
|
+
// Named plugin modules (not the `./plugins` barrel — that stays in exports).
|
|
74
|
+
if (subpath.startsWith("./plugins/")) {
|
|
75
|
+
return "plugins";
|
|
76
|
+
}
|
|
53
77
|
return "exports";
|
|
54
78
|
}
|
|
55
79
|
|
|
@@ -82,6 +106,30 @@ export async function listDriverModules(driversDir = DRIVERS_DIR): Promise<reado
|
|
|
82
106
|
return entries.filter(isMeasurableDriverFile).sort((a, b) => a.localeCompare(b));
|
|
83
107
|
}
|
|
84
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Resolve official plugin modules into measurement targets.
|
|
111
|
+
*
|
|
112
|
+
* @param options - Override plugins dir (tests)
|
|
113
|
+
*/
|
|
114
|
+
export function resolvePluginBudgetTargets(options?: {
|
|
115
|
+
readonly pluginsDir?: string;
|
|
116
|
+
readonly catalogue?: readonly OfficialPluginBudget[];
|
|
117
|
+
}): readonly ExportBudgetTarget[] {
|
|
118
|
+
const pluginsDir = options?.pluginsDir ?? PLUGINS_DIR;
|
|
119
|
+
const catalogue = options?.catalogue ?? OFFICIAL_PLUGIN_BUDGETS;
|
|
120
|
+
return catalogue.map((plugin) => {
|
|
121
|
+
const subpath = `./plugins/${plugin.name}`;
|
|
122
|
+
return {
|
|
123
|
+
id: `export:${subpath}`,
|
|
124
|
+
subpath,
|
|
125
|
+
label: plugin.name,
|
|
126
|
+
group: "plugins" as const,
|
|
127
|
+
entry: join(pluginsDir, plugin.file),
|
|
128
|
+
category: plugin.category,
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
85
133
|
/**
|
|
86
134
|
* Resolve every published export into a stable, sorted measurement target list.
|
|
87
135
|
*
|
|
@@ -91,12 +139,14 @@ export async function resolveExportBudgetTargets(options?: {
|
|
|
91
139
|
readonly root?: string;
|
|
92
140
|
readonly pkg?: PackageExports;
|
|
93
141
|
readonly driversDir?: string;
|
|
142
|
+
readonly pluginsDir?: string;
|
|
94
143
|
}): Promise<readonly ExportBudgetTarget[]> {
|
|
95
144
|
const root = options?.root ?? ROOT;
|
|
96
145
|
const pkg =
|
|
97
146
|
options?.pkg ?? ((await Bun.file(join(root, "package.json")).json()) as PackageExports);
|
|
98
147
|
const exportsMap = pkg.exports ?? {};
|
|
99
148
|
const driversDir = options?.driversDir ?? join(root, "src/drivers");
|
|
149
|
+
const pluginsDir = options?.pluginsDir ?? join(root, "src/plugins");
|
|
100
150
|
|
|
101
151
|
const targets: ExportBudgetTarget[] = [];
|
|
102
152
|
|
|
@@ -121,6 +171,8 @@ export async function resolveExportBudgetTargets(options?: {
|
|
|
121
171
|
targets.push(targetFor(subpath, resolve(root, entryRel)));
|
|
122
172
|
}
|
|
123
173
|
|
|
174
|
+
targets.push(...resolvePluginBudgetTargets({ pluginsDir }));
|
|
175
|
+
|
|
124
176
|
targets.sort((a, b) => {
|
|
125
177
|
const g = groupOrder(a.group) - groupOrder(b.group);
|
|
126
178
|
if (g !== 0) return g;
|
|
@@ -129,6 +181,12 @@ export async function resolveExportBudgetTargets(options?: {
|
|
|
129
181
|
if (b.subpath === "./drivers") return 1;
|
|
130
182
|
if (a.subpath === ".") return -1;
|
|
131
183
|
if (b.subpath === ".") return 1;
|
|
184
|
+
// Catalogue order for plugins (docs category order).
|
|
185
|
+
if (a.group === "plugins" && b.group === "plugins") {
|
|
186
|
+
const ai = OFFICIAL_PLUGIN_BUDGETS.findIndex((p) => p.name === a.label);
|
|
187
|
+
const bi = OFFICIAL_PLUGIN_BUDGETS.findIndex((p) => p.name === b.label);
|
|
188
|
+
if (ai !== -1 && bi !== -1) return ai - bi;
|
|
189
|
+
}
|
|
132
190
|
return a.label.localeCompare(b.label);
|
|
133
191
|
});
|
|
134
192
|
return targets;
|
|
@@ -146,6 +204,7 @@ function targetFor(subpath: string, entry: string): ExportBudgetTarget {
|
|
|
146
204
|
|
|
147
205
|
function groupOrder(group: BudgetGroup): number {
|
|
148
206
|
if (group === "exports") return 0;
|
|
149
|
-
if (group === "
|
|
150
|
-
return 2;
|
|
207
|
+
if (group === "plugins") return 1;
|
|
208
|
+
if (group === "drivers") return 2;
|
|
209
|
+
return 3;
|
|
151
210
|
}
|
package/src/release/index.ts
CHANGED
|
@@ -7,9 +7,14 @@ export {
|
|
|
7
7
|
exportBudgetLabel,
|
|
8
8
|
isMeasurableDriverFile,
|
|
9
9
|
listDriverModules,
|
|
10
|
+
OFFICIAL_PLUGIN_BUDGETS,
|
|
11
|
+
PLUGIN_BUDGET_CATEGORIES,
|
|
10
12
|
resolveExportBudgetTargets,
|
|
13
|
+
resolvePluginBudgetTargets,
|
|
11
14
|
type BudgetGroup,
|
|
12
15
|
type ExportBudgetTarget,
|
|
16
|
+
type OfficialPluginBudget,
|
|
17
|
+
type PluginBudgetCategory,
|
|
13
18
|
} from "./exports.ts";
|
|
14
19
|
|
|
15
20
|
export {
|
|
@@ -31,7 +31,7 @@ describe("export gzip budgets", () => {
|
|
|
31
31
|
);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
test("formatBudgetsReport groups Core / Exports / Drivers with short names", () => {
|
|
34
|
+
test("formatBudgetsReport groups Core / Exports / Plugins / Drivers with short names", () => {
|
|
35
35
|
const snapshot: BudgetsSnapshot = {
|
|
36
36
|
measuredAt: "2026-07-25T00:00:00.000Z",
|
|
37
37
|
version: "0.0.0",
|
|
@@ -56,6 +56,16 @@ describe("export gzip budgets", () => {
|
|
|
56
56
|
group: "exports",
|
|
57
57
|
ok: true,
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
id: "export:./plugins/cors",
|
|
61
|
+
label: "cors",
|
|
62
|
+
value: 100,
|
|
63
|
+
limit: 200,
|
|
64
|
+
unit: "bytes",
|
|
65
|
+
gate: "regression",
|
|
66
|
+
group: "plugins",
|
|
67
|
+
ok: true,
|
|
68
|
+
},
|
|
59
69
|
{
|
|
60
70
|
id: "export:./drivers/postgres",
|
|
61
71
|
label: "postgres",
|
|
@@ -71,8 +81,10 @@ describe("export gzip budgets", () => {
|
|
|
71
81
|
const report = formatBudgetsReport(snapshot);
|
|
72
82
|
expect(report).toContain("\nCore\n");
|
|
73
83
|
expect(report).toContain("\nExports\n");
|
|
84
|
+
expect(report).toContain("\nPlugins\n");
|
|
74
85
|
expect(report).toContain("\nDrivers\n");
|
|
75
86
|
expect(report).toContain("[ok] channel:");
|
|
87
|
+
expect(report).toContain("[ok] cors:");
|
|
76
88
|
expect(report).toContain("[ok] postgres:");
|
|
77
89
|
expect(report).not.toContain("regression Export");
|
|
78
90
|
expect(report).not.toContain("./channel");
|
package/src/release/measure.ts
CHANGED
|
@@ -6,7 +6,12 @@ import { mkdtemp, readdir, rm } from "node:fs/promises";
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { join, resolve } from "node:path";
|
|
8
8
|
import { createRouter } from "../kernel/router.ts";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type BudgetGroup,
|
|
11
|
+
OFFICIAL_PLUGIN_BUDGETS,
|
|
12
|
+
PLUGIN_BUDGET_CATEGORIES,
|
|
13
|
+
resolveExportBudgetTargets,
|
|
14
|
+
} from "./exports.ts";
|
|
10
15
|
import {
|
|
11
16
|
CLIENT_BUDGET_BYTES,
|
|
12
17
|
COLD_START_BUDGET_MS,
|
|
@@ -56,9 +61,18 @@ export type BudgetGate = "absolute" | "regression";
|
|
|
56
61
|
const GROUP_HEADINGS: Readonly<Record<BudgetGroup, string>> = {
|
|
57
62
|
core: "Core",
|
|
58
63
|
exports: "Exports",
|
|
64
|
+
plugins: "Plugins",
|
|
59
65
|
drivers: "Drivers",
|
|
60
66
|
};
|
|
61
67
|
|
|
68
|
+
/** Docs category → markdown subheading under Plugins. */
|
|
69
|
+
const PLUGIN_CATEGORY_HEADINGS: Readonly<Record<string, string>> = {
|
|
70
|
+
auth: "Auth",
|
|
71
|
+
security: "Security",
|
|
72
|
+
ops: "Ops",
|
|
73
|
+
perf: "Perf",
|
|
74
|
+
};
|
|
75
|
+
|
|
62
76
|
/** One measured budget sample. */
|
|
63
77
|
export interface BudgetSample {
|
|
64
78
|
/** Stable metric id. */
|
|
@@ -489,13 +503,13 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
|
|
|
489
503
|
}
|
|
490
504
|
|
|
491
505
|
/**
|
|
492
|
-
* Format a snapshot for CI logs (Core / Exports / Drivers
|
|
506
|
+
* Format a snapshot for CI logs (Core / Exports / Plugins / Drivers).
|
|
493
507
|
*
|
|
494
508
|
* @param snapshot - Measured budgets
|
|
495
509
|
*/
|
|
496
510
|
export function formatBudgetsReport(snapshot: BudgetsSnapshot): string {
|
|
497
511
|
const lines = [`okengine budgets v${snapshot.version} @ ${snapshot.measuredAt}`];
|
|
498
|
-
const order: readonly BudgetGroup[] = ["core", "exports", "drivers"];
|
|
512
|
+
const order: readonly BudgetGroup[] = ["core", "exports", "plugins", "drivers"];
|
|
499
513
|
for (const group of order) {
|
|
500
514
|
const rows = snapshot.budgets.filter((b) => b.group === group);
|
|
501
515
|
if (rows.length === 0) continue;
|
|
@@ -511,6 +525,28 @@ export function formatBudgetsReport(snapshot: BudgetsSnapshot): string {
|
|
|
511
525
|
return `${lines.join("\n")}\n`;
|
|
512
526
|
}
|
|
513
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Markdown table with column widths padded so `oxfmt` is a no-op.
|
|
530
|
+
* Compact `|---|` tables get realigned by oxfmt and make Format fail after
|
|
531
|
+
* every `bun run budgets` refresh.
|
|
532
|
+
*
|
|
533
|
+
* @param headers - Column headers (use `""` for an unnamed first column)
|
|
534
|
+
* @param rows - Body cells
|
|
535
|
+
*/
|
|
536
|
+
export function formatMarkdownTable(
|
|
537
|
+
headers: readonly string[],
|
|
538
|
+
rows: readonly (readonly string[])[],
|
|
539
|
+
): string[] {
|
|
540
|
+
const widths = headers.map((header, i) =>
|
|
541
|
+
Math.max(header.length, ...rows.map((row) => (row[i] ?? "").length)),
|
|
542
|
+
);
|
|
543
|
+
const cell = (value: string, i: number): string => value.padEnd(widths[i]!);
|
|
544
|
+
const line = (cells: readonly string[]): string =>
|
|
545
|
+
`| ${cells.map((value, i) => cell(value, i)).join(" | ")} |`;
|
|
546
|
+
const sep = `| ${widths.map((w) => "-".repeat(w)).join(" | ")} |`;
|
|
547
|
+
return [line(headers), sep, ...rows.map((row) => line(row))];
|
|
548
|
+
}
|
|
549
|
+
|
|
514
550
|
/**
|
|
515
551
|
* Full markdown document for [`BUDGETS.md`](../../BUDGETS.md).
|
|
516
552
|
* Written by `bun run budgets` — do not edit the tables by hand.
|
|
@@ -525,24 +561,51 @@ export function formatBudgetsMarkdown(snapshot: BudgetsSnapshot): string {
|
|
|
525
561
|
"",
|
|
526
562
|
`_okengine v${snapshot.version} · measured ${snapshot.measuredAt}_`,
|
|
527
563
|
"",
|
|
528
|
-
"Core rows are absolute AGENTS caps. Exports and Drivers fail on regression vs the prior [`budgets.json`](budgets.json) (max +256 B or +2%). Export gzip excludes peers/optionals (`zod`, `sently`, `oxc-parser`, `ajv`).",
|
|
564
|
+
"Core rows are absolute AGENTS caps. Exports, Plugins, and Drivers fail on regression vs the prior [`budgets.json`](budgets.json) (max +256 B or +2%). Export gzip excludes peers/optionals (`zod`, `sently`, `oxc-parser`, `ajv`).",
|
|
529
565
|
];
|
|
530
|
-
const order: readonly BudgetGroup[] = ["core", "exports", "drivers"];
|
|
566
|
+
const order: readonly BudgetGroup[] = ["core", "exports", "plugins", "drivers"];
|
|
531
567
|
for (const group of order) {
|
|
532
568
|
const rows = snapshot.budgets.filter((b) => b.group === group);
|
|
533
569
|
if (rows.length === 0) continue;
|
|
534
570
|
const limitCol = group === "core" ? "Limit" : "Ceiling";
|
|
535
571
|
lines.push("");
|
|
536
572
|
lines.push(`## ${GROUP_HEADINGS[group]}`);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
573
|
+
if (group === "plugins") {
|
|
574
|
+
for (const category of PLUGIN_BUDGET_CATEGORIES) {
|
|
575
|
+
const names = new Set(
|
|
576
|
+
OFFICIAL_PLUGIN_BUDGETS.filter((p) => p.category === category).map((p) => p.name),
|
|
577
|
+
);
|
|
578
|
+
const catRows = rows.filter((b) => names.has(b.label));
|
|
579
|
+
if (catRows.length === 0) continue;
|
|
580
|
+
lines.push("");
|
|
581
|
+
lines.push(`### ${PLUGIN_CATEGORY_HEADINGS[category] ?? category}`);
|
|
582
|
+
lines.push("");
|
|
583
|
+
lines.push(
|
|
584
|
+
...formatMarkdownTable(
|
|
585
|
+
["", "Measured", limitCol],
|
|
586
|
+
catRows.map((b) => {
|
|
587
|
+
const mark = b.ok ? "" : " **FAIL**";
|
|
588
|
+
return [
|
|
589
|
+
`${b.label}${mark}`,
|
|
590
|
+
formatValue(b.value, b.unit),
|
|
591
|
+
formatValue(b.limit, b.unit),
|
|
592
|
+
];
|
|
593
|
+
}),
|
|
594
|
+
),
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
continue;
|
|
545
598
|
}
|
|
599
|
+
lines.push("");
|
|
600
|
+
lines.push(
|
|
601
|
+
...formatMarkdownTable(
|
|
602
|
+
["", "Measured", limitCol],
|
|
603
|
+
rows.map((b) => {
|
|
604
|
+
const mark = b.ok ? "" : " **FAIL**";
|
|
605
|
+
return [`${b.label}${mark}`, formatValue(b.value, b.unit), formatValue(b.limit, b.unit)];
|
|
606
|
+
}),
|
|
607
|
+
),
|
|
608
|
+
);
|
|
546
609
|
}
|
|
547
610
|
return `${lines.join("\n")}\n`;
|
|
548
611
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official `okengine/plugins` named exports measured for CI budgets.
|
|
3
|
+
* Categories match `site/content/docs/plugins/index.mdx`.
|
|
4
|
+
* Keep in sync with the docs catalogue — landing + budgets both read this.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Docs / landing category for an official plugin. */
|
|
8
|
+
export type PluginBudgetCategory = "auth" | "security" | "ops" | "perf";
|
|
9
|
+
|
|
10
|
+
/** One official plugin entry measured as `export:./plugins/<name>`. */
|
|
11
|
+
export type OfficialPluginBudget = {
|
|
12
|
+
/** Named export from `okengine/plugins`. */
|
|
13
|
+
readonly name: string;
|
|
14
|
+
/** Basename under `src/plugins/` (e.g. `magic-link.ts`). */
|
|
15
|
+
readonly file: string;
|
|
16
|
+
/** Docs category. */
|
|
17
|
+
readonly category: PluginBudgetCategory;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Category display order (docs index). */
|
|
21
|
+
export const PLUGIN_BUDGET_CATEGORIES: readonly PluginBudgetCategory[] = [
|
|
22
|
+
"auth",
|
|
23
|
+
"security",
|
|
24
|
+
"ops",
|
|
25
|
+
"perf",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Canonical official plugins — membership, file mapping, and grouping.
|
|
30
|
+
* Order within a category is docs order; landing may re-sort by size.
|
|
31
|
+
*/
|
|
32
|
+
export const OFFICIAL_PLUGIN_BUDGETS: readonly OfficialPluginBudget[] = [
|
|
33
|
+
{ name: "username", file: "username.ts", category: "auth" },
|
|
34
|
+
{ name: "anonymous", file: "anonymous.ts", category: "auth" },
|
|
35
|
+
{ name: "magicLink", file: "magic-link.ts", category: "auth" },
|
|
36
|
+
{ name: "emailOtp", file: "email-otp.ts", category: "auth" },
|
|
37
|
+
{ name: "phoneNumber", file: "phone-number.ts", category: "auth" },
|
|
38
|
+
{ name: "twoFactor", file: "two-factor.ts", category: "auth" },
|
|
39
|
+
{ name: "passkey", file: "passkey.ts", category: "auth" },
|
|
40
|
+
{ name: "headers", file: "headers.ts", category: "security" },
|
|
41
|
+
{ name: "cors", file: "cors.ts", category: "security" },
|
|
42
|
+
{ name: "csrf", file: "csrf.ts", category: "security" },
|
|
43
|
+
{ name: "ipAllowlist", file: "ip-allowlist.ts", category: "security" },
|
|
44
|
+
{ name: "maintenanceMode", file: "maintenance-mode.ts", category: "ops" },
|
|
45
|
+
{ name: "compression", file: "compression.ts", category: "perf" },
|
|
46
|
+
] as const;
|
|
@@ -36,6 +36,26 @@ describe("budgets markdown", () => {
|
|
|
36
36
|
group: "exports",
|
|
37
37
|
ok: true,
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
id: "export:./plugins/cors",
|
|
41
|
+
label: "cors",
|
|
42
|
+
value: 2048,
|
|
43
|
+
limit: 2304,
|
|
44
|
+
unit: "bytes",
|
|
45
|
+
gate: "regression",
|
|
46
|
+
group: "plugins",
|
|
47
|
+
ok: true,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "export:./plugins/username",
|
|
51
|
+
label: "username",
|
|
52
|
+
value: 3072,
|
|
53
|
+
limit: 3328,
|
|
54
|
+
unit: "bytes",
|
|
55
|
+
gate: "regression",
|
|
56
|
+
group: "plugins",
|
|
57
|
+
ok: true,
|
|
58
|
+
},
|
|
39
59
|
{
|
|
40
60
|
id: "export:./drivers/postgres",
|
|
41
61
|
label: "postgres",
|
|
@@ -52,10 +72,18 @@ describe("budgets markdown", () => {
|
|
|
52
72
|
expect(md.startsWith("# Budgets\n")).toBe(true);
|
|
53
73
|
expect(md).toContain("## Core");
|
|
54
74
|
expect(md).toContain("## Exports");
|
|
75
|
+
expect(md).toContain("## Plugins");
|
|
76
|
+
expect(md).toContain("### Auth");
|
|
77
|
+
expect(md).toContain("### Security");
|
|
55
78
|
expect(md).toContain("## Drivers");
|
|
56
|
-
expect(md).
|
|
57
|
-
expect(md).
|
|
79
|
+
expect(md).toMatch(/\|\s*channel\s*\|/);
|
|
80
|
+
expect(md).toMatch(/\|\s*cors\s*\|/);
|
|
81
|
+
expect(md).toMatch(/\|\s*username\s*\|/);
|
|
82
|
+
expect(md).toMatch(/\|\s*postgres\s*\|/);
|
|
58
83
|
expect(md).toContain("[`budgets.json`](budgets.json)");
|
|
59
84
|
expect(md).not.toContain("./channel");
|
|
85
|
+
// oxfmt-aligned separators (not compact `|---|---|---|`) so Format stays green
|
|
86
|
+
expect(md).not.toContain("|---|---|---|");
|
|
87
|
+
expect(md).toMatch(/\| -{3,} \| -{3,} \| -{3,} \|/);
|
|
60
88
|
});
|
|
61
89
|
});
|
|
@@ -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
|
/**
|