okengine 0.9.1 → 0.10.1
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 +1 -1
- package/manifest.v1.schema.json +9 -1
- package/package.json +3 -3
- package/site/content/docs/console/runs.mdx +10 -0
- package/site/content/docs/console/traces.mdx +4 -0
- package/site/content/docs/elements/ai.mdx +54 -32
- package/site/content/docs/elements/channel.mdx +97 -9
- package/site/content/docs/elements/clock.mdx +1 -0
- package/site/content/docs/elements/flow.mdx +42 -8
- package/site/content/docs/get-started/basic-usage.mdx +5 -1
- package/site/content/docs/get-started/installation.mdx +9 -5
- package/site/content/docs/meta.json +2 -0
- package/site/content/docs/providers/azure-redis.mdx +85 -0
- package/site/content/docs/providers/cockroachdb.mdx +86 -0
- package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
- package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
- package/site/content/docs/providers/elasticache.mdx +92 -0
- package/site/content/docs/providers/index.mdx +101 -0
- package/site/content/docs/providers/memorystore.mdx +89 -0
- package/site/content/docs/providers/meta.json +20 -0
- package/site/content/docs/providers/neon.mdx +115 -0
- package/site/content/docs/providers/redis-cloud.mdx +93 -0
- package/site/content/docs/providers/supabase.mdx +117 -0
- package/site/content/docs/providers/upstash.mdx +91 -0
- package/site/content/docs/providers/yugabytedb.mdx +95 -0
- package/site/content/docs/recipes/caddy.mdx +128 -0
- package/site/content/docs/recipes/cockroachdb.mdx +127 -0
- package/site/content/docs/recipes/dragonfly.mdx +125 -0
- package/site/content/docs/recipes/index.mdx +146 -0
- package/site/content/docs/recipes/llama-cpp.mdx +153 -0
- package/site/content/docs/recipes/mailpit.mdx +136 -0
- package/site/content/docs/recipes/meilisearch.mdx +139 -0
- package/site/content/docs/recipes/meta.json +31 -0
- package/site/content/docs/recipes/ollama.mdx +142 -0
- package/site/content/docs/recipes/openbao.mdx +141 -0
- package/site/content/docs/recipes/pgdog.mdx +137 -0
- package/site/content/docs/recipes/postgres.mdx +136 -0
- package/site/content/docs/recipes/redis.mdx +130 -0
- package/site/content/docs/recipes/rustfs.mdx +148 -0
- package/site/content/docs/recipes/sglang.mdx +105 -0
- package/site/content/docs/recipes/supabase-docker.mdx +145 -0
- package/site/content/docs/recipes/timescale.mdx +129 -0
- package/site/content/docs/recipes/traefik.mdx +136 -0
- package/site/content/docs/recipes/valkey.mdx +121 -0
- package/site/content/docs/recipes/vllm.mdx +106 -0
- package/site/content/docs/recipes/yugabytedb.mdx +135 -0
- package/site/content/docs/reference/cli.md +42 -6
- package/site/content/docs/reference/configuration.mdx +5 -3
- package/site/content/docs/reference/environment-variables.mdx +12 -12
- package/site/content/docs/reference/fx.mdx +38 -4
- package/src/cli/ai-setup/ai-setup.test.ts +32 -12
- package/src/cli/ai-setup/apply.ts +65 -23
- package/src/cli/ai-setup/catalog.ts +1316 -35
- package/src/cli/ai-setup/detect-ollama.ts +47 -0
- package/src/cli/ai-setup/index.ts +44 -7
- package/src/cli/ai-setup/prompts.ts +430 -486
- package/src/cli/ai-setup/recommend.test.ts +150 -50
- package/src/cli/ai-setup/recommend.ts +118 -101
- package/src/cli/ask-seed.test.ts +68 -0
- package/src/cli/ask-seed.ts +79 -0
- package/src/cli/db-seed-redact.test.ts +25 -0
- package/src/cli/db-seed.ts +36 -7
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +2 -2
- package/src/cli/dev-app-runner.ts +6 -0
- package/src/cli/dev-controls.test.ts +107 -0
- package/src/cli/dev-controls.ts +354 -0
- package/src/cli/dev-schema-sync.ts +12 -4
- package/src/cli/dev.test.ts +50 -30
- package/src/cli/dev.ts +633 -66
- package/src/cli/docker-cli.test.ts +15 -0
- package/src/cli/drizzle-env.test.ts +40 -6
- package/src/cli/drizzle-env.ts +22 -1
- package/src/cli/ensure-drizzle-config.ts +6 -4
- package/src/cli/hero-meta.test.ts +13 -1
- package/src/cli/hero-meta.ts +28 -6
- package/src/cli/index.ts +5 -0
- package/src/cli/registry.ts +24 -0
- package/src/cli/replay.test.ts +105 -0
- package/src/cli/replay.ts +206 -0
- package/src/cli/start.ts +12 -1
- package/src/compiler/effects-infer.ts +37 -8
- package/src/compiler/extract.test.ts +125 -0
- package/src/compiler/extract.ts +31 -6
- package/src/config/index.ts +3 -3
- package/src/console/server/claim.ts +3 -1
- package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
- package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/overview/slo.ts +31 -3
- package/src/console/ui/overview/types.ts +6 -0
- package/src/console/ui/runs/errors.test.ts +53 -0
- package/src/console/ui/runs/errors.ts +88 -0
- package/src/console/ui/runs/index.ts +8 -0
- package/src/console/ui/runs/search.ts +16 -0
- package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
- package/src/docker/ai-model-status.test.ts +101 -0
- package/src/docker/ai-model-status.ts +276 -0
- package/src/docker/compose-health.test.ts +115 -0
- package/src/docker/compose-health.ts +234 -0
- package/src/docker/compose.ts +24 -8
- package/src/docker/derive.ts +35 -9
- package/src/docker/docker.test.ts +304 -12
- package/src/docker/helpers.ts +56 -0
- package/src/docker/index.ts +52 -0
- package/src/docker/ollama-pull.ts +232 -0
- package/src/docker/recipes/cockroach.ts +24 -0
- package/src/docker/recipes/index.ts +32 -0
- package/src/docker/recipes/llama-cpp.ts +273 -0
- package/src/docker/recipes/ollama.ts +19 -23
- package/src/docker/recipes/pgdog.ts +4 -1
- package/src/docker/recipes/postgres.ts +5 -18
- package/src/docker/recipes/sglang.ts +55 -0
- package/src/docker/recipes/supabase.ts +21 -0
- package/src/docker/recipes/timescale.ts +18 -0
- package/src/docker/recipes/vllm.ts +44 -0
- package/src/docker/recipes/yugabyte.ts +23 -0
- package/src/docker/stack-id.test.ts +22 -0
- package/src/docker/stack-id.ts +38 -4
- package/src/docker/types.ts +15 -0
- package/src/drivers/ai-ollama.integration.test.ts +7 -4
- package/src/drivers/signal-engine.ts +10 -0
- package/src/drivers/signal-postgres.ts +24 -2
- package/src/drivers/signal-types.ts +10 -0
- package/src/elements/channel/fallback.test.ts +91 -0
- package/src/elements/channel/hard-bounce.test.ts +82 -0
- package/src/elements/channel/injection.test.ts +153 -0
- package/src/elements/channel/locale-ar.test.ts +102 -0
- package/src/elements/channel/otp-delivery.ts +5 -2
- package/src/elements/channel/receipts-ledger.test.ts +100 -0
- package/src/elements/channel/runtime.ts +24 -1
- package/src/elements/channel/test-helpers.ts +60 -0
- package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
- package/src/elements/store/cache.test.ts +28 -0
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/emit-drizzle.ts +43 -9
- package/src/elements/store/runtime.ts +1 -1
- package/src/elements/store/schema-decl.test.ts +2 -0
- package/src/elements/store/schema-decl.ts +15 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/index.ts +1 -0
- package/src/kernel/app.ts +131 -10
- package/src/kernel/boot-bind/signal.ts +6 -2
- package/src/kernel/boot-bind/store.ts +2 -1
- package/src/kernel/boot.test.ts +6 -5
- package/src/kernel/boot.ts +113 -10
- package/src/kernel/compensate.test.ts +131 -0
- package/src/kernel/correlation.test.ts +108 -0
- package/src/kernel/effect-timing.test.ts +82 -0
- package/src/kernel/effects-stamping.test.ts +207 -0
- package/src/kernel/errors.ts +11 -0
- package/src/kernel/flow.ts +38 -0
- package/src/kernel/fx-runs.test.ts +49 -0
- package/src/kernel/fx.test.ts +14 -0
- package/src/kernel/fx.ts +153 -14
- package/src/manifest/sql-resource.ts +41 -0
- package/src/manifest/types.ts +8 -2
- package/src/runs/collect.ts +3 -0
- package/src/runs/export-otlp.ts +64 -0
- package/src/runs/index.ts +16 -0
- package/src/runs/parquet.ts +9 -0
- package/src/runs/types.ts +5 -0
- package/src/runs/window.test.ts +68 -0
- package/src/runs/window.ts +148 -0
- package/src/runtime/boot-warn.test.ts +45 -0
- package/src/runtime/boot-warn.ts +17 -0
- package/src/runtime/dev-request-log.ts +5 -5
- package/src/term.test.ts +95 -10
- package/src/term.ts +369 -14
- package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
package/src/term.test.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
6
|
import {
|
|
7
7
|
formatAppReadyLine,
|
|
8
|
+
formatBootWarn,
|
|
8
9
|
formatClaimNote,
|
|
10
|
+
formatCliChrome,
|
|
9
11
|
formatDevBanner,
|
|
10
12
|
formatDevHero,
|
|
11
13
|
formatDevLogSeparator,
|
|
@@ -13,6 +15,11 @@ import {
|
|
|
13
15
|
formatServiceLine,
|
|
14
16
|
formatRequestLine,
|
|
15
17
|
formatStackSummary,
|
|
18
|
+
countTermLines,
|
|
19
|
+
createAnchoredBoard,
|
|
20
|
+
createBootProgress,
|
|
21
|
+
createRewritableBlock,
|
|
22
|
+
formatStatusDot,
|
|
16
23
|
formatStatusLine,
|
|
17
24
|
termStyle,
|
|
18
25
|
} from "./term.ts";
|
|
@@ -38,9 +45,10 @@ describe("term", () => {
|
|
|
38
45
|
runtimeEnv: "local",
|
|
39
46
|
system: "darwin 25.4.0 · bun 1.3.14",
|
|
40
47
|
elements: [
|
|
41
|
-
{ element: "flow", detail: "
|
|
42
|
-
{ element: "store", detail: "sql postgres · kv redis" },
|
|
43
|
-
{ element: "signal", detail: "memory" },
|
|
48
|
+
{ element: "flow", detail: "", status: "ready" },
|
|
49
|
+
{ element: "store", detail: "sql postgres · kv redis", status: "ready" },
|
|
50
|
+
{ element: "signal", detail: "memory", status: "ready" },
|
|
51
|
+
{ element: "ai", detail: "openai-compatible · smollm2", status: "pending" },
|
|
44
52
|
],
|
|
45
53
|
});
|
|
46
54
|
expect(out).toContain("oke dev v0.2.4");
|
|
@@ -53,11 +61,65 @@ describe("term", () => {
|
|
|
53
61
|
expect(out).toContain("elements");
|
|
54
62
|
expect(out).toContain("store");
|
|
55
63
|
expect(out).toContain("postgres");
|
|
64
|
+
expect(out).toContain("● flow");
|
|
65
|
+
expect(out).toContain("● ai");
|
|
56
66
|
expect(out).not.toContain("on(Trigger)");
|
|
57
67
|
expect(out).not.toContain("O·K·E");
|
|
58
68
|
expect(out).not.toMatch(/\u001b\[/);
|
|
59
69
|
});
|
|
60
70
|
|
|
71
|
+
test("formatStatusDot colors by status", () => {
|
|
72
|
+
const s = termStyle(true);
|
|
73
|
+
expect(formatStatusDot("ready", true)).toBe(`${s.green}●${s.reset}`);
|
|
74
|
+
expect(formatStatusDot("pending", true)).toBe(`${s.yellow}●${s.reset}`);
|
|
75
|
+
expect(formatStatusDot("error", true)).toBe(`${s.red}●${s.reset}`);
|
|
76
|
+
expect(formatStatusDot("idle", true)).toBe(`${s.dim}●${s.reset}`);
|
|
77
|
+
expect(formatStatusDot("ready", false)).toBe("●");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("createRewritableBlock appends when disabled", () => {
|
|
81
|
+
let out = "";
|
|
82
|
+
const block = createRewritableBlock((t) => {
|
|
83
|
+
out += t;
|
|
84
|
+
}, false);
|
|
85
|
+
block.paint("a\nb\n");
|
|
86
|
+
block.paint("c\n");
|
|
87
|
+
expect(out).toBe("a\nb\nc\n");
|
|
88
|
+
expect(countTermLines("a\nb\n")).toBe(2);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("createBootProgress replaces keyed rows then clears", () => {
|
|
92
|
+
let out = "";
|
|
93
|
+
const progress = createBootProgress((t) => {
|
|
94
|
+
out += t;
|
|
95
|
+
}, false);
|
|
96
|
+
progress.set("compose", "compose…\n");
|
|
97
|
+
progress.set("compose", "compose ok\n");
|
|
98
|
+
progress.set("vault", "vault ok\n");
|
|
99
|
+
// Non-TTY appends each set (including replacements).
|
|
100
|
+
expect(out).toContain("compose ok\n");
|
|
101
|
+
expect(out).toContain("vault ok\n");
|
|
102
|
+
progress.clear();
|
|
103
|
+
// clear is a no-op on the stream when rewrite is disabled
|
|
104
|
+
expect(out).toContain("vault ok\n");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("createAnchoredBoard wrapWrite tracks below output", () => {
|
|
108
|
+
let out = "";
|
|
109
|
+
const board = createAnchoredBoard((t) => {
|
|
110
|
+
out += t;
|
|
111
|
+
}, false);
|
|
112
|
+
board.paint("board\n");
|
|
113
|
+
const w = board.wrapWrite((t) => {
|
|
114
|
+
out += t;
|
|
115
|
+
});
|
|
116
|
+
w("below\n");
|
|
117
|
+
board.paint("board2\n"); // non-TTY: no rewrite append
|
|
118
|
+
expect(out).toContain("board\n");
|
|
119
|
+
expect(out).toContain("below\n");
|
|
120
|
+
board.stop();
|
|
121
|
+
});
|
|
122
|
+
|
|
61
123
|
test("formatServiceLine / formatAppReadyLine include URL", () => {
|
|
62
124
|
expect(formatServiceLine("Console", "http://127.0.0.1:6533", false)).toContain(
|
|
63
125
|
"http://127.0.0.1:6533",
|
|
@@ -65,7 +127,7 @@ describe("term", () => {
|
|
|
65
127
|
expect(formatAppReadyLine("http://127.0.0.1:6530", false)).toContain("http://127.0.0.1:6530");
|
|
66
128
|
});
|
|
67
129
|
|
|
68
|
-
test("formatDevHero keeps
|
|
130
|
+
test("formatDevHero keeps Backend Console MCP URLs", () => {
|
|
69
131
|
const out = formatDevHero({
|
|
70
132
|
appUrl: "http://127.0.0.1:6530",
|
|
71
133
|
consoleUrl: "http://127.0.0.1:6533",
|
|
@@ -73,12 +135,12 @@ describe("term", () => {
|
|
|
73
135
|
profile: "local",
|
|
74
136
|
runtimeEnv: "local",
|
|
75
137
|
system: "darwin 25.4.0 · bun 1.3.14",
|
|
76
|
-
elements: [{ element: "flow", detail: "
|
|
138
|
+
elements: [{ element: "flow", detail: "", status: "ready" }],
|
|
77
139
|
color: false,
|
|
78
140
|
});
|
|
79
141
|
expect(out).toContain("oke dev");
|
|
80
142
|
expect(out).toContain("██");
|
|
81
|
-
expect(out).toContain("
|
|
143
|
+
expect(out).toContain("Backend");
|
|
82
144
|
expect(out).toContain("Console");
|
|
83
145
|
expect(out).toContain("MCP");
|
|
84
146
|
expect(out).toContain("http://127.0.0.1:6530");
|
|
@@ -106,10 +168,27 @@ describe("term", () => {
|
|
|
106
168
|
expect(formatStatusLine("stack up (sql)", false)).toContain("stack up");
|
|
107
169
|
});
|
|
108
170
|
|
|
171
|
+
test("formatBootWarn wraps under Notice chrome", () => {
|
|
172
|
+
const out = formatBootWarn(
|
|
173
|
+
"oke boot: Channel suppression defaults to process-local memory for multi-instance",
|
|
174
|
+
false,
|
|
175
|
+
);
|
|
176
|
+
expect(out).toContain("Notice");
|
|
177
|
+
expect(out).toContain("process-local");
|
|
178
|
+
expect(out).not.toContain("oke boot:");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("formatCliChrome routes boot and status lines", () => {
|
|
182
|
+
const out = formatCliChrome("oke db seed: ok\noke boot: hello world\n", false);
|
|
183
|
+
expect(out).toContain("oke db seed: ok");
|
|
184
|
+
expect(out).toContain("Notice");
|
|
185
|
+
expect(out).toContain("hello world");
|
|
186
|
+
});
|
|
187
|
+
|
|
109
188
|
test("formatRequestLine shows date, time, surface, flow, ms, status", () => {
|
|
110
189
|
const at = new Date(2026, 6, 26, 3, 11, 42);
|
|
111
190
|
const out = formatRequestLine({
|
|
112
|
-
surface: "
|
|
191
|
+
surface: "Backend",
|
|
113
192
|
method: "GET",
|
|
114
193
|
path: "/health",
|
|
115
194
|
flow: "main.health",
|
|
@@ -120,7 +199,7 @@ describe("term", () => {
|
|
|
120
199
|
});
|
|
121
200
|
expect(out).toContain("2026-07-26");
|
|
122
201
|
expect(out).toContain("03:11:42");
|
|
123
|
-
expect(out).toContain("
|
|
202
|
+
expect(out).toContain("Backend");
|
|
124
203
|
expect(out).toContain("200");
|
|
125
204
|
expect(out).not.toMatch(/\u001b\[/);
|
|
126
205
|
});
|
|
@@ -144,14 +223,20 @@ describe("term", () => {
|
|
|
144
223
|
const out = formatStackSummary({
|
|
145
224
|
project: "oke-dev-a3f791",
|
|
146
225
|
services: [
|
|
147
|
-
{ label: "postgres", hostPort: 15975 },
|
|
148
|
-
{ label: "redis", hostPort: 16975 },
|
|
226
|
+
{ label: "postgres", hostPort: 15975, status: "ready" },
|
|
227
|
+
{ label: "redis", hostPort: 16975, status: "ready" },
|
|
228
|
+
{ label: "ai", hostPort: 23975, detail: "gemma4:e4b-q4_K_M", status: "pending" },
|
|
149
229
|
],
|
|
150
230
|
appDrivers: ["postgres", "redis"],
|
|
151
231
|
color: false,
|
|
152
232
|
});
|
|
153
233
|
expect(out).toContain("Docker");
|
|
154
234
|
expect(out).toContain(":15975");
|
|
235
|
+
expect(out).toContain("gemma4:e4b-q4_K_M");
|
|
236
|
+
expect(out).toContain("● postgres");
|
|
237
|
+
expect(out).toContain("● ai");
|
|
238
|
+
// Blank │ row separates the Docker header from whatever sits above.
|
|
239
|
+
expect(out).toMatch(/│\n◇ {2}Docker/);
|
|
155
240
|
expect(out).not.toMatch(/\u001b\[/);
|
|
156
241
|
});
|
|
157
242
|
});
|
package/src/term.ts
CHANGED
|
@@ -60,10 +60,56 @@ export function termStyle(color: boolean = termColorEnabled()): TermStyle {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Readiness for hero / Docker status dots.
|
|
65
|
+
*
|
|
66
|
+
* - `ready` — green ●
|
|
67
|
+
* - `pending` — yellow ● (starting / loading)
|
|
68
|
+
* - `error` — red ●
|
|
69
|
+
* - `idle` — dim ● (unbound)
|
|
70
|
+
*/
|
|
71
|
+
export type DevStatus = "ready" | "pending" | "error" | "idle";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Colored ● for {@link DevStatus}.
|
|
75
|
+
*
|
|
76
|
+
* @param status - Readiness
|
|
77
|
+
* @param color - Color on/off
|
|
78
|
+
*/
|
|
79
|
+
export function formatStatusDot(status: DevStatus, color: boolean = termColorEnabled()): string {
|
|
80
|
+
const s = termStyle(color);
|
|
81
|
+
switch (status) {
|
|
82
|
+
case "ready":
|
|
83
|
+
return `${s.green}●${s.reset}`;
|
|
84
|
+
case "pending":
|
|
85
|
+
return `${s.yellow}●${s.reset}`;
|
|
86
|
+
case "error":
|
|
87
|
+
return `${s.red}●${s.reset}`;
|
|
88
|
+
case "idle":
|
|
89
|
+
return `${s.dim}●${s.reset}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Map AI model phase → {@link DevStatus}.
|
|
95
|
+
*
|
|
96
|
+
* @param phase - Probe phase string
|
|
97
|
+
*/
|
|
98
|
+
export function devStatusFromAiPhase(
|
|
99
|
+
phase: "unreachable" | "starting" | "loading" | "ready" | "error" | string,
|
|
100
|
+
): DevStatus {
|
|
101
|
+
if (phase === "ready") return "ready";
|
|
102
|
+
if (phase === "error") return "error";
|
|
103
|
+
if (phase === "unreachable" || phase === "starting" || phase === "loading") return "pending";
|
|
104
|
+
return "pending";
|
|
105
|
+
}
|
|
106
|
+
|
|
63
107
|
/** One eight-element row in the hero. */
|
|
64
108
|
export type DevHeroElement = {
|
|
65
109
|
readonly element: string;
|
|
66
110
|
readonly detail: string;
|
|
111
|
+
/** Status dot (default: idle when detail is `—`, else ready). */
|
|
112
|
+
readonly status?: DevStatus;
|
|
67
113
|
};
|
|
68
114
|
|
|
69
115
|
/** Shared options for the `oke dev` hero / banner. */
|
|
@@ -124,10 +170,18 @@ export function formatDevHeroDetails(options: DevHeroMeta = {}): string {
|
|
|
124
170
|
const elements = options.elements ?? [];
|
|
125
171
|
if (elements.length > 0) {
|
|
126
172
|
lines.push(`${bar} ${s.dim}elements${s.reset}`);
|
|
173
|
+
const color = options.color ?? termColorEnabled();
|
|
127
174
|
for (const row of elements) {
|
|
128
|
-
const idle = row.detail === "—";
|
|
129
|
-
const
|
|
130
|
-
|
|
175
|
+
const idle = row.detail === "—" || row.detail === "";
|
|
176
|
+
const status = row.status ?? (idle ? "idle" : "ready");
|
|
177
|
+
const dot = formatStatusDot(status, color);
|
|
178
|
+
const detail = idle
|
|
179
|
+
? row.detail === ""
|
|
180
|
+
? ""
|
|
181
|
+
: `${s.dim}—${s.reset}`
|
|
182
|
+
: `${s.cyan}${row.detail}${s.reset}`;
|
|
183
|
+
const detailPart = detail ? ` ${detail}` : "";
|
|
184
|
+
lines.push(`${bar} ${dot} ${s.dim}${row.element.padEnd(9)}${s.reset}${detailPart}`);
|
|
131
185
|
}
|
|
132
186
|
}
|
|
133
187
|
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
@@ -152,11 +206,220 @@ export function formatDevBanner(options: DevHeroMeta = {}): string {
|
|
|
152
206
|
if (options.watching !== false) {
|
|
153
207
|
lines.push(`${bar} ${s.dim}watching — client types regenerate on save${s.reset}`);
|
|
154
208
|
}
|
|
155
|
-
|
|
209
|
+
const details = formatDevHeroDetails(options).trimEnd();
|
|
210
|
+
if (details) lines.push(details);
|
|
156
211
|
lines.push(bar);
|
|
157
212
|
return `${lines.join("\n")}\n`;
|
|
158
213
|
}
|
|
159
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Count visual lines in a terminal string (trailing newline optional).
|
|
217
|
+
*
|
|
218
|
+
* @param text - Block text
|
|
219
|
+
*/
|
|
220
|
+
export function countTermLines(text: string): number {
|
|
221
|
+
if (text.length === 0) return 0;
|
|
222
|
+
const normalized = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
223
|
+
if (normalized.length === 0) return 0;
|
|
224
|
+
return normalized.split("\n").length;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Handle for a bottom-of-TTY block that can be repainted until frozen. */
|
|
228
|
+
export type RewritableBlock = {
|
|
229
|
+
/** Paint / repaint the block (must still be the last output). */
|
|
230
|
+
readonly paint: (text: string) => void;
|
|
231
|
+
/** Erase the block in place (no-op when frozen or non-TTY). */
|
|
232
|
+
readonly clear: () => void;
|
|
233
|
+
/** Stop rewriting — further {@link paint} calls append instead. */
|
|
234
|
+
readonly freeze: () => void;
|
|
235
|
+
readonly frozen: () => boolean;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Erase `lineCount` lines above the cursor (cursor ends on the first cleared row).
|
|
240
|
+
*
|
|
241
|
+
* @param write - stdout writer
|
|
242
|
+
* @param lineCount - Rows to clear
|
|
243
|
+
*/
|
|
244
|
+
function eraseTermLines(write: (text: string) => void, lineCount: number): void {
|
|
245
|
+
if (lineCount <= 0) return;
|
|
246
|
+
write(`\x1b[${lineCount}A`);
|
|
247
|
+
for (let i = 0; i < lineCount; i++) {
|
|
248
|
+
write("\x1b[2K");
|
|
249
|
+
if (i < lineCount - 1) write("\x1b[1B");
|
|
250
|
+
}
|
|
251
|
+
if (lineCount > 1) write(`\x1b[${lineCount - 1}A`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Bottom-anchored live block for hero elements / Docker summary.
|
|
256
|
+
*
|
|
257
|
+
* Repaints in place with ANSI cursor moves while at the bottom of the TTY.
|
|
258
|
+
* Call {@link RewritableBlock.freeze} before printing anything below it.
|
|
259
|
+
*
|
|
260
|
+
* @param write - stdout writer
|
|
261
|
+
* @param enabled - When false (non-TTY / tests), every paint appends
|
|
262
|
+
*/
|
|
263
|
+
export function createRewritableBlock(
|
|
264
|
+
write: (text: string) => void,
|
|
265
|
+
enabled: boolean = termColorEnabled() && process.stdout.isTTY === true,
|
|
266
|
+
): RewritableBlock {
|
|
267
|
+
let lineCount = 0;
|
|
268
|
+
let frozen = false;
|
|
269
|
+
return {
|
|
270
|
+
paint(text: string) {
|
|
271
|
+
if (text.length === 0) {
|
|
272
|
+
this.clear();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const body = text.endsWith("\n") ? text : `${text}\n`;
|
|
276
|
+
const nextLines = countTermLines(body);
|
|
277
|
+
if (!frozen && enabled && lineCount > 0) {
|
|
278
|
+
eraseTermLines(write, lineCount);
|
|
279
|
+
}
|
|
280
|
+
write(body);
|
|
281
|
+
if (!frozen && enabled) lineCount = nextLines;
|
|
282
|
+
else lineCount = 0;
|
|
283
|
+
},
|
|
284
|
+
clear() {
|
|
285
|
+
if (frozen) return;
|
|
286
|
+
if (enabled && lineCount > 0) eraseTermLines(write, lineCount);
|
|
287
|
+
lineCount = 0;
|
|
288
|
+
},
|
|
289
|
+
freeze() {
|
|
290
|
+
frozen = true;
|
|
291
|
+
lineCount = 0;
|
|
292
|
+
},
|
|
293
|
+
frozen: () => frozen,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Keyed ephemeral boot-progress lines (compose / vault / health / AI). */
|
|
298
|
+
export type BootProgress = {
|
|
299
|
+
/** Set or replace a progress row by key, then repaint. */
|
|
300
|
+
readonly set: (key: string, line: string) => void;
|
|
301
|
+
/** Erase all progress rows (TTY) before the final board. */
|
|
302
|
+
readonly clear: () => void;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Live boot-progress pane — keyed rows replace in place, then {@link BootProgress.clear}.
|
|
307
|
+
*
|
|
308
|
+
* @param write - stdout writer
|
|
309
|
+
* @param enabled - TTY rewrite on/off
|
|
310
|
+
*/
|
|
311
|
+
export function createBootProgress(
|
|
312
|
+
write: (text: string) => void,
|
|
313
|
+
enabled: boolean = termColorEnabled() && process.stdout.isTTY === true,
|
|
314
|
+
): BootProgress {
|
|
315
|
+
const block = createRewritableBlock(write, enabled);
|
|
316
|
+
const order: string[] = [];
|
|
317
|
+
const lines = new Map<string, string>();
|
|
318
|
+
const repaint = (): void => {
|
|
319
|
+
if (order.length === 0) {
|
|
320
|
+
block.clear();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
block.paint(order.map((key) => lines.get(key) ?? "").join(""));
|
|
324
|
+
};
|
|
325
|
+
return {
|
|
326
|
+
set(key: string, line: string) {
|
|
327
|
+
const body = line.endsWith("\n") ? line : `${line}\n`;
|
|
328
|
+
if (!lines.has(key)) order.push(key);
|
|
329
|
+
lines.set(key, body);
|
|
330
|
+
if (!enabled) {
|
|
331
|
+
write(body);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
repaint();
|
|
335
|
+
},
|
|
336
|
+
clear() {
|
|
337
|
+
order.length = 0;
|
|
338
|
+
lines.clear();
|
|
339
|
+
block.clear();
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Live elements/Docker board that can rewrite after content is printed below. */
|
|
345
|
+
export type AnchoredBoard = {
|
|
346
|
+
/** Paint or rewrite the board (same height preferred). */
|
|
347
|
+
readonly paint: (text: string) => void;
|
|
348
|
+
/** Wrap a writer so post-board output is tracked for cursor math. */
|
|
349
|
+
readonly wrapWrite: (inner: (text: string) => void) => (text: string) => void;
|
|
350
|
+
/**
|
|
351
|
+
* Forget board geometry (after a full-screen clear) so the next
|
|
352
|
+
* {@link paint} writes fresh at the cursor.
|
|
353
|
+
*/
|
|
354
|
+
readonly reset: () => void;
|
|
355
|
+
/** Disable further in-place rewrites (session end). */
|
|
356
|
+
readonly stop: () => void;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Anchored status board — rewrites in place even after Backend / Logs lines.
|
|
361
|
+
*
|
|
362
|
+
* Uses saved cursor + line offsets. Soft-reload full-screen clears can desync;
|
|
363
|
+
* later paints then append instead.
|
|
364
|
+
*
|
|
365
|
+
* @param write - stdout writer
|
|
366
|
+
* @param enabled - TTY rewrite on/off
|
|
367
|
+
*/
|
|
368
|
+
export function createAnchoredBoard(
|
|
369
|
+
write: (text: string) => void,
|
|
370
|
+
enabled: boolean = termColorEnabled() && process.stdout.isTTY === true,
|
|
371
|
+
): AnchoredBoard {
|
|
372
|
+
let boardLines = 0;
|
|
373
|
+
let belowLines = 0;
|
|
374
|
+
let stopped = false;
|
|
375
|
+
let lastBody = "";
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
paint(text: string) {
|
|
379
|
+
if (stopped) return;
|
|
380
|
+
const body = text.endsWith("\n") ? text : `${text}\n`;
|
|
381
|
+
const nextLines = countTermLines(body);
|
|
382
|
+
if (!enabled) {
|
|
383
|
+
if (boardLines === 0) write(body);
|
|
384
|
+
boardLines = nextLines;
|
|
385
|
+
lastBody = body;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (boardLines === 0) {
|
|
389
|
+
write(body);
|
|
390
|
+
boardLines = nextLines;
|
|
391
|
+
lastBody = body;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (body === lastBody) return;
|
|
395
|
+
// Move to board start, replace rows, restore cursor.
|
|
396
|
+
write("\x1b[s");
|
|
397
|
+
write(`\x1b[${belowLines + boardLines}A`);
|
|
398
|
+
eraseTermLines(write, boardLines);
|
|
399
|
+
write(body);
|
|
400
|
+
boardLines = nextLines;
|
|
401
|
+
lastBody = body;
|
|
402
|
+
write("\x1b[u");
|
|
403
|
+
},
|
|
404
|
+
wrapWrite(inner: (text: string) => void) {
|
|
405
|
+
return (text: string) => {
|
|
406
|
+
if (!stopped && enabled && boardLines > 0) {
|
|
407
|
+
belowLines += countTermLines(text);
|
|
408
|
+
}
|
|
409
|
+
inner(text);
|
|
410
|
+
};
|
|
411
|
+
},
|
|
412
|
+
reset() {
|
|
413
|
+
boardLines = 0;
|
|
414
|
+
belowLines = 0;
|
|
415
|
+
lastBody = "";
|
|
416
|
+
},
|
|
417
|
+
stop() {
|
|
418
|
+
stopped = true;
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
160
423
|
/**
|
|
161
424
|
* One service URL line (Console / MCP after bind).
|
|
162
425
|
*
|
|
@@ -175,14 +438,14 @@ export function formatServiceLine(
|
|
|
175
438
|
}
|
|
176
439
|
|
|
177
440
|
/**
|
|
178
|
-
*
|
|
441
|
+
* Backend child ready line (`bun --hot` soft reload).
|
|
179
442
|
*
|
|
180
|
-
* @param url -
|
|
443
|
+
* @param url - Backend base URL
|
|
181
444
|
* @param color - Color on/off
|
|
182
445
|
*/
|
|
183
446
|
export function formatAppReadyLine(url: string, color: boolean = termColorEnabled()): string {
|
|
184
447
|
const s = termStyle(color);
|
|
185
|
-
return `${s.green}●${s.reset} ${s.dim}
|
|
448
|
+
return `${s.green}●${s.reset} ${s.dim}${"Backend".padEnd(7)}${s.reset} ${s.cyan}${url}${s.reset}\n`;
|
|
186
449
|
}
|
|
187
450
|
|
|
188
451
|
/**
|
|
@@ -241,10 +504,87 @@ export function formatDevLogSeparator(color: boolean = termColorEnabled()): stri
|
|
|
241
504
|
*
|
|
242
505
|
* @param message - Status text
|
|
243
506
|
* @param color - Color on/off
|
|
507
|
+
* @param status - Optional colored ● prefix
|
|
508
|
+
*/
|
|
509
|
+
export function formatStatusLine(
|
|
510
|
+
message: string,
|
|
511
|
+
color: boolean = termColorEnabled(),
|
|
512
|
+
status?: DevStatus,
|
|
513
|
+
): string {
|
|
514
|
+
const s = termStyle(color);
|
|
515
|
+
const dot = status ? `${formatStatusDot(status, color)} ` : "";
|
|
516
|
+
return `${s.dim}│${s.reset} ${dot}${s.dim}${message}${s.reset}\n`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Wrap a long message to `width` columns (word-aware).
|
|
521
|
+
*
|
|
522
|
+
* @param text - Source text
|
|
523
|
+
* @param width - Max columns per line
|
|
524
|
+
*/
|
|
525
|
+
function wrapWords(text: string, width: number): string[] {
|
|
526
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
527
|
+
if (words.length === 0) return [];
|
|
528
|
+
const lines: string[] = [];
|
|
529
|
+
let cur = "";
|
|
530
|
+
for (const word of words) {
|
|
531
|
+
if (cur.length === 0) {
|
|
532
|
+
cur = word;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (`${cur} ${word}`.length <= width) {
|
|
536
|
+
cur = `${cur} ${word}`;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
lines.push(cur);
|
|
540
|
+
cur = word;
|
|
541
|
+
}
|
|
542
|
+
if (cur.length > 0) lines.push(cur);
|
|
543
|
+
return lines;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Boot honesty notice — Clack-like column under the `oke dev` hero.
|
|
548
|
+
*
|
|
549
|
+
* @param message - Body (with or without `oke boot:` prefix)
|
|
550
|
+
* @param color - Color on/off
|
|
244
551
|
*/
|
|
245
|
-
export function
|
|
552
|
+
export function formatBootWarn(message: string, color: boolean = termColorEnabled()): string {
|
|
246
553
|
const s = termStyle(color);
|
|
247
|
-
|
|
554
|
+
const bar = `${s.dim}│${s.reset}`;
|
|
555
|
+
const body = message.replace(/^oke boot:\s*/i, "").trim();
|
|
556
|
+
const width = Math.max(40, Math.min(72, (process.stdout.columns ?? 80) - 6));
|
|
557
|
+
const wrapped = wrapWords(body, width);
|
|
558
|
+
const lines = [`${s.yellow}◇${s.reset} ${s.dim}Notice${s.reset}`];
|
|
559
|
+
for (const line of wrapped) {
|
|
560
|
+
lines.push(`${bar} ${s.dim}${line}${s.reset}`);
|
|
561
|
+
}
|
|
562
|
+
lines.push(bar);
|
|
563
|
+
return `${lines.join("\n")}\n`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Route raw CLI / boot lines into hero chrome (`│ …` or Notice boxes).
|
|
568
|
+
*
|
|
569
|
+
* @param text - Chunk that may contain newlines
|
|
570
|
+
* @param color - Color on/off
|
|
571
|
+
*/
|
|
572
|
+
export function formatCliChrome(text: string, color: boolean = termColorEnabled()): string {
|
|
573
|
+
const parts = text.split("\n");
|
|
574
|
+
const out: string[] = [];
|
|
575
|
+
for (let i = 0; i < parts.length; i++) {
|
|
576
|
+
const line = parts[i]!;
|
|
577
|
+
const isLastEmpty = i === parts.length - 1 && line.length === 0;
|
|
578
|
+
if (isLastEmpty) continue;
|
|
579
|
+
const trimmed = line.trimEnd();
|
|
580
|
+
if (trimmed.length === 0) continue;
|
|
581
|
+
if (/^oke boot:/i.test(trimmed)) {
|
|
582
|
+
out.push(formatBootWarn(trimmed, color));
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
out.push(formatStatusLine(trimmed, color));
|
|
586
|
+
}
|
|
587
|
+
return out.join("");
|
|
248
588
|
}
|
|
249
589
|
|
|
250
590
|
/** One infra service row for {@link formatStackSummary}. */
|
|
@@ -253,10 +593,16 @@ export type StackSummaryService = {
|
|
|
253
593
|
readonly label: string;
|
|
254
594
|
/** Published host port. */
|
|
255
595
|
readonly hostPort: number;
|
|
596
|
+
/** Optional trailing detail (e.g. AI model id). */
|
|
597
|
+
readonly detail?: string;
|
|
598
|
+
/** Status dot (default `pending` until probed). */
|
|
599
|
+
readonly status?: DevStatus;
|
|
600
|
+
/** Compose service name for health lookup (`ai`, `store-sql`). */
|
|
601
|
+
readonly serviceName?: string;
|
|
256
602
|
};
|
|
257
603
|
|
|
258
604
|
/** Surfaces that emit request lines during `oke dev`. */
|
|
259
|
-
export type DevLogSurface = "
|
|
605
|
+
export type DevLogSurface = "Backend" | "Console" | "MCP";
|
|
260
606
|
|
|
261
607
|
/**
|
|
262
608
|
* Local calendar date `YYYY-MM-DD` (no time).
|
|
@@ -286,7 +632,7 @@ export function formatDevLogTime(at: Date = new Date()): string {
|
|
|
286
632
|
* One HTTP/RPC request line for the `oke dev` TTY.
|
|
287
633
|
*
|
|
288
634
|
* @example
|
|
289
|
-
* `●
|
|
635
|
+
* `● Backend GET /health main.health 12ms 200 2026-07-26 03:11:42`
|
|
290
636
|
*
|
|
291
637
|
* @param options - Surface, method, path, flow, timing, status
|
|
292
638
|
*/
|
|
@@ -308,7 +654,7 @@ export function formatRequestLine(options: {
|
|
|
308
654
|
const date = formatDevLogDate(at);
|
|
309
655
|
const time = formatDevLogTime(at);
|
|
310
656
|
const surfaceColor =
|
|
311
|
-
options.surface === "
|
|
657
|
+
options.surface === "Backend" ? s.green : options.surface === "Console" ? s.magenta : s.cyan;
|
|
312
658
|
const statusColor = options.status >= 500 ? s.red : options.status >= 400 ? s.yellow : s.green;
|
|
313
659
|
const methodRaw = options.method.toUpperCase();
|
|
314
660
|
const methodColor =
|
|
@@ -356,15 +702,24 @@ export function formatStackSummary(options: {
|
|
|
356
702
|
const s = termStyle(options.color ?? termColorEnabled());
|
|
357
703
|
const bar = `${s.dim}│${s.reset}`;
|
|
358
704
|
const pad = (label: string) => label.padEnd(8);
|
|
705
|
+
const color = options.color ?? termColorEnabled();
|
|
359
706
|
const lines: string[] = [
|
|
707
|
+
`${s.dim}│${s.reset}`,
|
|
360
708
|
`${s.green}◇${s.reset} ${s.bold}Docker${s.reset} ${s.cyan}${options.project}${s.reset}`,
|
|
361
709
|
];
|
|
362
710
|
for (const svc of options.services) {
|
|
363
|
-
|
|
711
|
+
const detail = svc.detail?.trim();
|
|
712
|
+
const status = svc.status ?? "pending";
|
|
713
|
+
const dot = formatStatusDot(status, color);
|
|
714
|
+
lines.push(
|
|
715
|
+
`${bar} ${dot} ${s.dim}${pad(svc.label)}${s.reset} ${s.cyan}:${svc.hostPort}${s.reset}` +
|
|
716
|
+
(detail ? ` ${s.dim}${detail}${s.reset}` : ""),
|
|
717
|
+
);
|
|
364
718
|
}
|
|
365
719
|
const drivers = options.appDrivers ?? [];
|
|
366
720
|
const appDetail = drivers.length > 0 ? `host Bun · ${drivers.join(" + ")}` : "host Bun";
|
|
367
|
-
|
|
721
|
+
const appDot = formatStatusDot("ready", color);
|
|
722
|
+
lines.push(`${bar} ${appDot} ${s.dim}${pad("app")}${s.reset} ${s.dim}${appDetail}${s.reset}`);
|
|
368
723
|
lines.push(bar);
|
|
369
724
|
return `${lines.join("\n")}\n`;
|
|
370
725
|
}
|