okengine 0.10.1 → 0.10.3

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 (141) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/site/content/docs/ai/mcp.mdx +0 -1
  4. package/site/content/docs/elements/ai.mdx +0 -6
  5. package/site/content/docs/elements/channel.mdx +0 -6
  6. package/site/content/docs/elements/clock.mdx +2 -8
  7. package/site/content/docs/elements/flow.mdx +10 -17
  8. package/site/content/docs/elements/gate.mdx +4 -10
  9. package/site/content/docs/elements/signal.mdx +4 -13
  10. package/site/content/docs/elements/store.mdx +9 -16
  11. package/site/content/docs/elements/vault.mdx +1 -7
  12. package/site/content/docs/get-started/basic-usage.mdx +1 -9
  13. package/site/content/docs/get-started/introduction.mdx +1 -1
  14. package/site/content/docs/get-started/why.mdx +1 -2
  15. package/site/content/docs/index.mdx +0 -1
  16. package/site/content/docs/meta.json +0 -1
  17. package/site/content/docs/reference/cli.md +12 -12
  18. package/site/content/docs/reference/fx.mdx +6 -7
  19. package/src/auth/auth.test.ts +1 -2
  20. package/src/auth/bindings.ts +5 -15
  21. package/src/auth/gate-auth.test.ts +2 -2
  22. package/src/cli/ai-setup/ai-setup.test.ts +3 -1
  23. package/src/cli/ai-setup/apply.ts +13 -0
  24. package/src/cli/build.test.ts +67 -0
  25. package/src/cli/build.ts +33 -0
  26. package/src/cli/db-seed.ts +1 -1
  27. package/src/cli/dev-controls.test.ts +43 -48
  28. package/src/cli/dev-controls.ts +23 -172
  29. package/src/cli/dev.test.ts +90 -5
  30. package/src/cli/dev.ts +39 -16
  31. package/src/cli/doctor.ts +98 -5
  32. package/src/cli/load-config.images.test.ts +31 -1
  33. package/src/cli/load-config.ts +2 -2
  34. package/src/cli/safe-defaults.test.ts +1 -1
  35. package/src/client/notes-contract.test.ts +3 -4
  36. package/src/compiler/aot.test.ts +1 -2
  37. package/src/compiler/extract.test.ts +76 -21
  38. package/src/compiler/extract.ts +15 -3
  39. package/src/compiler/fixtures/raw/raw-unannotated.ts +2 -4
  40. package/src/compiler/fixtures/skyport/oke.config.ts +4 -2
  41. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +4 -8
  42. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -2
  43. package/src/compiler/fixtures/skyport/src/flows/support/index.ts +1 -2
  44. package/src/compiler/fixtures/skyport.expected.json +3 -3
  45. package/src/compiler/fixtures/triggers/five-triggers.ts +5 -10
  46. package/src/compiler/generate-adopt.test.ts +85 -0
  47. package/src/compiler/generate-adopt.ts +111 -0
  48. package/src/config/driver-defaults.test.ts +122 -0
  49. package/src/config/driver-defaults.ts +186 -0
  50. package/src/config/index.ts +97 -7
  51. package/src/console/server/dry-run.audit.test.ts +2 -2
  52. package/src/console/server/flows.ts +50 -150
  53. package/src/docker/compose.ts +13 -3
  54. package/src/docker/images-config.test.ts +168 -0
  55. package/src/docker/recipes/llama-cpp.ts +10 -1
  56. package/src/docker/stack-id.ts +1 -0
  57. package/src/elements/channel/declare.ts +27 -1
  58. package/src/elements/clock/chaos-child.ts +3 -6
  59. package/src/elements/clock/chaos.test.ts +1 -2
  60. package/src/elements/clock.test.ts +2 -4
  61. package/src/elements/signal/declare.ts +25 -1
  62. package/src/elements/store/declare.ts +27 -1
  63. package/src/elements/store/resource.ts +6 -11
  64. package/src/elements/store/upsert-app.test.ts +1 -2
  65. package/src/elements/vault/declare.ts +29 -1
  66. package/src/kernel/adopt-barrel-fresh.test.ts +103 -0
  67. package/src/kernel/app.ts +66 -9
  68. package/src/kernel/auto-registry.test.ts +198 -0
  69. package/src/kernel/boot-bind/channel.ts +12 -4
  70. package/src/kernel/boot-bind/clock.ts +3 -3
  71. package/src/kernel/boot-bind/gate.ts +7 -4
  72. package/src/kernel/boot-bind/honor-config.test.ts +5 -5
  73. package/src/kernel/boot-bind/journal.ts +3 -3
  74. package/src/kernel/boot-bind/signal.ts +3 -1
  75. package/src/kernel/boot-bind/store.test.ts +19 -0
  76. package/src/kernel/boot-bind/store.ts +24 -9
  77. package/src/kernel/boot-bind/vault.ts +3 -3
  78. package/src/kernel/boot.test.ts +19 -15
  79. package/src/kernel/boot.ts +81 -1
  80. package/src/kernel/call.test.ts +5 -10
  81. package/src/kernel/compensate.test.ts +3 -6
  82. package/src/kernel/concurrency.test.ts +3 -5
  83. package/src/kernel/correlation.test.ts +4 -8
  84. package/src/kernel/edge.test.ts +1 -1
  85. package/src/kernel/effects-stamping.test.ts +4 -7
  86. package/src/kernel/element-registries.ts +27 -0
  87. package/src/kernel/errors.ts +11 -0
  88. package/src/kernel/flow.test.ts +8 -15
  89. package/src/kernel/flow.ts +12 -14
  90. package/src/kernel/hook-timing.test.ts +2 -2
  91. package/src/kernel/hooks.test.ts +5 -10
  92. package/src/kernel/horizontal-child.ts +5 -10
  93. package/src/kernel/journal-boot.test.ts +2 -4
  94. package/src/kernel/pipeline.test.ts +6 -12
  95. package/src/kernel/plugin/capabilities.test.ts +1 -1
  96. package/src/kernel/plugin/decorate.test.ts +3 -8
  97. package/src/kernel/plugin/scoping.test.ts +6 -16
  98. package/src/kernel/plugin-elements.test.ts +1 -1
  99. package/src/kernel/plugin-needs.test.ts +1 -1
  100. package/src/kernel/ready.test.ts +1 -2
  101. package/src/kernel/registry-isolation.test.ts +5 -5
  102. package/src/kernel/triggers.ts +1 -1
  103. package/src/plugins/anonymous.ts +1 -3
  104. package/src/plugins/compression.test.ts +6 -8
  105. package/src/plugins/config-source.test.ts +1 -1
  106. package/src/plugins/config-source.ts +1 -2
  107. package/src/plugins/cors.test.ts +10 -10
  108. package/src/plugins/csrf.test.ts +2 -2
  109. package/src/plugins/headers.test.ts +14 -15
  110. package/src/plugins/ip-allowlist.test.ts +9 -9
  111. package/src/plugins/magic-link.ts +2 -6
  112. package/src/plugins/maintenance-mode.test.ts +6 -6
  113. package/src/plugins/otp.ts +5 -15
  114. package/src/plugins/passkey.ts +4 -12
  115. package/src/plugins/two-factor.ts +3 -9
  116. package/src/plugins/username.ts +2 -6
  117. package/src/release/measure.ts +1 -1
  118. package/src/runs/runs.test.ts +3 -5
  119. package/src/runtime/serve.test.ts +2 -4
  120. package/src/test/create-test-app.test.ts +3 -7
  121. package/src/test/provisions.integration.test.ts +4 -9
  122. package/src/test/reset-element-registries.ts +31 -0
  123. package/site/content/docs/console/access.mdx +0 -29
  124. package/site/content/docs/console/ai.mdx +0 -35
  125. package/site/content/docs/console/architecture.mdx +0 -35
  126. package/site/content/docs/console/channels.mdx +0 -37
  127. package/site/content/docs/console/clock.mdx +0 -51
  128. package/site/content/docs/console/flows.mdx +0 -31
  129. package/site/content/docs/console/gates.mdx +0 -73
  130. package/site/content/docs/console/index.mdx +0 -54
  131. package/site/content/docs/console/manifest-diff.mdx +0 -34
  132. package/site/content/docs/console/meta.json +0 -24
  133. package/site/content/docs/console/overview.mdx +0 -40
  134. package/site/content/docs/console/plugins.mdx +0 -41
  135. package/site/content/docs/console/privacy.mdx +0 -32
  136. package/site/content/docs/console/runs.mdx +0 -50
  137. package/site/content/docs/console/signals.mdx +0 -40
  138. package/site/content/docs/console/store.mdx +0 -32
  139. package/site/content/docs/console/tenancy.mdx +0 -32
  140. package/site/content/docs/console/traces.mdx +0 -38
  141. package/site/content/docs/console/vault.mdx +0 -41
@@ -5,6 +5,7 @@
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
7
  import { OLLAMA_IMAGE } from "../../docker/recipes/index.ts";
8
+ import { DEFAULT_DOCKER_DIR } from "../../docker/types.ts";
8
9
 
9
10
  /** Choices applied to the project. */
10
11
  export type AiSetupApplyInput = {
@@ -91,6 +92,18 @@ export function applyAiSetup(
91
92
  }
92
93
  writeFileSync(envPath, env.endsWith("\n") ? env : `${env}\n`, "utf8");
93
94
 
95
+ // `docker/.env.docker` is regenerated from `.env.local` only on its first
96
+ // boot (existing values there are treated as durable, possibly hand-edited
97
+ // pins — see stack-id.ts). Once it exists, a later `oke ai setup` run must
98
+ // update it directly, or the docker profile keeps the old model forever.
99
+ if (input.chatModel) {
100
+ const dockerEnvPath = join(cwd, DEFAULT_DOCKER_DIR, ".env.docker");
101
+ if (existsSync(dockerEnvPath)) {
102
+ const dockerEnv = readFileSync(dockerEnvPath, "utf8");
103
+ writeFileSync(dockerEnvPath, upsertEnv(dockerEnv, "OKE_AI_MODEL", input.chatModel), "utf8");
104
+ }
105
+ }
106
+
94
107
  const aiTsPath = join(cwd, "src", "core", "ai.ts");
95
108
  mkdirSync(dirname(aiTsPath), { recursive: true });
96
109
  writeFileSync(aiTsPath, renderAiTs(input), "utf8");
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `oke build` regenerates `src/flows/generated.ts` before bundling — real
3
+ * before/after proof, not an assumption about the wiring.
4
+ */
5
+
6
+ import { describe, expect, test } from "bun:test";
7
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { runBuild } from "./build.ts";
11
+
12
+ async function makeUnit(root: string, name: string): Promise<void> {
13
+ const dir = join(root, "src/flows", name);
14
+ await mkdir(dir, { recursive: true });
15
+ await writeFile(join(dir, "index.ts"), "export const nothing = 1;\n");
16
+ }
17
+
18
+ describe("oke build — .adopt() barrel regeneration", () => {
19
+ test("real before/after: no generated.ts before build, real barrel content after", async () => {
20
+ const root = await mkdtemp(join(tmpdir(), "oke-build-adopt-"));
21
+ try {
22
+ await makeUnit(root, "main");
23
+ await makeUnit(root, "notes");
24
+
25
+ const generatedPath = join(root, "src/flows/generated.ts");
26
+ await expect(readFile(generatedPath, "utf8")).rejects.toThrow();
27
+
28
+ const code = await runBuild({
29
+ rootDir: root,
30
+ bundle: async () => ({ success: true, logs: "" }),
31
+ });
32
+ expect(code).toBe(0);
33
+
34
+ const after = await readFile(generatedPath, "utf8");
35
+ expect(after).toContain('export * as main from "./main/index.ts";');
36
+ expect(after).toContain('export * as notes from "./notes/index.ts";');
37
+ } finally {
38
+ await rm(root, { recursive: true, force: true });
39
+ }
40
+ });
41
+
42
+ test("regeneration runs before bundling, using the injected syncAdoptBarrel", async () => {
43
+ const calls: string[] = [];
44
+ const code = await runBuild({
45
+ rootDir: "/fake/root",
46
+ syncAdoptBarrel: async (rootDir) => {
47
+ calls.push(`sync:${rootDir}`);
48
+ return ["main", "notes"];
49
+ },
50
+ bundle: async () => {
51
+ calls.push("bundle");
52
+ return { success: true, logs: "" };
53
+ },
54
+ });
55
+ expect(code).toBe(0);
56
+ expect(calls).toEqual(["sync:/fake/root", "bundle"]);
57
+ });
58
+
59
+ test("bundle failure still returns non-zero after a successful sync", async () => {
60
+ const code = await runBuild({
61
+ rootDir: "/fake/root",
62
+ syncAdoptBarrel: async () => [],
63
+ bundle: async () => ({ success: false, logs: "boom" }),
64
+ });
65
+ expect(code).toBe(1);
66
+ });
67
+ });
package/src/cli/build.ts CHANGED
@@ -8,12 +8,39 @@ export interface BuildOptions {
8
8
  readonly entry?: string;
9
9
  readonly outdir?: string;
10
10
  readonly write?: (text: string) => void;
11
+ /** Project root for the `.adopt()` barrel sync (default `process.cwd()`). */
12
+ readonly rootDir?: string;
11
13
  /** Inject bun.build (tests). */
12
14
  readonly bundle?: (opts: {
13
15
  entry: string;
14
16
  outdir: string;
15
17
  target: "bun" | "node" | "browser";
16
18
  }) => Promise<{ success: boolean; logs: string }>;
19
+ /**
20
+ * Inject `.adopt()` barrel regeneration (tests). Default: real
21
+ * `generateAdoptBarrel` + atomic write to `<rootDir>/src/flows/generated.ts`
22
+ * (`generated.ts.tmp` → rename). Returns the unit names written, for the
23
+ * status line.
24
+ */
25
+ readonly syncAdoptBarrel?: (rootDir: string) => Promise<readonly string[]>;
26
+ }
27
+
28
+ /**
29
+ * Regenerate `src/flows/generated.ts` from every `src/flows/<unit>/index.ts`
30
+ * unit folder — pre-step for `oke build` (mirrors `oke db`'s `schema.generated.ts`
31
+ * pre-step). A real file on disk, not a virtual module: identical resolution
32
+ * under `oke dev`'s runtime `import()` and `oke build`'s `Bun.build()`
33
+ * (investigated — a virtual-module Bun plugin does not resolve consistently
34
+ * across those two paths). Written atomically so a concurrent reader never
35
+ * sees a torn file.
36
+ *
37
+ * @param rootDir - Project root
38
+ */
39
+ async function defaultSyncAdoptBarrel(rootDir: string): Promise<readonly string[]> {
40
+ const { generateAdoptBarrel, writeAdoptBarrel } = await import("../compiler/generate-adopt.ts");
41
+ const { source, units } = await generateAdoptBarrel({ rootDir });
42
+ await writeAdoptBarrel(rootDir, source);
43
+ return units;
17
44
  }
18
45
 
19
46
  /**
@@ -26,6 +53,7 @@ export async function runBuild(options: BuildOptions = {}): Promise<number> {
26
53
  const target = options.target ?? "bun";
27
54
  const entry = options.entry ?? "src/app.ts";
28
55
  const outdir = options.outdir ?? "dist";
56
+ const rootDir = options.rootDir ?? process.cwd();
29
57
  const bunTarget = target === "edge" ? "browser" : target === "node" ? "node" : "bun";
30
58
 
31
59
  const bundle =
@@ -43,8 +71,13 @@ export async function runBuild(options: BuildOptions = {}): Promise<number> {
43
71
  logs: result.logs.map((l) => String(l)).join("\n"),
44
72
  };
45
73
  });
74
+ const syncAdoptBarrel = options.syncAdoptBarrel ?? defaultSyncAdoptBarrel;
46
75
 
47
76
  try {
77
+ const units = await syncAdoptBarrel(rootDir);
78
+ write(
79
+ `oke build: .adopt() barrel → src/flows/generated.ts (${units.length} unit${units.length === 1 ? "" : "s"})\n`,
80
+ );
48
81
  const result = await bundle({ entry, outdir, target: bunTarget });
49
82
  if (!result.success) {
50
83
  console.error(result.logs || "oke build: failed");
@@ -233,7 +233,7 @@ export async function bootSeedFx(
233
233
  if (!app) {
234
234
  throw new Error(`oke db seed: no oke() app export in ${entryAbs}`);
235
235
  }
236
- await app.boot({ env, startScheduler: false, config: config ?? undefined });
236
+ await app.boot({ env, startScheduler: false, config: config ?? undefined, rootDir: cwd });
237
237
  const storeRuntime = app.elements?.store ?? app.bootResult?.store;
238
238
  if (!storeRuntime) {
239
239
  throw new Error(
@@ -1,65 +1,42 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
- controlServicesFromStack,
4
3
  createDevControlDispatcher,
5
4
  formatDevControlsHelp,
6
5
  formatDevControlsHint,
7
- formatDevControlsServiceList,
8
6
  parseDevControlKey,
9
7
  } from "./dev-controls.ts";
10
8
 
9
+ const CTRL_C = String.fromCharCode(3);
10
+
11
11
  describe("dev-controls", () => {
12
12
  test("parseDevControlKey maps raw keys", () => {
13
13
  expect(parseDevControlKey("q")).toBe("q");
14
- expect(parseDevControlKey("\u0003")).toBe("q");
15
14
  expect(parseDevControlKey("?")).toBe("?");
16
15
  expect(parseDevControlKey("h")).toBe("?");
17
- expect(parseDevControlKey("c")).toBe("c");
18
- expect(parseDevControlKey("l")).toBe("l");
16
+ expect(parseDevControlKey("r")).toBe("r");
19
17
  expect(parseDevControlKey("u")).toBe("u");
20
18
  expect(parseDevControlKey("x")).toBe("x");
21
- expect(parseDevControlKey("r")).toBe("r");
22
- expect(parseDevControlKey("3")).toBe("3");
23
- expect(parseDevControlKey("\u001b")).toBe("esc");
19
+ expect(parseDevControlKey("c")).toBeNull();
20
+ expect(parseDevControlKey("l")).toBeNull();
21
+ expect(parseDevControlKey("3")).toBeNull();
22
+ expect(parseDevControlKey("")).toBeNull();
24
23
  expect(parseDevControlKey("z")).toBeNull();
25
24
  });
26
25
 
26
+ test("parseDevControlKey maps Ctrl+C to quit", () => {
27
+ expect(parseDevControlKey(CTRL_C)).toBe("q");
28
+ });
29
+
27
30
  test("format hint and help are scannable", () => {
28
31
  expect(formatDevControlsHint(false)).toContain("keys");
29
32
  expect(formatDevControlsHint(false)).toContain("refresh");
30
33
  expect(formatDevControlsHint(false)).toContain("quit");
31
- expect(formatDevControlsHelp(false)).toContain("select service");
34
+ expect(formatDevControlsHint(false)).not.toContain("services");
32
35
  expect(formatDevControlsHelp(false)).toContain("refresh");
33
- expect(formatDevControlsHelp(false)).not.toMatch(/\u001b\[/);
36
+ expect(formatDevControlsHelp(false)).not.toContain("select service");
34
37
  });
35
38
 
36
- test("controlServicesFromStack dedupes by serviceName", () => {
37
- const list = controlServicesFromStack([
38
- { label: "ai", serviceName: "ai" },
39
- { label: "mail", serviceName: "channel-email" },
40
- { label: "mail-ui", serviceName: "channel-email" },
41
- { label: "postgres", serviceName: "store-sql" },
42
- ]);
43
- expect(list.map((s) => s.serviceName)).toEqual(["ai", "channel-email", "store-sql"]);
44
- });
45
-
46
- test("formatDevControlsServiceList numbers rows", () => {
47
- const out = formatDevControlsServiceList(
48
- [
49
- { label: "ai", serviceName: "ai" },
50
- { label: "postgres", serviceName: "store-sql" },
51
- ],
52
- (name) => (name === "ai" ? "error" : "ready"),
53
- false,
54
- 1,
55
- );
56
- expect(out).toContain("1");
57
- expect(out).toContain("ai");
58
- expect(out).toContain("postgres");
59
- expect(out).toContain(">");
60
- });
61
-
62
- test("dispatcher select → stop → up stack → quit", async () => {
39
+ test("dispatcher up -> stop -> refresh -> help -> quit", async () => {
63
40
  const calls: string[] = [];
64
41
  let quit = false;
65
42
  let settled = 0;
@@ -79,29 +56,47 @@ describe("dev-controls", () => {
79
56
  onComposeSettled: () => {
80
57
  settled += 1;
81
58
  },
82
- services: () => [
83
- { label: "ai", serviceName: "ai" },
84
- { label: "postgres", serviceName: "store-sql" },
85
- ],
86
- composeAction: async (action, names) => {
87
- calls.push(`${action}:${names.join(",") || "*"}`);
59
+ composeAction: async (action) => {
60
+ calls.push(action);
88
61
  },
89
62
  });
90
- d.handleKey("l");
63
+ d.handleKey("u");
91
64
  await Bun.sleep(10);
92
- d.handleKey("1");
93
65
  d.handleKey("x");
94
66
  await Bun.sleep(10);
95
- d.handleKey("u");
67
+ d.handleKey("r");
96
68
  await Bun.sleep(10);
97
- d.handleKey("c");
69
+ d.handleKey("?");
98
70
  await Bun.sleep(10);
99
71
  d.handleKey("q");
100
- expect(calls).toEqual(["stop:ai", "up:*"]);
72
+ expect(calls).toEqual(["up", "stop"]);
101
73
  expect(settled).toBe(2);
102
74
  expect(refreshed).toBe(1);
103
75
  expect(panels).toBe(1);
104
76
  expect(quit).toBe(true);
105
77
  d.stop();
106
78
  });
79
+
80
+ test("busy compose action ignores a concurrent key press", async () => {
81
+ let running = 0;
82
+ let maxConcurrent = 0;
83
+ const calls: string[] = [];
84
+ const d = createDevControlDispatcher({
85
+ write: () => {},
86
+ onQuit: () => {},
87
+ composeAction: async (action) => {
88
+ running += 1;
89
+ maxConcurrent = Math.max(maxConcurrent, running);
90
+ await Bun.sleep(20);
91
+ calls.push(action);
92
+ running -= 1;
93
+ },
94
+ });
95
+ d.handleKey("u");
96
+ d.handleKey("x");
97
+ await Bun.sleep(40);
98
+ expect(calls).toEqual(["up"]);
99
+ expect(maxConcurrent).toBe(1);
100
+ d.stop();
101
+ });
107
102
  });
@@ -1,31 +1,17 @@
1
1
  /**
2
2
  * Keyboard controls for a live `oke dev` session (TTY raw mode).
3
3
  *
4
- * Keys: `?` help · `q` quit · `c` refresh · `l` list · `u` up all ·
5
- * `x` stop all · `1`–`9` select service then `u`/`x`/`r`.
4
+ * Keys: `?` help · `q` quit · `r` refresh · `u` up all · `x` stop all.
6
5
  *
7
- * Compose health / model phase update the status board above Logs — not
8
- * the log stream. `c` clears the TTY and reprints the latest board.
6
+ * Compose health / model phase update the status board above Logs — it
7
+ * already lists every service with a live status, so refresh alone is
8
+ * enough to see the stack; there is no separate services panel.
9
9
  */
10
10
 
11
- import {
12
- formatStatusDot,
13
- formatStatusLine,
14
- termColorEnabled,
15
- termStyle,
16
- type DevStatus,
17
- } from "../term.ts";
11
+ import { formatStatusLine, termColorEnabled, termStyle } from "../term.ts";
18
12
 
19
- /** One Docker service row addressable by a digit key. */
20
- export type DevControlService = {
21
- /** Human label (`ai`, `postgres`). */
22
- readonly label: string;
23
- /** Compose service name (`ai`, `store-sql`). */
24
- readonly serviceName: string;
25
- };
26
-
27
- /** Compose action against zero or more service names. */
28
- export type DevComposeControlAction = "up" | "stop" | "restart";
13
+ /** Compose action against the whole stack. */
14
+ export type DevComposeControlAction = "up" | "stop";
29
15
 
30
16
  /** Options for {@link startDevControls}. */
31
17
  export type StartDevControlsOptions = {
@@ -37,30 +23,22 @@ export type StartDevControlsOptions = {
37
23
  */
38
24
  readonly onRefresh?: () => void | Promise<void>;
39
25
  /**
40
- * Show help / services in a clean pane (typically refresh chrome first so
41
- * the panel is not interleaved with request logs).
26
+ * Show help in a clean pane (refreshes chrome first so the panel is not
27
+ * interleaved with request logs).
42
28
  *
43
29
  * @param body - Formatted panel text
44
30
  */
45
31
  readonly onShowPanel?: (body: string) => void | Promise<void>;
46
32
  /**
47
- * After a compose up/stop/restart — sync health into the board (no log lines).
33
+ * After a compose up/stop — sync health into the board (no log lines).
48
34
  */
49
35
  readonly onComposeSettled?: () => void | Promise<void>;
50
- /** Live service catalogue (unique compose services). */
51
- readonly services: () => readonly DevControlService[];
52
36
  /**
53
- * Run a compose control action.
37
+ * Run a compose control action against the whole stack.
54
38
  *
55
- * @param action - up / stop / restart
56
- * @param serviceNames - Empty → whole stack
39
+ * @param action - up / stop
57
40
  */
58
- readonly composeAction: (
59
- action: DevComposeControlAction,
60
- serviceNames: readonly string[],
61
- ) => Promise<void>;
62
- /** Optional live ● status for list view. */
63
- readonly statusOf?: (serviceName: string) => DevStatus | undefined;
41
+ readonly composeAction: (action: DevComposeControlAction) => Promise<void>;
64
42
  readonly stdin?: NodeJS.ReadStream;
65
43
  readonly isTTY?: boolean;
66
44
  readonly color?: boolean;
@@ -81,9 +59,8 @@ export function formatDevControlsHint(color: boolean = termColorEnabled()): stri
81
59
  return (
82
60
  `${s.dim}│${s.reset} ${s.dim}keys${s.reset} ` +
83
61
  `${s.cyan}?${s.reset} help · ` +
84
- `${s.cyan}c${s.reset} refresh · ` +
62
+ `${s.cyan}r${s.reset} refresh · ` +
85
63
  `${s.cyan}q${s.reset} quit · ` +
86
- `${s.cyan}l${s.reset} services · ` +
87
64
  `${s.cyan}u${s.reset} up · ` +
88
65
  `${s.cyan}x${s.reset} stop` +
89
66
  `\n`
@@ -104,63 +81,15 @@ export function formatDevControlsHelp(color: boolean = termColorEnabled()): stri
104
81
  "",
105
82
  `${s.green}◇${s.reset} ${s.bold}Keys${s.reset}`,
106
83
  k("?", "help"),
107
- k("c", "refresh — clear logs, show latest ●"),
84
+ k("r", "refresh — clear logs, show latest ●"),
108
85
  k("q", "quit oke dev"),
109
- k("l", "list docker services"),
110
86
  k("u", "compose up -d (all)"),
111
87
  k("x", "compose stop (all)"),
112
- k("1-9", "select service"),
113
- k("… u", "start selected"),
114
- k("… x", "stop selected"),
115
- k("… r", "restart selected"),
116
- k("esc", "clear selection"),
117
88
  bar,
118
89
  "",
119
90
  ].join("\n");
120
91
  }
121
92
 
122
- /**
123
- * Format a numbered service list for `l`.
124
- *
125
- * @param services - Catalogue
126
- * @param statusOf - Optional ● lookup
127
- * @param color - Color on/off
128
- * @param selected - Highlighted 1-based index
129
- */
130
- export function formatDevControlsServiceList(
131
- services: readonly DevControlService[],
132
- statusOf?: (serviceName: string) => DevStatus | undefined,
133
- color: boolean = termColorEnabled(),
134
- selected?: number,
135
- ): string {
136
- const s = termStyle(color);
137
- const bar = `${s.dim}│${s.reset}`;
138
- if (services.length === 0) {
139
- return `${bar} ${s.dim}no docker services${s.reset}\n`;
140
- }
141
- const lines = [
142
- "",
143
- `${s.green}◇${s.reset} ${s.bold}Services${s.reset} ${s.dim}1-9 then u/x/r${s.reset}`,
144
- ];
145
- const max = Math.min(9, services.length);
146
- for (let i = 0; i < max; i++) {
147
- const svc = services[i]!;
148
- const st = statusOf?.(svc.serviceName) ?? "pending";
149
- const dot = formatStatusDot(st, color);
150
- const num = `${i + 1}`;
151
- const mark = selected === i + 1 ? `${s.cyan}>${s.reset}` : " ";
152
- lines.push(
153
- `${bar} ${mark}${s.cyan}${num}${s.reset} ${dot} ${svc.label.padEnd(12)} ${s.dim}${svc.serviceName}${s.reset}`,
154
- );
155
- }
156
- if (services.length > 9) {
157
- lines.push(`${bar} ${s.dim}… ${services.length - 9} more (first 9 only)${s.reset}`);
158
- }
159
- lines.push(bar);
160
- lines.push("");
161
- return `${lines.join("\n")}\n`;
162
- }
163
-
164
93
  /**
165
94
  * Parse one raw stdin chunk into a control key (tests / non-TTY inject).
166
95
  *
@@ -169,16 +98,12 @@ export function formatDevControlsServiceList(
169
98
  export function parseDevControlKey(chunk: string): string | null {
170
99
  if (chunk.length === 0) return null;
171
100
  if (chunk === "\u0003") return "q"; // Ctrl+C
172
- if (chunk === "\u001b") return "esc";
173
101
  const ch = chunk[0]!;
174
102
  if (ch === "?" || ch === "h" || ch === "H") return "?";
175
- if (ch === "c" || ch === "C") return "c";
176
103
  if (ch === "q" || ch === "Q") return "q";
177
- if (ch === "l" || ch === "L") return "l";
104
+ if (ch === "r" || ch === "R") return "r";
178
105
  if (ch === "u" || ch === "U") return "u";
179
106
  if (ch === "x" || ch === "X") return "x";
180
- if (ch === "r" || ch === "R") return "r";
181
- if (ch >= "1" && ch <= "9") return ch;
182
107
  return null;
183
108
  }
184
109
 
@@ -191,23 +116,18 @@ export type DevControlDispatcher = {
191
116
  /**
192
117
  * Pure key dispatcher (no stdin) — used by tests and the TTY listener.
193
118
  *
194
- * @param opts - Quit / compose / services
119
+ * @param opts - Quit / compose actions
195
120
  */
196
121
  export function createDevControlDispatcher(opts: StartDevControlsOptions): DevControlDispatcher {
197
122
  const color = opts.color ?? termColorEnabled();
198
- let selected: number | null = null;
199
123
  let busy = false;
200
124
  let stopped = false;
201
125
 
202
- const runAction = async (
203
- action: DevComposeControlAction,
204
- serviceNames: readonly string[],
205
- _label: string,
206
- ): Promise<void> => {
126
+ const runAction = async (action: DevComposeControlAction): Promise<void> => {
207
127
  if (busy) return;
208
128
  busy = true;
209
129
  try {
210
- await opts.composeAction(action, serviceNames);
130
+ await opts.composeAction(action);
211
131
  await opts.onComposeSettled?.();
212
132
  } catch (err) {
213
133
  const msg = err instanceof Error ? err.message : String(err);
@@ -224,7 +144,7 @@ export function createDevControlDispatcher(opts: StartDevControlsOptions): DevCo
224
144
  opts.onQuit();
225
145
  return;
226
146
  }
227
- if (key === "c") {
147
+ if (key === "r") {
228
148
  void Promise.resolve(opts.onRefresh?.()).catch((err: unknown) => {
229
149
  const msg = err instanceof Error ? err.message : String(err);
230
150
  opts.write(formatStatusLine(`refresh failed — ${msg}`, color, "error"));
@@ -236,58 +156,8 @@ export function createDevControlDispatcher(opts: StartDevControlsOptions): DevCo
236
156
  void Promise.resolve(opts.onShowPanel?.(body) ?? opts.write(body));
237
157
  return;
238
158
  }
239
- if (key === "esc") {
240
- selected = null;
241
- return;
242
- }
243
- if (key === "l") {
244
- const body = formatDevControlsServiceList(
245
- opts.services(),
246
- opts.statusOf,
247
- color,
248
- selected ?? undefined,
249
- );
250
- void Promise.resolve(opts.onShowPanel?.(body) ?? opts.write(body));
251
- return;
252
- }
253
- if (key >= "1" && key <= "9") {
254
- const n = Number(key);
255
- const list = opts.services();
256
- if (n < 1 || n > list.length || n > 9) {
257
- opts.write(formatStatusLine(`no service ${n} — press l to list`, color, "pending"));
258
- return;
259
- }
260
- selected = n;
261
- const svc = list[n - 1]!;
262
- opts.write(
263
- formatStatusLine(
264
- `selected ${n} ${svc.label} — u start · x stop · r restart · esc`,
265
- color,
266
- opts.statusOf?.(svc.serviceName) ?? "pending",
267
- ),
268
- );
269
- return;
270
- }
271
- if (key === "u" || key === "x" || key === "r") {
272
- const action: DevComposeControlAction =
273
- key === "u" ? "up" : key === "x" ? "stop" : "restart";
274
- if (selected !== null) {
275
- const list = opts.services();
276
- const svc = list[selected - 1];
277
- if (!svc) {
278
- selected = null;
279
- opts.write(formatStatusLine("selection expired — press l", color, "pending"));
280
- return;
281
- }
282
- selected = null;
283
- void runAction(action, [svc.serviceName], svc.label);
284
- return;
285
- }
286
- if (key === "r") {
287
- opts.write(formatStatusLine("select a service first (l then 1-9)", color, "pending"));
288
- return;
289
- }
290
- void runAction(action, [], "stack");
159
+ if (key === "u" || key === "x") {
160
+ void runAction(key === "u" ? "up" : "stop");
291
161
  }
292
162
  },
293
163
  stop() {
@@ -299,7 +169,7 @@ export function createDevControlDispatcher(opts: StartDevControlsOptions): DevCo
299
169
  /**
300
170
  * Start raw-mode keyboard controls. No-op when stdin is not a TTY.
301
171
  *
302
- * @param opts - Quit / compose / services
172
+ * @param opts - Quit / compose actions
303
173
  */
304
174
  export function startDevControls(opts: StartDevControlsOptions): DevControlsHandle {
305
175
  const stdin = opts.stdin ?? process.stdin;
@@ -333,22 +203,3 @@ export function startDevControls(opts: StartDevControlsOptions): DevControlsHand
333
203
  },
334
204
  };
335
205
  }
336
-
337
- /**
338
- * Dedupe stack summary rows into control services (first label wins).
339
- *
340
- * @param rows - Stack summary services with compose names
341
- */
342
- export function controlServicesFromStack(
343
- rows: readonly { readonly label: string; readonly serviceName?: string }[],
344
- ): DevControlService[] {
345
- const seen = new Set<string>();
346
- const out: DevControlService[] = [];
347
- for (const row of rows) {
348
- const name = row.serviceName?.trim();
349
- if (!name || seen.has(name)) continue;
350
- seen.add(name);
351
- out.push({ label: row.label, serviceName: name });
352
- }
353
- return out;
354
- }