okengine 0.9.1 → 0.10.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.
Files changed (166) hide show
  1. package/AGENTS.md +1 -1
  2. package/manifest.v1.schema.json +9 -1
  3. package/package.json +3 -3
  4. package/site/content/docs/console/runs.mdx +10 -0
  5. package/site/content/docs/console/traces.mdx +4 -0
  6. package/site/content/docs/elements/ai.mdx +54 -32
  7. package/site/content/docs/elements/channel.mdx +97 -9
  8. package/site/content/docs/elements/clock.mdx +1 -0
  9. package/site/content/docs/elements/flow.mdx +42 -8
  10. package/site/content/docs/get-started/basic-usage.mdx +5 -1
  11. package/site/content/docs/get-started/installation.mdx +9 -5
  12. package/site/content/docs/meta.json +2 -0
  13. package/site/content/docs/providers/azure-redis.mdx +85 -0
  14. package/site/content/docs/providers/cockroachdb.mdx +86 -0
  15. package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
  16. package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
  17. package/site/content/docs/providers/elasticache.mdx +92 -0
  18. package/site/content/docs/providers/index.mdx +101 -0
  19. package/site/content/docs/providers/memorystore.mdx +89 -0
  20. package/site/content/docs/providers/meta.json +20 -0
  21. package/site/content/docs/providers/neon.mdx +115 -0
  22. package/site/content/docs/providers/redis-cloud.mdx +93 -0
  23. package/site/content/docs/providers/supabase.mdx +117 -0
  24. package/site/content/docs/providers/upstash.mdx +91 -0
  25. package/site/content/docs/providers/yugabytedb.mdx +95 -0
  26. package/site/content/docs/recipes/caddy.mdx +128 -0
  27. package/site/content/docs/recipes/cockroachdb.mdx +127 -0
  28. package/site/content/docs/recipes/dragonfly.mdx +125 -0
  29. package/site/content/docs/recipes/index.mdx +146 -0
  30. package/site/content/docs/recipes/llama-cpp.mdx +153 -0
  31. package/site/content/docs/recipes/mailpit.mdx +136 -0
  32. package/site/content/docs/recipes/meilisearch.mdx +139 -0
  33. package/site/content/docs/recipes/meta.json +31 -0
  34. package/site/content/docs/recipes/ollama.mdx +142 -0
  35. package/site/content/docs/recipes/openbao.mdx +141 -0
  36. package/site/content/docs/recipes/pgdog.mdx +137 -0
  37. package/site/content/docs/recipes/postgres.mdx +136 -0
  38. package/site/content/docs/recipes/redis.mdx +130 -0
  39. package/site/content/docs/recipes/rustfs.mdx +148 -0
  40. package/site/content/docs/recipes/sglang.mdx +105 -0
  41. package/site/content/docs/recipes/supabase-docker.mdx +145 -0
  42. package/site/content/docs/recipes/timescale.mdx +129 -0
  43. package/site/content/docs/recipes/traefik.mdx +136 -0
  44. package/site/content/docs/recipes/valkey.mdx +121 -0
  45. package/site/content/docs/recipes/vllm.mdx +106 -0
  46. package/site/content/docs/recipes/yugabytedb.mdx +135 -0
  47. package/site/content/docs/reference/cli.md +42 -6
  48. package/site/content/docs/reference/configuration.mdx +5 -3
  49. package/site/content/docs/reference/environment-variables.mdx +12 -12
  50. package/site/content/docs/reference/fx.mdx +38 -4
  51. package/src/cli/ai-setup/ai-setup.test.ts +32 -12
  52. package/src/cli/ai-setup/apply.ts +65 -23
  53. package/src/cli/ai-setup/catalog.ts +1316 -35
  54. package/src/cli/ai-setup/detect-ollama.ts +47 -0
  55. package/src/cli/ai-setup/index.ts +44 -7
  56. package/src/cli/ai-setup/prompts.ts +430 -486
  57. package/src/cli/ai-setup/recommend.test.ts +150 -50
  58. package/src/cli/ai-setup/recommend.ts +118 -101
  59. package/src/cli/ask-seed.test.ts +68 -0
  60. package/src/cli/ask-seed.ts +79 -0
  61. package/src/cli/db-seed-redact.test.ts +25 -0
  62. package/src/cli/db-seed.ts +36 -7
  63. package/src/cli/db.test.ts +1 -1
  64. package/src/cli/db.ts +2 -2
  65. package/src/cli/dev-controls.test.ts +107 -0
  66. package/src/cli/dev-controls.ts +354 -0
  67. package/src/cli/dev-schema-sync.ts +12 -4
  68. package/src/cli/dev.test.ts +50 -30
  69. package/src/cli/dev.ts +607 -66
  70. package/src/cli/drizzle-env.test.ts +40 -6
  71. package/src/cli/drizzle-env.ts +22 -1
  72. package/src/cli/ensure-drizzle-config.ts +6 -4
  73. package/src/cli/hero-meta.test.ts +13 -1
  74. package/src/cli/hero-meta.ts +28 -6
  75. package/src/cli/index.ts +5 -0
  76. package/src/cli/registry.ts +24 -0
  77. package/src/cli/replay.test.ts +105 -0
  78. package/src/cli/replay.ts +206 -0
  79. package/src/compiler/extract.ts +3 -3
  80. package/src/config/index.ts +3 -3
  81. package/src/console/server/claim.ts +3 -1
  82. package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
  83. package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
  84. package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
  85. package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
  86. package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
  87. package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
  88. package/src/console/ui/dist/index.html +1 -1
  89. package/src/console/ui/overview/slo.ts +31 -3
  90. package/src/console/ui/overview/types.ts +6 -0
  91. package/src/console/ui/runs/errors.test.ts +53 -0
  92. package/src/console/ui/runs/errors.ts +88 -0
  93. package/src/console/ui/runs/index.ts +8 -0
  94. package/src/console/ui/runs/search.ts +16 -0
  95. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
  96. package/src/docker/ai-model-status.test.ts +101 -0
  97. package/src/docker/ai-model-status.ts +276 -0
  98. package/src/docker/compose-health.test.ts +115 -0
  99. package/src/docker/compose-health.ts +234 -0
  100. package/src/docker/compose.ts +24 -8
  101. package/src/docker/derive.ts +35 -9
  102. package/src/docker/docker.test.ts +304 -12
  103. package/src/docker/helpers.ts +56 -0
  104. package/src/docker/index.ts +52 -0
  105. package/src/docker/ollama-pull.ts +232 -0
  106. package/src/docker/recipes/cockroach.ts +24 -0
  107. package/src/docker/recipes/index.ts +32 -0
  108. package/src/docker/recipes/llama-cpp.ts +273 -0
  109. package/src/docker/recipes/ollama.ts +19 -23
  110. package/src/docker/recipes/pgdog.ts +4 -1
  111. package/src/docker/recipes/postgres.ts +5 -18
  112. package/src/docker/recipes/sglang.ts +55 -0
  113. package/src/docker/recipes/supabase.ts +21 -0
  114. package/src/docker/recipes/timescale.ts +18 -0
  115. package/src/docker/recipes/vllm.ts +44 -0
  116. package/src/docker/recipes/yugabyte.ts +23 -0
  117. package/src/docker/stack-id.test.ts +22 -0
  118. package/src/docker/stack-id.ts +38 -4
  119. package/src/docker/types.ts +15 -0
  120. package/src/drivers/ai-ollama.integration.test.ts +7 -4
  121. package/src/drivers/signal-engine.ts +10 -0
  122. package/src/drivers/signal-postgres.ts +24 -2
  123. package/src/drivers/signal-types.ts +10 -0
  124. package/src/elements/channel/fallback.test.ts +91 -0
  125. package/src/elements/channel/hard-bounce.test.ts +82 -0
  126. package/src/elements/channel/injection.test.ts +153 -0
  127. package/src/elements/channel/locale-ar.test.ts +102 -0
  128. package/src/elements/channel/otp-delivery.ts +5 -2
  129. package/src/elements/channel/receipts-ledger.test.ts +100 -0
  130. package/src/elements/channel/runtime.ts +24 -1
  131. package/src/elements/channel/test-helpers.ts +60 -0
  132. package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
  133. package/src/elements/store/cache.ts +1 -1
  134. package/src/elements/store/emit-drizzle.ts +43 -9
  135. package/src/elements/store/runtime.ts +1 -1
  136. package/src/elements/store/schema-decl.test.ts +2 -0
  137. package/src/elements/store/schema-decl.ts +15 -0
  138. package/src/elements/store/seed.ts +1 -1
  139. package/src/index.ts +1 -0
  140. package/src/kernel/app.ts +117 -10
  141. package/src/kernel/boot-bind/signal.ts +6 -2
  142. package/src/kernel/boot-bind/store.ts +2 -1
  143. package/src/kernel/boot.test.ts +6 -5
  144. package/src/kernel/boot.ts +8 -3
  145. package/src/kernel/compensate.test.ts +131 -0
  146. package/src/kernel/correlation.test.ts +108 -0
  147. package/src/kernel/effect-timing.test.ts +82 -0
  148. package/src/kernel/flow.ts +38 -0
  149. package/src/kernel/fx-runs.test.ts +49 -0
  150. package/src/kernel/fx.test.ts +14 -0
  151. package/src/kernel/fx.ts +111 -1
  152. package/src/manifest/types.ts +8 -2
  153. package/src/runs/collect.ts +3 -0
  154. package/src/runs/export-otlp.ts +64 -0
  155. package/src/runs/index.ts +16 -0
  156. package/src/runs/parquet.ts +9 -0
  157. package/src/runs/types.ts +5 -0
  158. package/src/runs/window.test.ts +68 -0
  159. package/src/runs/window.ts +148 -0
  160. package/src/runtime/boot-warn.test.ts +45 -0
  161. package/src/runtime/boot-warn.ts +17 -0
  162. package/src/runtime/dev-request-log.ts +5 -5
  163. package/src/term.test.ts +95 -10
  164. package/src/term.ts +369 -14
  165. package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
  166. package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Optional OTLP metrics export shape — additive sink for WideEvents.
3
+ *
4
+ * Not a prerequisite for native alerting (Runs + Clock + Channel).
5
+ * Protocol-named (`otlp`); vendor choice stays in config / images.
6
+ */
7
+
8
+ import type { WideEvent } from "./types.ts";
9
+
10
+ /** One OTLP-shaped metric datapoint derived from a wide event. */
11
+ export interface OtlpRunMetric {
12
+ readonly name: "oke.runs" | "oke.run.duration_ms";
13
+ readonly kind: "counter" | "histogram";
14
+ readonly value: number;
15
+ readonly attributes: Readonly<Record<string, string>>;
16
+ readonly timeUnixMs: number;
17
+ }
18
+
19
+ /**
20
+ * Map a WideEvent onto OTLP-friendly metric datapoints.
21
+ *
22
+ * @param event - Appended run
23
+ */
24
+ export function wideEventToOtlpMetrics(event: WideEvent): readonly OtlpRunMetric[] {
25
+ const attributes = {
26
+ flow: event.flow,
27
+ trigger: event.trigger,
28
+ outcome: event.error ? "error" : "ok",
29
+ ...(event.error ? { error_code: event.error.code } : {}),
30
+ };
31
+ return [
32
+ {
33
+ name: "oke.runs",
34
+ kind: "counter",
35
+ value: 1,
36
+ attributes,
37
+ timeUnixMs: event.endedAt,
38
+ },
39
+ {
40
+ name: "oke.run.duration_ms",
41
+ kind: "histogram",
42
+ value: event.durationMs,
43
+ attributes: { flow: event.flow, trigger: event.trigger },
44
+ timeUnixMs: event.endedAt,
45
+ },
46
+ ];
47
+ }
48
+
49
+ /** Pluggable sink for teams with an existing Grafana / Datadog stack. */
50
+ export type OtlpMetricsSink = (metrics: readonly OtlpRunMetric[]) => void | Promise<void>;
51
+
52
+ /**
53
+ * Fan-out helper: call after `runs.record` when an OTLP sink is configured.
54
+ *
55
+ * @param event - Just-recorded wide event
56
+ * @param sink - Optional exporter
57
+ */
58
+ export async function exportRunMetrics(
59
+ event: WideEvent,
60
+ sink: OtlpMetricsSink | undefined,
61
+ ): Promise<void> {
62
+ if (!sink) return;
63
+ await sink(wideEventToOtlpMetrics(event));
64
+ }
package/src/runs/index.ts CHANGED
@@ -68,5 +68,21 @@ export {
68
68
 
69
69
  export { privacyErase, type PrivacyEraseOptions, type PrivacyEraseResult } from "./privacy.ts";
70
70
 
71
+ export {
72
+ evaluateSloBreaches,
73
+ parseLatencyMs,
74
+ percentile,
75
+ windowStatsForFlow,
76
+ type RunWindowStats,
77
+ type SloBreach,
78
+ } from "./window.ts";
79
+
80
+ export {
81
+ exportRunMetrics,
82
+ wideEventToOtlpMetrics,
83
+ type OtlpMetricsSink,
84
+ type OtlpRunMetric,
85
+ } from "./export-otlp.ts";
86
+
71
87
  export { createRunsPostgresFake } from "./drivers/postgres.ts";
72
88
  export { createRunsClickHouseFake } from "./drivers/clickhouse.ts";
@@ -42,6 +42,7 @@ export function wideEventToRow(event: WideEvent): ParquetRow {
42
42
  build_version: event.buildVersion ?? null,
43
43
  error_code: event.error?.code ?? null,
44
44
  error_message: event.error?.message ?? null,
45
+ input: event.input === undefined ? null : JSON.stringify(event.input),
45
46
  effects: JSON.stringify(event.effects),
46
47
  logs: JSON.stringify(event.logs),
47
48
  duration_ms: event.durationMs,
@@ -103,6 +104,14 @@ export function rowToWideEvent(row: Record<string, unknown>): WideEvent {
103
104
  ...(row.error_message != null ? { message: String(row.error_message) } : {}),
104
105
  }
105
106
  : null,
107
+ ...(row.input != null && row.input !== ""
108
+ ? {
109
+ input:
110
+ typeof row.input === "string"
111
+ ? (JSON.parse(row.input) as unknown)
112
+ : (row.input as unknown),
113
+ }
114
+ : {}),
106
115
  effects: parseJsonArray(row.effects) as WideEvent["effects"],
107
116
  logs: parseJsonArray(row.logs) as WideEvent["logs"],
108
117
  durationMs: Number(row.duration_ms ?? 0),
package/src/runs/types.ts CHANGED
@@ -69,6 +69,11 @@ export interface WideEvent {
69
69
  readonly buildVersion?: string;
70
70
  /** Typed error when the flow failed. */
71
71
  readonly error?: RunError | null;
72
+ /**
73
+ * Validated flow input snapshot for local replay (`oke replay`).
74
+ * Declared personal fields may be crypto-shredded into {@link archived}.
75
+ */
76
+ readonly input?: unknown;
72
77
  /** Effect ledger snapshot. */
73
78
  readonly effects: readonly EffectEntry[];
74
79
  /**
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { WideEvent } from "./types.ts";
3
+ import { evaluateSloBreaches, parseLatencyMs, windowStatsForFlow } from "./window.ts";
4
+
5
+ function ev(
6
+ partial: Partial<WideEvent> & Pick<WideEvent, "id" | "durationMs" | "startedAt">,
7
+ ): WideEvent {
8
+ return {
9
+ flow: "checkout.create",
10
+ trigger: "http",
11
+ plane: "user",
12
+ gates: [],
13
+ cache: "none",
14
+ error: null,
15
+ effects: [],
16
+ logs: [],
17
+ endedAt: partial.startedAt + partial.durationMs,
18
+ dimensions: {},
19
+ ...partial,
20
+ };
21
+ }
22
+
23
+ describe("runs window stats", () => {
24
+ test("parseLatencyMs", () => {
25
+ expect(parseLatencyMs("200ms")).toBe(200);
26
+ expect(parseLatencyMs("1s")).toBe(1000);
27
+ expect(parseLatencyMs("bad")).toBeNull();
28
+ });
29
+
30
+ test("computes p95 and error rate", () => {
31
+ const now = 10_000;
32
+ const events = [
33
+ ev({ id: "1", durationMs: 10, startedAt: now - 1000 }),
34
+ ev({ id: "2", durationMs: 20, startedAt: now - 900 }),
35
+ ev({ id: "3", durationMs: 30, startedAt: now - 800 }),
36
+ ev({ id: "4", durationMs: 40, startedAt: now - 700 }),
37
+ ev({
38
+ id: "5",
39
+ durationMs: 200,
40
+ startedAt: now - 600,
41
+ error: { code: "Timeout" },
42
+ }),
43
+ ];
44
+ const stats = windowStatsForFlow(events, "checkout.create", now, 5 * 60_000);
45
+ expect(stats.total).toBe(5);
46
+ expect(stats.errors).toBe(1);
47
+ expect(stats.errorRate).toBeCloseTo(0.2);
48
+ expect(stats.p95Ms).toBe(200);
49
+ });
50
+
51
+ test("evaluateSloBreaches flags latency and availability", () => {
52
+ const stats = windowStatsForFlow(
53
+ [
54
+ ev({ id: "1", durationMs: 500, startedAt: 9000, error: { code: "X" } }),
55
+ ev({ id: "2", durationMs: 500, startedAt: 9100 }),
56
+ ],
57
+ "checkout.create",
58
+ 10_000,
59
+ 5_000,
60
+ );
61
+ const breaches = evaluateSloBreaches(stats, {
62
+ availability: "99.9%",
63
+ latency: { p95: "100ms" },
64
+ });
65
+ expect(breaches.some((b) => b.kind === "availability")).toBe(true);
66
+ expect(breaches.some((b) => b.kind === "latency_p95")).toBe(true);
67
+ });
68
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Rolling window metrics over WideEvents — shared by Console Overview and
3
+ * Flow-facing `fx.runs` checkers (native alerting without `fx.metric`).
4
+ */
5
+
6
+ import type { WideEvent } from "./types.ts";
7
+
8
+ /** One flow's rolling stats over a time window. */
9
+ export interface RunWindowStats {
10
+ readonly flow: string;
11
+ readonly total: number;
12
+ readonly errors: number;
13
+ readonly errorRate: number;
14
+ readonly p50Ms: number;
15
+ readonly p95Ms: number;
16
+ readonly p99Ms: number;
17
+ readonly from: number;
18
+ readonly to: number;
19
+ }
20
+
21
+ /**
22
+ * Percentile of a sorted numeric array.
23
+ *
24
+ * @param sorted - Ascending durations
25
+ * @param p - Percentile in (0, 1]
26
+ */
27
+ export function percentile(sorted: readonly number[], p: number): number {
28
+ if (sorted.length === 0) return 0;
29
+ if (sorted.length === 1) return sorted[0]!;
30
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1));
31
+ return sorted[idx]!;
32
+ }
33
+
34
+ /**
35
+ * Parse a Manifest latency string like `200ms` / `1s` into milliseconds.
36
+ *
37
+ * @param raw - Latency threshold
38
+ */
39
+ export function parseLatencyMs(raw: string | undefined): number | null {
40
+ if (!raw) return null;
41
+ const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m)?$/i.exec(raw.trim());
42
+ if (!match) return null;
43
+ const n = Number(match[1]);
44
+ if (!Number.isFinite(n) || n < 0) return null;
45
+ const unit = (match[2] ?? "ms").toLowerCase();
46
+ if (unit === "s") return Math.round(n * 1000);
47
+ if (unit === "m") return Math.round(n * 60_000);
48
+ return Math.round(n);
49
+ }
50
+
51
+ /**
52
+ * Compute rolling stats for one flow over `[to - windowMs, to]`.
53
+ *
54
+ * @param events - Wide events
55
+ * @param flow - Flow name filter
56
+ * @param to - Window end (epoch-ms)
57
+ * @param windowMs - Window length
58
+ */
59
+ export function windowStatsForFlow(
60
+ events: readonly WideEvent[],
61
+ flow: string,
62
+ to: number,
63
+ windowMs: number,
64
+ ): RunWindowStats {
65
+ const from = to - Math.max(0, windowMs);
66
+ const mine = events.filter((e) => e.flow === flow && e.startedAt >= from && e.startedAt <= to);
67
+ const durations = mine.map((e) => e.durationMs).sort((a, b) => a - b);
68
+ const errors = mine.filter((e) => e.error != null).length;
69
+ const total = mine.length;
70
+ return {
71
+ flow,
72
+ total,
73
+ errors,
74
+ errorRate: total === 0 ? 0 : errors / total,
75
+ p50Ms: percentile(durations, 0.5),
76
+ p95Ms: percentile(durations, 0.95),
77
+ p99Ms: percentile(durations, 0.99),
78
+ from,
79
+ to,
80
+ };
81
+ }
82
+
83
+ /** Breach report for a declared latency / availability SLO. */
84
+ export interface SloBreach {
85
+ readonly flow: string;
86
+ readonly kind: "availability" | "latency_p95" | "latency_p99";
87
+ readonly threshold: number;
88
+ readonly observed: number;
89
+ readonly sampleCount: number;
90
+ }
91
+
92
+ /**
93
+ * Evaluate Manifest-style SLO thresholds against window stats.
94
+ *
95
+ * @param stats - Window stats
96
+ * @param slo - Declared availability + latency
97
+ */
98
+ export function evaluateSloBreaches(
99
+ stats: RunWindowStats,
100
+ slo: {
101
+ readonly availability?: string;
102
+ readonly latency?: { readonly p95?: string; readonly p99?: string };
103
+ },
104
+ ): SloBreach[] {
105
+ const out: SloBreach[] = [];
106
+ if (stats.total === 0) return out;
107
+
108
+ if (slo.availability) {
109
+ const match = /^(\d+(?:\.\d+)?)\s*%?$/.exec(slo.availability.trim());
110
+ if (match) {
111
+ const pct = Number(match[1]);
112
+ const tolerable = Math.max(0, 1 - pct / 100);
113
+ if (stats.errorRate > tolerable) {
114
+ out.push({
115
+ flow: stats.flow,
116
+ kind: "availability",
117
+ threshold: tolerable,
118
+ observed: stats.errorRate,
119
+ sampleCount: stats.total,
120
+ });
121
+ }
122
+ }
123
+ }
124
+
125
+ const p95 = parseLatencyMs(slo.latency?.p95);
126
+ if (p95 != null && stats.p95Ms > p95) {
127
+ out.push({
128
+ flow: stats.flow,
129
+ kind: "latency_p95",
130
+ threshold: p95,
131
+ observed: stats.p95Ms,
132
+ sampleCount: stats.total,
133
+ });
134
+ }
135
+
136
+ const p99 = parseLatencyMs(slo.latency?.p99);
137
+ if (p99 != null && stats.p99Ms > p99) {
138
+ out.push({
139
+ flow: stats.flow,
140
+ kind: "latency_p99",
141
+ threshold: p99,
142
+ observed: stats.p99Ms,
143
+ sampleCount: stats.total,
144
+ });
145
+ }
146
+
147
+ return out;
148
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Boot honesty suppress flag for the `oke dev` Backend child.
3
+ */
4
+
5
+ import { afterEach, describe, expect, test } from "bun:test";
6
+ import { emitBootWarn } from "./boot-warn.ts";
7
+
8
+ describe("emitBootWarn", () => {
9
+ const prev = process.env["OKE_SUPPRESS_BOOT_WARN"];
10
+
11
+ afterEach(() => {
12
+ if (prev === undefined) delete process.env["OKE_SUPPRESS_BOOT_WARN"];
13
+ else process.env["OKE_SUPPRESS_BOOT_WARN"] = prev;
14
+ });
15
+
16
+ test("forwards to console.warn by default", () => {
17
+ delete process.env["OKE_SUPPRESS_BOOT_WARN"];
18
+ const lines: string[] = [];
19
+ const original = console.warn;
20
+ console.warn = (...args: unknown[]) => {
21
+ lines.push(args.map(String).join(" "));
22
+ };
23
+ try {
24
+ emitBootWarn("oke boot: test notice");
25
+ expect(lines).toEqual(["oke boot: test notice"]);
26
+ } finally {
27
+ console.warn = original;
28
+ }
29
+ });
30
+
31
+ test("no-ops when OKE_SUPPRESS_BOOT_WARN=1", () => {
32
+ process.env["OKE_SUPPRESS_BOOT_WARN"] = "1";
33
+ const lines: string[] = [];
34
+ const original = console.warn;
35
+ console.warn = (...args: unknown[]) => {
36
+ lines.push(args.map(String).join(" "));
37
+ };
38
+ try {
39
+ emitBootWarn("oke boot: hidden");
40
+ expect(lines).toEqual([]);
41
+ } finally {
42
+ console.warn = original;
43
+ }
44
+ });
45
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Boot honesty lines (`oke boot: …`) — once per process, suppressible in
3
+ * the `oke dev` Backend child so the parent Console owns the notice.
4
+ */
5
+
6
+ /**
7
+ * Emit a boot warning unless suppressed for this process.
8
+ *
9
+ * Set `OKE_SUPPRESS_BOOT_WARN=1` on the Backend child during `oke dev` so
10
+ * Signal / Channel / files process-local notices print once (parent only).
11
+ *
12
+ * @param message - Full warning text (usually prefixed `oke boot:`)
13
+ */
14
+ export function emitBootWarn(message: string): void {
15
+ if (process.env["OKE_SUPPRESS_BOOT_WARN"] === "1") return;
16
+ console.warn(message);
17
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Dev-only request lines for `oke dev` (App / Console / MCP).
2
+ * Dev-only request lines for `oke dev` (Backend / Console / MCP).
3
3
  *
4
4
  * Gated by `OKE_DEV_REQUEST_LOG=1`. Surfaces share one TTY; label + color
5
5
  * keep streams readable without a multiplexer.
@@ -18,22 +18,22 @@ export function shouldLogDevRequests(): boolean {
18
18
  }
19
19
 
20
20
  /**
21
- * Active surface for this async context, or `OKE_DEV_SURFACE`, or `App`.
21
+ * Active surface for this async context, or `OKE_DEV_SURFACE`, or `Backend`.
22
22
  */
23
23
  export function currentDevSurface(): DevLogSurface {
24
24
  const fromAls = surfaceAls.getStore();
25
25
  if (fromAls) return fromAls;
26
26
  const fromEnv = process.env.OKE_DEV_SURFACE;
27
- if (fromEnv === "App" || fromEnv === "Console" || fromEnv === "MCP") {
27
+ if (fromEnv === "Backend" || fromEnv === "Console" || fromEnv === "MCP") {
28
28
  return fromEnv;
29
29
  }
30
- return "App";
30
+ return "Backend";
31
31
  }
32
32
 
33
33
  /**
34
34
  * Run a handler tagged with a surface (Console wrap around `app.fetch`).
35
35
  *
36
- * @param surface - App · Console · MCP
36
+ * @param surface - Backend · Console · MCP
37
37
  * @param fn - Async work
38
38
  */
39
39
  export function runWithDevSurface<T>(
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 App Console MCP URLs", () => {
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("App");
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: "App",
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("App");
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
  });