okengine 0.10.3 → 0.11.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 (318) hide show
  1. package/package.json +76 -10
  2. package/site/content/docs/deployment/docker-swarm.mdx +48 -111
  3. package/site/content/docs/deployment/docker.mdx +43 -36
  4. package/site/content/docs/deployment/index.mdx +9 -9
  5. package/site/content/docs/deployment/kubernetes.mdx +4 -4
  6. package/site/content/docs/elements/ai.mdx +7 -7
  7. package/site/content/docs/elements/channel.mdx +9 -7
  8. package/site/content/docs/elements/clock.mdx +8 -8
  9. package/site/content/docs/elements/flow.mdx +46 -47
  10. package/site/content/docs/elements/gate.mdx +6 -5
  11. package/site/content/docs/elements/signal.mdx +2 -2
  12. package/site/content/docs/elements/store.mdx +71 -78
  13. package/site/content/docs/elements/vault.mdx +89 -70
  14. package/site/content/docs/get-started/basic-usage.mdx +8 -8
  15. package/site/content/docs/get-started/installation.mdx +36 -36
  16. package/site/content/docs/get-started/introduction.mdx +11 -0
  17. package/site/content/docs/get-started/why.mdx +2 -2
  18. package/site/content/docs/plugins/anonymous.mdx +1 -1
  19. package/site/content/docs/plugins/magic-link.mdx +11 -11
  20. package/site/content/docs/plugins/otp.mdx +2 -2
  21. package/site/content/docs/plugins/passkey.mdx +1 -1
  22. package/site/content/docs/plugins/two-factor.mdx +1 -1
  23. package/site/content/docs/plugins/username.mdx +1 -1
  24. package/site/content/docs/recipes/caddy.mdx +1 -1
  25. package/site/content/docs/recipes/cockroachdb.mdx +2 -2
  26. package/site/content/docs/recipes/dragonfly.mdx +1 -1
  27. package/site/content/docs/recipes/index.mdx +1 -2
  28. package/site/content/docs/recipes/llama-cpp.mdx +21 -23
  29. package/site/content/docs/recipes/mailpit.mdx +11 -11
  30. package/site/content/docs/recipes/meilisearch.mdx +10 -10
  31. package/site/content/docs/recipes/meta.json +0 -1
  32. package/site/content/docs/recipes/ollama.mdx +1 -1
  33. package/site/content/docs/recipes/pgdog.mdx +12 -9
  34. package/site/content/docs/recipes/postgres.mdx +3 -3
  35. package/site/content/docs/recipes/redis.mdx +2 -2
  36. package/site/content/docs/recipes/rustfs.mdx +10 -7
  37. package/site/content/docs/recipes/sglang.mdx +1 -1
  38. package/site/content/docs/recipes/supabase-docker.mdx +1 -1
  39. package/site/content/docs/recipes/timescale.mdx +2 -2
  40. package/site/content/docs/recipes/valkey.mdx +1 -1
  41. package/site/content/docs/recipes/vllm.mdx +1 -1
  42. package/site/content/docs/recipes/yugabytedb.mdx +2 -2
  43. package/site/content/docs/reference/cli.md +61 -38
  44. package/site/content/docs/reference/configuration.mdx +145 -70
  45. package/site/content/docs/reference/environment-variables.mdx +36 -28
  46. package/site/content/docs/reference/errors.mdx +28 -29
  47. package/site/content/docs/reference/fx.mdx +11 -4
  48. package/site/content/docs/reference/i18n.mdx +17 -6
  49. package/site/content/docs/reference/index.mdx +5 -0
  50. package/site/content/docs/reference/meta.json +1 -0
  51. package/site/content/docs/reference/migrating-environments.mdx +158 -0
  52. package/site/content/docs/reference/plugins.mdx +2 -2
  53. package/src/auth/gate-auth.test.ts +12 -10
  54. package/src/cli/ai-setup/ai-setup.test.ts +27 -15
  55. package/src/cli/ai-setup/apply.ts +111 -15
  56. package/src/cli/ai-setup/catalog.ts +3 -3
  57. package/src/cli/ai-setup/index.ts +1 -1
  58. package/src/cli/ai-setup/prompts.ts +1 -1
  59. package/src/cli/ai-setup/recommend.test.ts +1 -1
  60. package/src/cli/ask-seed.test.ts +3 -3
  61. package/src/cli/build.ts +1 -1
  62. package/src/cli/db-auto-push.test.ts +6 -3
  63. package/src/cli/db-auto-push.ts +7 -5
  64. package/src/cli/db-seed.ts +7 -8
  65. package/src/cli/db.test.ts +70 -71
  66. package/src/cli/db.ts +86 -12
  67. package/src/cli/dev-controller.test.ts +35 -0
  68. package/src/cli/dev-controller.ts +298 -0
  69. package/src/cli/dev-controls.ts +11 -65
  70. package/src/cli/dev-db-push.test.ts +47 -15
  71. package/src/cli/dev-schema-sync.test.ts +11 -11
  72. package/src/cli/dev-schema-sync.ts +10 -10
  73. package/src/cli/dev-session-lock.test.ts +106 -0
  74. package/src/cli/dev-session-lock.ts +209 -0
  75. package/src/cli/dev.test.ts +166 -121
  76. package/src/cli/dev.ts +138 -192
  77. package/src/cli/doc-staleness.test.ts +3 -6
  78. package/src/cli/docker-clean.test.ts +33 -1
  79. package/src/cli/docker-clean.ts +28 -33
  80. package/src/cli/docker-cli.test.ts +12 -11
  81. package/src/cli/docker.ts +95 -20
  82. package/src/cli/doctor.test.ts +40 -0
  83. package/src/cli/doctor.ts +55 -15
  84. package/src/cli/drizzle-env.test.ts +11 -13
  85. package/src/cli/drizzle-env.ts +18 -11
  86. package/src/cli/ensure-drizzle-config.ts +11 -14
  87. package/src/cli/hero-meta.test.ts +15 -15
  88. package/src/cli/hero-meta.ts +10 -9
  89. package/src/cli/index.ts +36 -6
  90. package/src/cli/load-config.images.test.ts +16 -12
  91. package/src/cli/load-config.source.test.ts +50 -0
  92. package/src/cli/load-config.ts +34 -28
  93. package/src/cli/meilisearch-local.ts +1 -2
  94. package/src/cli/mode.ts +7 -58
  95. package/src/cli/registry.ts +175 -19
  96. package/src/cli/resolve-dev-sql-env.test.ts +8 -16
  97. package/src/cli/resolve-dev-sql-env.ts +13 -15
  98. package/src/cli/safe-defaults.test.ts +28 -10
  99. package/src/cli/schema.ts +65 -16
  100. package/src/cli/test.ts +58 -0
  101. package/src/cli/tui/App.tsx +252 -0
  102. package/src/cli/tui/DevLive.tsx +116 -0
  103. package/src/cli/tui/components/ConfirmDialog.tsx +48 -0
  104. package/src/cli/tui/components/HeaderBar.tsx +67 -0
  105. package/src/cli/tui/components/LogStream.tsx +47 -0
  106. package/src/cli/tui/components/PanelCard.tsx +39 -0
  107. package/src/cli/tui/components/SelectRow.tsx +37 -0
  108. package/src/cli/tui/components/SlashPalette.tsx +172 -0
  109. package/src/cli/tui/components/Spinner.tsx +32 -0
  110. package/src/cli/tui/components/StatusBar.tsx +51 -0
  111. package/src/cli/tui/components/StatusDot.tsx +37 -0
  112. package/src/cli/tui/components/TabBar.tsx +49 -0
  113. package/src/cli/tui/components/Wordmark.tsx +43 -0
  114. package/src/cli/tui/format.ts +38 -0
  115. package/src/cli/tui/keys.test.ts +18 -0
  116. package/src/cli/tui/keys.ts +26 -0
  117. package/src/cli/tui/launch.test.ts +18 -0
  118. package/src/cli/tui/launch.ts +57 -0
  119. package/src/cli/tui/panels/Dashboard.tsx +201 -0
  120. package/src/cli/tui/panels/Database.tsx +198 -0
  121. package/src/cli/tui/panels/Dev.tsx +257 -0
  122. package/src/cli/tui/panels/Docker.tsx +357 -0
  123. package/src/cli/tui/panels/Navigator.test.ts +22 -0
  124. package/src/cli/tui/panels/Navigator.tsx +213 -0
  125. package/src/cli/tui/slash-catalog.test.ts +31 -0
  126. package/src/cli/tui/slash-catalog.ts +184 -0
  127. package/src/cli/tui/slash-run.ts +54 -0
  128. package/src/cli/tui/state/ports.ts +95 -0
  129. package/src/cli/tui/theme.ts +24 -0
  130. package/src/cli/vault-cmd.test.ts +402 -0
  131. package/src/cli/vault-cmd.ts +727 -69
  132. package/src/cli/vault-secure-input.test.ts +56 -0
  133. package/src/cli/vault-secure-input.ts +124 -0
  134. package/src/compiler/effects-infer.ts +7 -0
  135. package/src/compiler/fixtures/skyport/oke.config.ts +3 -3
  136. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
  137. package/src/compiler/fixtures/skyport.expected.json +1 -1
  138. package/src/compiler-entry.ts +7 -0
  139. package/src/config/define-config.test.ts +111 -47
  140. package/src/config/driver-defaults.test.ts +16 -23
  141. package/src/config/driver-defaults.ts +33 -63
  142. package/src/config/index.ts +169 -144
  143. package/src/config/resolve-driver.test.ts +14 -19
  144. package/src/console/server/app.ts +4 -2
  145. package/src/console/server/console.test.ts +58 -1
  146. package/src/console/server/flows.ts +22 -2
  147. package/src/console/server/index.ts +3 -0
  148. package/src/console/server/operator-db.test.ts +55 -13
  149. package/src/console/server/operator-db.ts +305 -263
  150. package/src/console/server/serve.ts +16 -7
  151. package/src/console/server/state.ts +31 -9
  152. package/src/console/server/vault.test.ts +120 -5
  153. package/src/console/server/vault.ts +208 -12
  154. package/src/console/ui/dist/assets/{index-D-9KFce5.js → index--Y0Uaz-m.js} +2 -2
  155. package/src/console/ui/dist/assets/panel-vault-BkZdJPX8.js +1 -0
  156. package/src/console/ui/dist/assets/style-C3L349iZ.css +3 -0
  157. package/src/console/ui/dist/index.html +2 -2
  158. package/src/console/ui/shell/client.ts +16 -0
  159. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +54 -1
  160. package/src/console/ui/vault/VaultA11yView.tsx +33 -1
  161. package/src/console/ui/vault/backend.test.ts +75 -0
  162. package/src/console/ui/vault/backend.ts +117 -0
  163. package/src/console/ui/vault/fixture.ts +21 -2
  164. package/src/console/ui/vault/index.ts +11 -1
  165. package/src/console/ui/vault/types.ts +26 -0
  166. package/src/docker/compose.ts +164 -61
  167. package/src/docker/derive.ts +106 -12
  168. package/src/docker/docker.test.ts +156 -110
  169. package/src/docker/helpers.ts +98 -0
  170. package/src/docker/images-config.test.ts +3 -8
  171. package/src/docker/index.ts +30 -5
  172. package/src/docker/recipes/index.ts +0 -3
  173. package/src/docker/recipes/llama-cpp.ts +4 -4
  174. package/src/docker/recipes/pgdog.ts +9 -2
  175. package/src/docker/resources.test.ts +71 -0
  176. package/src/docker/resources.ts +176 -0
  177. package/src/docker/stack-id.test.ts +11 -4
  178. package/src/docker/stack-id.ts +42 -32
  179. package/src/docker/stack.integration.test.ts +4 -15
  180. package/src/docker/types.ts +35 -3
  181. package/src/drivers/ai-ollama.integration.test.ts +2 -2
  182. package/src/drivers/conformance.test.ts +11 -12
  183. package/src/drivers/drizzle-dialect.test.ts +0 -4
  184. package/src/drivers/drizzle-dialect.ts +7 -13
  185. package/src/drivers/index.ts +24 -7
  186. package/src/drivers/journal-postgres.test.ts +3 -1
  187. package/src/drivers/journal-postgres.ts +4 -2
  188. package/src/drivers/meilisearch.ts +1 -1
  189. package/src/drivers/pglite.ts +28 -5
  190. package/src/drivers/types.ts +9 -11
  191. package/src/drivers/vault-aws-secrets-manager.test.ts +159 -0
  192. package/src/drivers/vault-aws-secrets-manager.ts +333 -0
  193. package/src/drivers/vault-builtin.test.ts +110 -0
  194. package/src/drivers/vault-builtin.ts +184 -0
  195. package/src/drivers/vault-driver-removal.test.ts +13 -0
  196. package/src/drivers/vault-managed.test.ts +95 -0
  197. package/src/drivers/vault-managed.ts +101 -25
  198. package/src/drivers/vault-types.ts +25 -5
  199. package/src/elements/ai/runtime.ts +1 -1
  200. package/src/elements/clock/chaos.test.ts +4 -1
  201. package/src/elements/clock/durable.ts +57 -4
  202. package/src/elements/clock.test.ts +13 -14
  203. package/src/elements/gate/constants.ts +18 -0
  204. package/src/elements/gate.test.ts +1 -1
  205. package/src/elements/gate.ts +2 -2
  206. package/src/elements/store/domain-ddl.test.ts +5 -12
  207. package/src/elements/store/emit-drizzle.ts +44 -19
  208. package/src/elements/store/index-boot.test.ts +15 -116
  209. package/src/elements/store/load-plugin-tables.ts +1 -1
  210. package/src/elements/store/resource-list-docs.fixture.ts +12 -6
  211. package/src/elements/store/resource-list-docs.test.ts +64 -44
  212. package/src/elements/store/resource.test.ts +17 -10
  213. package/src/elements/store/runtime.ts +9 -12
  214. package/src/elements/store/schema-decl.test.ts +16 -32
  215. package/src/elements/store/seed.test.ts +1 -2
  216. package/src/elements/store/seed.ts +7 -8
  217. package/src/elements/store/sql-condition.test.ts +2 -2
  218. package/src/elements/store/sql-session.test.ts +8 -10
  219. package/src/elements/store/sql-session.ts +10 -2
  220. package/src/elements/store.test.ts +13 -13
  221. package/src/elements/vault/audit.ts +233 -0
  222. package/src/elements/vault/boot-chain.test.ts +78 -0
  223. package/src/elements/vault/boot-chain.ts +43 -39
  224. package/src/elements/vault/builtin-adapter.test.ts +479 -0
  225. package/src/elements/vault/builtin-adapter.ts +1184 -0
  226. package/src/elements/vault/crypto.test.ts +237 -0
  227. package/src/elements/vault/crypto.ts +431 -0
  228. package/src/elements/vault/declare.ts +164 -3
  229. package/src/elements/vault/errors.ts +56 -0
  230. package/src/elements/vault/kms-unseal.test.ts +76 -0
  231. package/src/elements/vault/kms-unseal.ts +181 -0
  232. package/src/elements/vault/path.ts +90 -0
  233. package/src/elements/vault/resilience.test.ts +102 -0
  234. package/src/elements/vault/resilience.ts +107 -0
  235. package/src/elements/vault/runtime.ts +36 -4
  236. package/src/elements/vault/security-checklist.test.ts +130 -0
  237. package/src/elements/vault/storage.ts +448 -0
  238. package/src/elements/vault/types.ts +288 -0
  239. package/src/elements/vault/unseal.ts +150 -0
  240. package/src/elements/vault.test.ts +72 -4
  241. package/src/elements/vault.ts +91 -2
  242. package/src/full.ts +248 -0
  243. package/src/http.ts +41 -0
  244. package/src/i18n/interpolate-simple.ts +37 -0
  245. package/src/i18n/messages.ts +51 -4
  246. package/src/i18n-entry.ts +7 -0
  247. package/src/index.ts +35 -145
  248. package/src/journal-entry.ts +20 -0
  249. package/src/kernel/adopt-barrel-fresh.test.ts +25 -10
  250. package/src/kernel/app.ts +40 -42
  251. package/src/kernel/auto-registry.test.ts +2 -2
  252. package/src/kernel/boot-bind/ai.test.ts +5 -5
  253. package/src/kernel/boot-bind/ai.ts +2 -1
  254. package/src/kernel/boot-bind/channel.test.ts +6 -6
  255. package/src/kernel/boot-bind/channel.ts +3 -10
  256. package/src/kernel/boot-bind/clock.ts +2 -2
  257. package/src/kernel/boot-bind/gate.ts +5 -9
  258. package/src/kernel/boot-bind/honor-config.test.ts +42 -42
  259. package/src/kernel/boot-bind/journal.ts +2 -2
  260. package/src/kernel/boot-bind/store.test.ts +43 -44
  261. package/src/kernel/boot-bind/store.ts +16 -41
  262. package/src/kernel/boot-bind/vault.ts +16 -1
  263. package/src/kernel/boot.test.ts +12 -4
  264. package/src/kernel/boot.ts +31 -19
  265. package/src/kernel/budget-entry.ts +1 -1
  266. package/src/kernel/compensate.test.ts +288 -1
  267. package/src/kernel/compensate.ts +126 -0
  268. package/src/kernel/concurrency.test.ts +10 -9
  269. package/src/kernel/effects-stamping.test.ts +27 -2
  270. package/src/kernel/element-registries.ts +5 -3
  271. package/src/kernel/flow.ts +4 -2
  272. package/src/kernel/fx.test.ts +119 -3
  273. package/src/kernel/fx.ts +215 -40
  274. package/src/kernel/index.ts +14 -0
  275. package/src/kernel/journal.ts +130 -7
  276. package/src/kernel/pipeline.test.ts +20 -6
  277. package/src/kernel/redacted.test.ts +35 -0
  278. package/src/kernel/redacted.ts +24 -2
  279. package/src/kernel/router/create-default.ts +16 -0
  280. package/src/kernel/router/create-edge.ts +18 -0
  281. package/src/kernel/router/create.ts +23 -0
  282. package/src/kernel/router/index.ts +23 -0
  283. package/src/kernel/router/linear.ts +88 -0
  284. package/src/kernel/router/regexp.ts +188 -0
  285. package/src/kernel/router/smart.ts +120 -0
  286. package/src/kernel/router/trie.ts +108 -0
  287. package/src/kernel/router/types.ts +56 -0
  288. package/src/kernel/router.ts +18 -532
  289. package/src/kernel-entry.ts +43 -0
  290. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  291. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  292. package/src/plugins/config-source.test.ts +7 -7
  293. package/src/release/build-lib.ts +58 -0
  294. package/src/release/exports.test.ts +4 -2
  295. package/src/release/exports.ts +30 -4
  296. package/src/release/measure.ts +101 -22
  297. package/src/runs/duckdb.ts +7 -2
  298. package/src/shared/optional-peer.ts +41 -0
  299. package/src/term.test.ts +3 -44
  300. package/src/test/create-test-app.ts +12 -0
  301. package/src/test/provisions.integration.test.ts +1 -1
  302. package/src/test/reset-element-registries.ts +2 -0
  303. package/src/upgrade/codemods.test.ts +7 -20
  304. package/src/upgrade/codemods.ts +93 -60
  305. package/site/content/docs/recipes/openbao.mdx +0 -141
  306. package/src/cli/ask-dev-mode.ts +0 -40
  307. package/src/cli/dev-mode.test.ts +0 -45
  308. package/src/cli/dev-mode.ts +0 -78
  309. package/src/cli/openbao-bootstrap.test.ts +0 -150
  310. package/src/cli/openbao-bootstrap.ts +0 -321
  311. package/src/cli/openbao-restart.integration.test.ts +0 -145
  312. package/src/console/ui/dist/assets/panel-vault-Za1GyJfM.js +0 -1
  313. package/src/console/ui/dist/assets/style-C8MxEWPd.css +0 -3
  314. package/src/docker/recipes/openbao.ts +0 -47
  315. package/src/drivers/libsql.ts +0 -179
  316. package/src/drivers/sqlite.ts +0 -44
  317. package/src/drivers/vault-openbao.test.ts +0 -97
  318. package/src/drivers/vault-openbao.ts +0 -143
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Docker panel — live stack by project name; clean tears down whole projects.
3
+ *
4
+ * Never shells into interactive `oke docker clean` (clack) while Ink is mounted —
5
+ * that fights stdout and leaves ghost frames.
6
+ */
7
+
8
+ import { Box, Text, useInput } from "ink";
9
+ import { useEffect, useState, type ReactElement } from "react";
10
+ import type { DevStatus } from "../../../term.ts";
11
+ import { composeRowToStatus } from "../../../docker/compose-health.ts";
12
+ import {
13
+ composeProjectName,
14
+ downStack,
15
+ listOkeComposeProjects,
16
+ listStackContainers,
17
+ type OkeStack,
18
+ } from "../../../docker/cleanup.ts";
19
+ import { ConfirmDialog } from "../components/ConfirmDialog.tsx";
20
+ import type { FooterStatus } from "../components/StatusBar.tsx";
21
+ import { StatusDot, statusWord } from "../components/StatusDot.tsx";
22
+ import { TUI_ACCENT, TUI_MUTED, TUI_OK, TUI_WARN } from "../theme.ts";
23
+
24
+ /** Props for {@link DockerPanel}. */
25
+ export type DockerPanelProps = {
26
+ readonly cwd: string;
27
+ readonly active: boolean;
28
+ readonly onLog: (line: string) => void;
29
+ readonly onStatus: (status: FooterStatus) => void;
30
+ };
31
+
32
+ type DockerAction = "derive" | "clean" | "restart";
33
+
34
+ type ServiceRow = {
35
+ readonly name: string;
36
+ readonly status: DevStatus;
37
+ readonly detail: string;
38
+ };
39
+
40
+ type Mode = "main" | "pickClean" | "confirmClean";
41
+
42
+ /**
43
+ * Compose-oriented panel.
44
+ *
45
+ * @param props - cwd + log
46
+ */
47
+ export function DockerPanel(props: DockerPanelProps): ReactElement {
48
+ const project = composeProjectName(props.cwd);
49
+ const [rows, setRows] = useState<readonly ServiceRow[]>([]);
50
+ const [stacks, setStacks] = useState<readonly OkeStack[]>([]);
51
+ const [selected, setSelected] = useState(0);
52
+ const [mode, setMode] = useState<Mode>("main");
53
+ const [cleanFocus, setCleanFocus] = useState(0);
54
+ const [cleanPicked, setCleanPicked] = useState<ReadonlySet<string>>(() => new Set());
55
+ const [busy, setBusy] = useState(false);
56
+
57
+ const actions: readonly { id: DockerAction; label: string }[] = [
58
+ { id: "derive", label: "[D]erive" },
59
+ { id: "clean", label: "[C]lean" },
60
+ { id: "restart", label: "[R]estart stack" },
61
+ ];
62
+
63
+ const refresh = async (): Promise<void> => {
64
+ try {
65
+ const containers = await listStackContainers(project);
66
+ setRows(
67
+ containers.map((c) => ({
68
+ name: c.service || c.name,
69
+ status: composeRowToStatus({
70
+ Service: c.service,
71
+ State: c.state,
72
+ Status: c.state,
73
+ }),
74
+ detail: c.state || c.name,
75
+ })),
76
+ );
77
+ const all = await listOkeComposeProjects();
78
+ setStacks(all);
79
+ } catch (err) {
80
+ props.onLog(`${err instanceof Error ? err.message : String(err)}\n`);
81
+ }
82
+ };
83
+
84
+ useEffect(() => {
85
+ void refresh();
86
+ const id = setInterval(() => void refresh(), 4000);
87
+ return () => clearInterval(id);
88
+ }, [props.cwd]);
89
+
90
+ useInput(
91
+ (input, key) => {
92
+ if (!props.active || busy) return;
93
+
94
+ if (mode === "pickClean") {
95
+ if (key.escape) {
96
+ setMode("main");
97
+ return;
98
+ }
99
+ if (key.upArrow) setCleanFocus((i) => Math.max(0, i - 1));
100
+ if (key.downArrow) setCleanFocus((i) => Math.min(Math.max(0, stacks.length - 1), i + 1));
101
+ if (input === " ") {
102
+ const stack = stacks[cleanFocus];
103
+ if (!stack) return;
104
+ setCleanPicked((prev) => {
105
+ const next = new Set(prev);
106
+ if (next.has(stack.project)) next.delete(stack.project);
107
+ else next.add(stack.project);
108
+ return next;
109
+ });
110
+ return;
111
+ }
112
+ if (key.return) {
113
+ if (cleanPicked.size === 0) return;
114
+ setMode("confirmClean");
115
+ }
116
+ return;
117
+ }
118
+
119
+ if (mode !== "main") return;
120
+
121
+ if (key.upArrow) setSelected((i) => Math.max(0, i - 1));
122
+ if (key.downArrow) setSelected((i) => Math.min(actions.length - 1, i + 1));
123
+ const lower = input.toLowerCase();
124
+ if (lower === "d") void run("derive");
125
+ else if (lower === "c") void beginClean();
126
+ else if (lower === "r") void run("restart");
127
+ else if (key.return) {
128
+ const a = actions[selected];
129
+ if (!a) return;
130
+ if (a.id === "clean") void beginClean();
131
+ else void run(a.id);
132
+ }
133
+ },
134
+ { isActive: props.active && mode !== "confirmClean" },
135
+ );
136
+
137
+ const beginClean = async (): Promise<void> => {
138
+ try {
139
+ const all = await listOkeComposeProjects();
140
+ setStacks(all);
141
+ if (all.length === 0) {
142
+ props.onStatus({ kind: "done", label: "No oke-dev-* stacks" });
143
+ return;
144
+ }
145
+ const initial = new Set<string>();
146
+ if (all.some((s) => s.project === project)) initial.add(project);
147
+ else if (all[0]) initial.add(all[0].project);
148
+ setCleanPicked(initial);
149
+ setCleanFocus(
150
+ Math.max(
151
+ 0,
152
+ all.findIndex((s) => s.project === project),
153
+ ),
154
+ );
155
+ setMode("pickClean");
156
+ } catch (err) {
157
+ const m = err instanceof Error ? err.message : String(err);
158
+ props.onLog(`${m}\n`);
159
+ props.onStatus({ kind: "done", label: m });
160
+ }
161
+ };
162
+
163
+ const runClean = async (): Promise<void> => {
164
+ setBusy(true);
165
+ setMode("main");
166
+ const targets = [...cleanPicked].sort();
167
+ props.onStatus({
168
+ kind: "running",
169
+ label: `Removing ${targets.length} stack(s)…`,
170
+ });
171
+ try {
172
+ for (const p of targets) {
173
+ props.onLog(`oke docker clean: down -v ${p}\n`);
174
+ await downStack(p);
175
+ }
176
+ props.onStatus({
177
+ kind: "done",
178
+ label: `Removed ${targets.length} stack(s)`,
179
+ });
180
+ await refresh();
181
+ } catch (err) {
182
+ const m = err instanceof Error ? err.message : String(err);
183
+ props.onLog(`${m}\n`);
184
+ props.onStatus({ kind: "done", label: m });
185
+ } finally {
186
+ setBusy(false);
187
+ }
188
+ };
189
+
190
+ const run = async (id: Exclude<DockerAction, "clean">): Promise<void> => {
191
+ setBusy(true);
192
+ const labels = {
193
+ derive: "Running oke docker…",
194
+ restart: `Restarting ${project}…`,
195
+ } as const;
196
+ props.onStatus({ kind: "running", label: labels[id] });
197
+ try {
198
+ if (id === "derive") {
199
+ const { dockerCli } = await import("../../docker.ts");
200
+ const prev = process.cwd();
201
+ process.chdir(props.cwd);
202
+ let code = 1;
203
+ try {
204
+ code = await dockerCli([]);
205
+ } finally {
206
+ process.chdir(prev);
207
+ }
208
+ props.onLog(`oke docker → ${code}\n`);
209
+ props.onStatus({
210
+ kind: "done",
211
+ label: code === 0 ? "Derive ok" : `Derive exited ${code}`,
212
+ });
213
+ } else {
214
+ const proc = Bun.spawn(["docker", "compose", "-p", project, "restart"], {
215
+ stdout: "pipe",
216
+ stderr: "pipe",
217
+ });
218
+ const [stdout, stderr, code] = await Promise.all([
219
+ new Response(proc.stdout).text(),
220
+ new Response(proc.stderr).text(),
221
+ proc.exited,
222
+ ]);
223
+ if (stdout) props.onLog(stdout);
224
+ if (stderr) props.onLog(stderr);
225
+ props.onStatus({
226
+ kind: "done",
227
+ label: code === 0 ? "Stack restarted" : `Restart exited ${code}`,
228
+ });
229
+ }
230
+ await refresh();
231
+ } finally {
232
+ setBusy(false);
233
+ }
234
+ };
235
+
236
+ if (mode === "confirmClean") {
237
+ const list = [...cleanPicked].sort().join(", ");
238
+ return (
239
+ <ConfirmDialog
240
+ message={`Remove stack(s) — containers, networks, volumes?\n${list}`}
241
+ onConfirm={() => void runClean()}
242
+ onCancel={() => setMode("pickClean")}
243
+ />
244
+ );
245
+ }
246
+
247
+ if (mode === "pickClean") {
248
+ return (
249
+ <Box flexDirection="column" gap={1} flexGrow={1}>
250
+ <Text bold color={TUI_ACCENT}>
251
+ Clean stacks
252
+ </Text>
253
+ <Text color={TUI_MUTED} dimColor>
254
+ Space toggle · Enter confirm · Esc cancel — whole project, not per-service
255
+ </Text>
256
+ {stacks.length === 0 ? (
257
+ <Text color={TUI_WARN}>No oke-dev-* stacks on this machine</Text>
258
+ ) : (
259
+ stacks.map((s, i) => {
260
+ const on = i === cleanFocus;
261
+ const picked = cleanPicked.has(s.project);
262
+ const n = s.containers.length;
263
+ const current = s.project === project;
264
+ return (
265
+ <Text key={s.project} bold={on} color={on ? TUI_ACCENT : undefined}>
266
+ {on ? "› " : " "}
267
+ {picked ? "●" : "○"} {s.project}
268
+ <Text color={TUI_MUTED}>
269
+ {` ${n} container${n === 1 ? "" : "s"}`}
270
+ {current ? " · this project" : ""}
271
+ </Text>
272
+ </Text>
273
+ );
274
+ })
275
+ )}
276
+ </Box>
277
+ );
278
+ }
279
+
280
+ const online = rows.filter((r) => r.status === "ready").length;
281
+ const total = rows.length;
282
+ const allOnline = total > 0 && online === total;
283
+ const stackKnown = stacks.some((s) => s.project === project);
284
+
285
+ return (
286
+ <Box flexDirection="column" gap={1} flexGrow={1}>
287
+ <Text bold color={TUI_ACCENT}>
288
+ Docker Compose
289
+ </Text>
290
+ <Box gap={1}>
291
+ <StatusDot status={allOnline ? "ready" : total === 0 ? "idle" : "pending"} />
292
+ <Text bold color={allOnline ? TUI_OK : TUI_WARN}>
293
+ {total === 0
294
+ ? stackKnown
295
+ ? `0 services · ${project}`
296
+ : `No stack · ${project}`
297
+ : `${online}/${total} services online`}
298
+ </Text>
299
+ {total > 0 ? <Text color={TUI_MUTED}>· {project}</Text> : null}
300
+ </Box>
301
+
302
+ <Box>
303
+ <Box width={22}>
304
+ <Text bold color={TUI_MUTED}>
305
+ Service
306
+ </Text>
307
+ </Box>
308
+ <Box width={10}>
309
+ <Text bold color={TUI_MUTED}>
310
+ Status
311
+ </Text>
312
+ </Box>
313
+ <Text bold color={TUI_MUTED}>
314
+ Detail
315
+ </Text>
316
+ </Box>
317
+ <Text color={TUI_MUTED}>{"─".repeat(48)}</Text>
318
+
319
+ {rows.length === 0 ? (
320
+ <Text color={TUI_MUTED}>
321
+ No containers for {project}. Start `oke dev` or run [D]erive + compose up.
322
+ </Text>
323
+ ) : (
324
+ rows.map((r) => {
325
+ const sw = statusWord(r.status);
326
+ return (
327
+ <Box key={r.name}>
328
+ <Box width={22} gap={1}>
329
+ <StatusDot status={r.status} />
330
+ <Text>{r.name}</Text>
331
+ </Box>
332
+ <Box width={10}>
333
+ <Text color={sw.color}>{sw.word}</Text>
334
+ </Box>
335
+ <Text color={TUI_MUTED}>{r.detail}</Text>
336
+ </Box>
337
+ );
338
+ })
339
+ )}
340
+
341
+ <Box gap={2} marginTop={1}>
342
+ {actions.map((a, i) => (
343
+ <Text
344
+ key={a.id}
345
+ bold={props.active && i === selected}
346
+ color={
347
+ props.active && i === selected ? TUI_ACCENT : a.id === "clean" ? TUI_WARN : undefined
348
+ }
349
+ >
350
+ {props.active && i === selected ? "› " : " "}
351
+ {a.label}
352
+ </Text>
353
+ ))}
354
+ </Box>
355
+ </Box>
356
+ );
357
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { Manifest } from "../../../manifest/types.ts";
3
+ import { manifestToNavTree } from "./Navigator.tsx";
4
+
5
+ describe("manifestToNavTree", () => {
6
+ test("builds element sections from a Manifest", () => {
7
+ const manifest = {
8
+ oke: "1.0",
9
+ app: "demo",
10
+ flows: { hello: {} },
11
+ signals: { ping: {} },
12
+ stores: {},
13
+ ai: { models: { m1: {} }, prompts: {}, agents: {} },
14
+ } as unknown as Manifest;
15
+ const tree = manifestToNavTree(manifest);
16
+ expect(tree.some((n) => n.label.includes("app: demo"))).toBe(true);
17
+ const flows = tree.find((n) => n.id === "flows");
18
+ expect(flows?.children?.some((c) => c.label === "hello")).toBe(true);
19
+ const ai = tree.find((n) => n.id === "ai");
20
+ expect(ai?.children?.some((c) => c.id === "ai.models")).toBe(true);
21
+ });
22
+ });
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Project Navigator — Manifest tree with expand/collapse.
3
+ */
4
+
5
+ import { Box, Text, useInput } from "ink";
6
+ import { useEffect, useState, type ReactElement } from "react";
7
+ import type { Manifest } from "../../../manifest/types.ts";
8
+ import { TUI_ACCENT, TUI_ERR, TUI_MUTED } from "../theme.ts";
9
+
10
+ /** Props for {@link NavigatorPanel}. */
11
+ export type NavigatorPanelProps = {
12
+ readonly cwd: string;
13
+ readonly active: boolean;
14
+ };
15
+
16
+ type NavNode = {
17
+ readonly id: string;
18
+ readonly label: string;
19
+ readonly kind?: "category" | "item" | "meta";
20
+ readonly count?: number;
21
+ readonly children?: readonly NavNode[];
22
+ };
23
+
24
+ /**
25
+ * Build a tree from a Manifest.
26
+ *
27
+ * @param manifest - Parsed/extracted manifest
28
+ */
29
+ export function manifestToNavTree(manifest: Manifest): readonly NavNode[] {
30
+ const section = (title: string, record: Record<string, unknown> | undefined): NavNode => {
31
+ const keys = record ? Object.keys(record).sort() : [];
32
+ return {
33
+ id: title,
34
+ label: title,
35
+ kind: "category",
36
+ count: keys.length,
37
+ children: keys.map((name) => ({
38
+ id: `${title}:${name}`,
39
+ label: name,
40
+ kind: "item" as const,
41
+ })),
42
+ };
43
+ };
44
+
45
+ const ai = manifest.ai;
46
+ const aiChildren: NavNode[] = [];
47
+ if (ai?.models) {
48
+ const keys = Object.keys(ai.models);
49
+ aiChildren.push({
50
+ id: "ai.models",
51
+ label: "models",
52
+ kind: "category",
53
+ count: keys.length,
54
+ children: keys.map((n) => ({ id: `ai.models:${n}`, label: n, kind: "item" })),
55
+ });
56
+ }
57
+ if (ai?.prompts) {
58
+ const keys = Object.keys(ai.prompts);
59
+ aiChildren.push({
60
+ id: "ai.prompts",
61
+ label: "prompts",
62
+ kind: "category",
63
+ count: keys.length,
64
+ children: keys.map((n) => ({ id: `ai.prompts:${n}`, label: n, kind: "item" })),
65
+ });
66
+ }
67
+ if (ai?.agents) {
68
+ const keys = Object.keys(ai.agents);
69
+ aiChildren.push({
70
+ id: "ai.agents",
71
+ label: "agents",
72
+ kind: "category",
73
+ count: keys.length,
74
+ children: keys.map((n) => ({ id: `ai.agents:${n}`, label: n, kind: "item" })),
75
+ });
76
+ }
77
+
78
+ return [
79
+ { id: "app", label: `app: ${manifest.app}`, kind: "meta" },
80
+ section("flows", manifest.flows as Record<string, unknown> | undefined),
81
+ section("signals", manifest.signals as Record<string, unknown> | undefined),
82
+ section("stores", manifest.stores as Record<string, unknown> | undefined),
83
+ section("clocks", manifest.clocks as Record<string, unknown> | undefined),
84
+ section("gates", manifest.gates as Record<string, unknown> | undefined),
85
+ section("vault", manifest.vault as Record<string, unknown> | undefined),
86
+ section("channels", manifest.channels as Record<string, unknown> | undefined),
87
+ {
88
+ id: "ai",
89
+ label: "ai",
90
+ kind: "category",
91
+ count: aiChildren.length,
92
+ children: aiChildren,
93
+ },
94
+ ];
95
+ }
96
+
97
+ /**
98
+ * Flatten tree for arrow-key navigation.
99
+ *
100
+ * @param nodes - Roots
101
+ * @param expanded - Expanded ids
102
+ */
103
+ function flatten(
104
+ nodes: readonly NavNode[],
105
+ expanded: ReadonlySet<string>,
106
+ depth = 0,
107
+ ): readonly { node: NavNode; depth: number }[] {
108
+ const out: { node: NavNode; depth: number }[] = [];
109
+ for (const node of nodes) {
110
+ out.push({ node, depth });
111
+ if (node.children && expanded.has(node.id)) {
112
+ out.push(...flatten(node.children, expanded, depth + 1));
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+
118
+ /**
119
+ * Manifest browser.
120
+ *
121
+ * @param props - cwd
122
+ */
123
+ export function NavigatorPanel(props: NavigatorPanelProps): ReactElement {
124
+ const [tree, setTree] = useState<readonly NavNode[]>([]);
125
+ const [expanded, setExpanded] = useState<Set<string>>(
126
+ () => new Set(["flows", "signals", "stores"]),
127
+ );
128
+ const [selected, setSelected] = useState(0);
129
+ const [error, setError] = useState("");
130
+
131
+ useEffect(() => {
132
+ let cancelled = false;
133
+ void (async () => {
134
+ try {
135
+ const { extractManifest } = await import("../../../compiler/extract.ts");
136
+ const manifest = await extractManifest({ rootDir: props.cwd });
137
+ if (!cancelled) {
138
+ setTree(manifestToNavTree(manifest));
139
+ setError("");
140
+ }
141
+ } catch (err) {
142
+ if (!cancelled) setError(err instanceof Error ? err.message : String(err));
143
+ }
144
+ })();
145
+ return () => {
146
+ cancelled = true;
147
+ };
148
+ }, [props.cwd]);
149
+
150
+ const rows = flatten(tree, expanded);
151
+
152
+ const toggle = (expand: boolean | "toggle"): void => {
153
+ const row = rows[selected];
154
+ if (!row?.node.children?.length) return;
155
+ setExpanded((prev) => {
156
+ const next = new Set(prev);
157
+ if (expand === true) next.add(row.node.id);
158
+ else if (expand === false) next.delete(row.node.id);
159
+ else if (next.has(row.node.id)) next.delete(row.node.id);
160
+ else next.add(row.node.id);
161
+ return next;
162
+ });
163
+ };
164
+
165
+ useInput(
166
+ (_input, key) => {
167
+ if (!props.active) return;
168
+ if (key.upArrow) setSelected((i) => Math.max(0, i - 1));
169
+ if (key.downArrow) setSelected((i) => Math.min(Math.max(0, rows.length - 1), i + 1));
170
+ if (key.rightArrow) toggle(true);
171
+ if (key.leftArrow) toggle(false);
172
+ if (key.return) toggle("toggle");
173
+ },
174
+ { isActive: props.active },
175
+ );
176
+
177
+ return (
178
+ <Box flexDirection="column" gap={0} flexGrow={1}>
179
+ <Text bold color={TUI_ACCENT}>
180
+ Project Navigator
181
+ </Text>
182
+ <Text color={TUI_MUTED} dimColor>
183
+ Manifest tree · ↑↓ · → expand · ← collapse · Enter toggle
184
+ </Text>
185
+ {error ? <Text color={TUI_ERR}>{error}</Text> : null}
186
+ {rows.map((row, i) => {
187
+ const hasKids = Boolean(row.node.children?.length);
188
+ const open = expanded.has(row.node.id);
189
+ const branch = hasKids ? (open ? "▾ " : "▸ ") : " ";
190
+ const on = props.active && i === selected;
191
+ const isCategory = row.node.kind === "category";
192
+ return (
193
+ <Text
194
+ key={row.node.id}
195
+ bold={on || isCategory}
196
+ color={on ? TUI_ACCENT : isCategory ? TUI_ACCENT : undefined}
197
+ >
198
+ {" ".repeat(row.depth)}
199
+ {on ? "› " : " "}
200
+ {branch}
201
+ {row.node.label}
202
+ {typeof row.node.count === "number" ? (
203
+ <Text color={TUI_MUTED}>{` (${row.node.count})`}</Text>
204
+ ) : null}
205
+ </Text>
206
+ );
207
+ })}
208
+ {rows.length === 0 && !error ? (
209
+ <Text color={TUI_MUTED}>Loading Manifest… extract from src/ or oke.manifest.json</Text>
210
+ ) : null}
211
+ </Box>
212
+ );
213
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { commonPrefix, filterSlashActions, parseSlashArgv, slashCatalog } from "./slash-catalog.ts";
3
+
4
+ describe("slashCatalog", () => {
5
+ test("includes panel jumps and core cli commands", () => {
6
+ const all = slashCatalog();
7
+ expect(all.some((a) => a.id === "panel dashboard")).toBe(true);
8
+ expect(all.some((a) => a.id === "dev")).toBe(true);
9
+ expect(all.some((a) => a.id === "db push")).toBe(true);
10
+ expect(all.some((a) => a.id === "db studio")).toBe(true);
11
+ expect(all.some((a) => a.id === "doctor")).toBe(true);
12
+ });
13
+
14
+ test("filterSlashActions prefixes db", () => {
15
+ const hits = filterSlashActions("db", 20);
16
+ expect(hits.length).toBeGreaterThan(0);
17
+ expect(hits.every((h) => h.id.includes("db") || h.summary.toLowerCase().includes("db"))).toBe(
18
+ true,
19
+ );
20
+ expect(hits[0]?.id.startsWith("db")).toBe(true);
21
+ });
22
+
23
+ test("filterSlashActions empty returns top slice", () => {
24
+ expect(filterSlashActions("", 5)).toHaveLength(5);
25
+ });
26
+
27
+ test("commonPrefix and parseSlashArgv", () => {
28
+ expect(commonPrefix(["db push", "db studio", "db seed"])).toBe("db ");
29
+ expect(parseSlashArgv(" db push ")).toEqual(["db", "push"]);
30
+ });
31
+ });