okengine 0.10.2 → 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 (343) hide show
  1. package/package.json +76 -10
  2. package/site/content/docs/ai/mcp.mdx +0 -1
  3. package/site/content/docs/deployment/docker-swarm.mdx +48 -111
  4. package/site/content/docs/deployment/docker.mdx +43 -36
  5. package/site/content/docs/deployment/index.mdx +9 -9
  6. package/site/content/docs/deployment/kubernetes.mdx +4 -4
  7. package/site/content/docs/elements/ai.mdx +7 -13
  8. package/site/content/docs/elements/channel.mdx +9 -13
  9. package/site/content/docs/elements/clock.mdx +8 -14
  10. package/site/content/docs/elements/flow.mdx +46 -54
  11. package/site/content/docs/elements/gate.mdx +6 -11
  12. package/site/content/docs/elements/signal.mdx +2 -8
  13. package/site/content/docs/elements/store.mdx +79 -93
  14. package/site/content/docs/elements/vault.mdx +89 -76
  15. package/site/content/docs/get-started/basic-usage.mdx +8 -14
  16. package/site/content/docs/get-started/installation.mdx +36 -36
  17. package/site/content/docs/get-started/introduction.mdx +11 -0
  18. package/site/content/docs/get-started/why.mdx +3 -4
  19. package/site/content/docs/index.mdx +0 -1
  20. package/site/content/docs/meta.json +0 -1
  21. package/site/content/docs/plugins/anonymous.mdx +1 -1
  22. package/site/content/docs/plugins/magic-link.mdx +11 -11
  23. package/site/content/docs/plugins/otp.mdx +2 -2
  24. package/site/content/docs/plugins/passkey.mdx +1 -1
  25. package/site/content/docs/plugins/two-factor.mdx +1 -1
  26. package/site/content/docs/plugins/username.mdx +1 -1
  27. package/site/content/docs/recipes/caddy.mdx +1 -1
  28. package/site/content/docs/recipes/cockroachdb.mdx +2 -2
  29. package/site/content/docs/recipes/dragonfly.mdx +1 -1
  30. package/site/content/docs/recipes/index.mdx +1 -2
  31. package/site/content/docs/recipes/llama-cpp.mdx +21 -23
  32. package/site/content/docs/recipes/mailpit.mdx +11 -11
  33. package/site/content/docs/recipes/meilisearch.mdx +10 -10
  34. package/site/content/docs/recipes/meta.json +0 -1
  35. package/site/content/docs/recipes/ollama.mdx +1 -1
  36. package/site/content/docs/recipes/pgdog.mdx +12 -9
  37. package/site/content/docs/recipes/postgres.mdx +3 -3
  38. package/site/content/docs/recipes/redis.mdx +2 -2
  39. package/site/content/docs/recipes/rustfs.mdx +10 -7
  40. package/site/content/docs/recipes/sglang.mdx +1 -1
  41. package/site/content/docs/recipes/supabase-docker.mdx +1 -1
  42. package/site/content/docs/recipes/timescale.mdx +2 -2
  43. package/site/content/docs/recipes/valkey.mdx +1 -1
  44. package/site/content/docs/recipes/vllm.mdx +1 -1
  45. package/site/content/docs/recipes/yugabytedb.mdx +2 -2
  46. package/site/content/docs/reference/cli.md +61 -38
  47. package/site/content/docs/reference/configuration.mdx +145 -70
  48. package/site/content/docs/reference/environment-variables.mdx +36 -28
  49. package/site/content/docs/reference/errors.mdx +28 -29
  50. package/site/content/docs/reference/fx.mdx +14 -7
  51. package/site/content/docs/reference/i18n.mdx +17 -6
  52. package/site/content/docs/reference/index.mdx +5 -0
  53. package/site/content/docs/reference/meta.json +1 -0
  54. package/site/content/docs/reference/migrating-environments.mdx +158 -0
  55. package/site/content/docs/reference/plugins.mdx +2 -2
  56. package/src/auth/gate-auth.test.ts +12 -10
  57. package/src/cli/ai-setup/ai-setup.test.ts +30 -16
  58. package/src/cli/ai-setup/apply.ts +111 -15
  59. package/src/cli/ai-setup/catalog.ts +3 -3
  60. package/src/cli/ai-setup/index.ts +1 -1
  61. package/src/cli/ai-setup/prompts.ts +1 -1
  62. package/src/cli/ai-setup/recommend.test.ts +1 -1
  63. package/src/cli/ask-seed.test.ts +3 -3
  64. package/src/cli/build.ts +8 -9
  65. package/src/cli/db-auto-push.test.ts +6 -3
  66. package/src/cli/db-auto-push.ts +7 -5
  67. package/src/cli/db-seed.ts +7 -8
  68. package/src/cli/db.test.ts +70 -71
  69. package/src/cli/db.ts +86 -12
  70. package/src/cli/dev-controller.test.ts +35 -0
  71. package/src/cli/dev-controller.ts +298 -0
  72. package/src/cli/dev-controls.ts +11 -65
  73. package/src/cli/dev-db-push.test.ts +47 -15
  74. package/src/cli/dev-schema-sync.test.ts +11 -11
  75. package/src/cli/dev-schema-sync.ts +10 -10
  76. package/src/cli/dev-session-lock.test.ts +106 -0
  77. package/src/cli/dev-session-lock.ts +209 -0
  78. package/src/cli/dev.test.ts +246 -114
  79. package/src/cli/dev.ts +153 -205
  80. package/src/cli/doc-staleness.test.ts +3 -6
  81. package/src/cli/docker-clean.test.ts +33 -1
  82. package/src/cli/docker-clean.ts +28 -33
  83. package/src/cli/docker-cli.test.ts +12 -11
  84. package/src/cli/docker.ts +95 -20
  85. package/src/cli/doctor.test.ts +40 -0
  86. package/src/cli/doctor.ts +142 -9
  87. package/src/cli/drizzle-env.test.ts +11 -13
  88. package/src/cli/drizzle-env.ts +18 -11
  89. package/src/cli/ensure-drizzle-config.ts +11 -14
  90. package/src/cli/hero-meta.test.ts +15 -15
  91. package/src/cli/hero-meta.ts +10 -9
  92. package/src/cli/index.ts +36 -6
  93. package/src/cli/load-config.images.test.ts +44 -10
  94. package/src/cli/load-config.source.test.ts +50 -0
  95. package/src/cli/load-config.ts +35 -29
  96. package/src/cli/meilisearch-local.ts +1 -2
  97. package/src/cli/mode.ts +7 -58
  98. package/src/cli/registry.ts +175 -19
  99. package/src/cli/resolve-dev-sql-env.test.ts +8 -16
  100. package/src/cli/resolve-dev-sql-env.ts +13 -15
  101. package/src/cli/safe-defaults.test.ts +29 -11
  102. package/src/cli/schema.ts +65 -16
  103. package/src/cli/test.ts +58 -0
  104. package/src/cli/tui/App.tsx +252 -0
  105. package/src/cli/tui/DevLive.tsx +116 -0
  106. package/src/cli/tui/components/ConfirmDialog.tsx +48 -0
  107. package/src/cli/tui/components/HeaderBar.tsx +67 -0
  108. package/src/cli/tui/components/LogStream.tsx +47 -0
  109. package/src/cli/tui/components/PanelCard.tsx +39 -0
  110. package/src/cli/tui/components/SelectRow.tsx +37 -0
  111. package/src/cli/tui/components/SlashPalette.tsx +172 -0
  112. package/src/cli/tui/components/Spinner.tsx +32 -0
  113. package/src/cli/tui/components/StatusBar.tsx +51 -0
  114. package/src/cli/tui/components/StatusDot.tsx +37 -0
  115. package/src/cli/tui/components/TabBar.tsx +49 -0
  116. package/src/cli/tui/components/Wordmark.tsx +43 -0
  117. package/src/cli/tui/format.ts +38 -0
  118. package/src/cli/tui/keys.test.ts +18 -0
  119. package/src/cli/tui/keys.ts +26 -0
  120. package/src/cli/tui/launch.test.ts +18 -0
  121. package/src/cli/tui/launch.ts +57 -0
  122. package/src/cli/tui/panels/Dashboard.tsx +201 -0
  123. package/src/cli/tui/panels/Database.tsx +198 -0
  124. package/src/cli/tui/panels/Dev.tsx +257 -0
  125. package/src/cli/tui/panels/Docker.tsx +357 -0
  126. package/src/cli/tui/panels/Navigator.test.ts +22 -0
  127. package/src/cli/tui/panels/Navigator.tsx +213 -0
  128. package/src/cli/tui/slash-catalog.test.ts +31 -0
  129. package/src/cli/tui/slash-catalog.ts +184 -0
  130. package/src/cli/tui/slash-run.ts +54 -0
  131. package/src/cli/tui/state/ports.ts +95 -0
  132. package/src/cli/tui/theme.ts +24 -0
  133. package/src/cli/vault-cmd.test.ts +402 -0
  134. package/src/cli/vault-cmd.ts +727 -69
  135. package/src/cli/vault-secure-input.test.ts +56 -0
  136. package/src/cli/vault-secure-input.ts +124 -0
  137. package/src/compiler/effects-infer.ts +7 -0
  138. package/src/compiler/extract.ts +12 -0
  139. package/src/compiler/fixtures/skyport/oke.config.ts +7 -5
  140. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
  141. package/src/compiler/fixtures/skyport.expected.json +1 -1
  142. package/src/compiler/generate-adopt.ts +27 -1
  143. package/src/compiler-entry.ts +7 -0
  144. package/src/config/define-config.test.ts +111 -47
  145. package/src/config/driver-defaults.test.ts +115 -0
  146. package/src/config/driver-defaults.ts +156 -0
  147. package/src/config/index.ts +246 -131
  148. package/src/config/resolve-driver.test.ts +14 -19
  149. package/src/console/server/app.ts +4 -2
  150. package/src/console/server/console.test.ts +58 -1
  151. package/src/console/server/flows.ts +22 -2
  152. package/src/console/server/index.ts +3 -0
  153. package/src/console/server/operator-db.test.ts +55 -13
  154. package/src/console/server/operator-db.ts +305 -263
  155. package/src/console/server/serve.ts +16 -7
  156. package/src/console/server/state.ts +31 -9
  157. package/src/console/server/vault.test.ts +120 -5
  158. package/src/console/server/vault.ts +208 -12
  159. package/src/console/ui/dist/assets/{index-D-9KFce5.js → index--Y0Uaz-m.js} +2 -2
  160. package/src/console/ui/dist/assets/panel-vault-BkZdJPX8.js +1 -0
  161. package/src/console/ui/dist/assets/style-C3L349iZ.css +3 -0
  162. package/src/console/ui/dist/index.html +2 -2
  163. package/src/console/ui/shell/client.ts +16 -0
  164. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +54 -1
  165. package/src/console/ui/vault/VaultA11yView.tsx +33 -1
  166. package/src/console/ui/vault/backend.test.ts +75 -0
  167. package/src/console/ui/vault/backend.ts +117 -0
  168. package/src/console/ui/vault/fixture.ts +21 -2
  169. package/src/console/ui/vault/index.ts +11 -1
  170. package/src/console/ui/vault/types.ts +26 -0
  171. package/src/docker/compose.ts +164 -61
  172. package/src/docker/derive.ts +106 -12
  173. package/src/docker/docker.test.ts +156 -110
  174. package/src/docker/helpers.ts +98 -0
  175. package/src/docker/images-config.test.ts +163 -0
  176. package/src/docker/index.ts +30 -5
  177. package/src/docker/recipes/index.ts +0 -3
  178. package/src/docker/recipes/llama-cpp.ts +4 -4
  179. package/src/docker/recipes/pgdog.ts +9 -2
  180. package/src/docker/resources.test.ts +71 -0
  181. package/src/docker/resources.ts +176 -0
  182. package/src/docker/stack-id.test.ts +11 -4
  183. package/src/docker/stack-id.ts +42 -32
  184. package/src/docker/stack.integration.test.ts +4 -15
  185. package/src/docker/types.ts +35 -3
  186. package/src/drivers/ai-ollama.integration.test.ts +2 -2
  187. package/src/drivers/conformance.test.ts +11 -12
  188. package/src/drivers/drizzle-dialect.test.ts +0 -4
  189. package/src/drivers/drizzle-dialect.ts +7 -13
  190. package/src/drivers/index.ts +24 -7
  191. package/src/drivers/journal-postgres.test.ts +3 -1
  192. package/src/drivers/journal-postgres.ts +4 -2
  193. package/src/drivers/meilisearch.ts +1 -1
  194. package/src/drivers/pglite.ts +28 -5
  195. package/src/drivers/types.ts +9 -11
  196. package/src/drivers/vault-aws-secrets-manager.test.ts +159 -0
  197. package/src/drivers/vault-aws-secrets-manager.ts +333 -0
  198. package/src/drivers/vault-builtin.test.ts +110 -0
  199. package/src/drivers/vault-builtin.ts +184 -0
  200. package/src/drivers/vault-driver-removal.test.ts +13 -0
  201. package/src/drivers/vault-managed.test.ts +95 -0
  202. package/src/drivers/vault-managed.ts +101 -25
  203. package/src/drivers/vault-types.ts +25 -5
  204. package/src/elements/ai/runtime.ts +1 -1
  205. package/src/elements/clock/chaos.test.ts +4 -1
  206. package/src/elements/clock/durable.ts +57 -4
  207. package/src/elements/clock.test.ts +13 -14
  208. package/src/elements/gate/constants.ts +18 -0
  209. package/src/elements/gate.test.ts +1 -1
  210. package/src/elements/gate.ts +2 -2
  211. package/src/elements/store/domain-ddl.test.ts +5 -12
  212. package/src/elements/store/emit-drizzle.ts +44 -19
  213. package/src/elements/store/index-boot.test.ts +15 -116
  214. package/src/elements/store/load-plugin-tables.ts +1 -1
  215. package/src/elements/store/resource-list-docs.fixture.ts +12 -6
  216. package/src/elements/store/resource-list-docs.test.ts +64 -44
  217. package/src/elements/store/resource.test.ts +17 -10
  218. package/src/elements/store/runtime.ts +9 -12
  219. package/src/elements/store/schema-decl.test.ts +16 -32
  220. package/src/elements/store/seed.test.ts +1 -2
  221. package/src/elements/store/seed.ts +7 -8
  222. package/src/elements/store/sql-condition.test.ts +2 -2
  223. package/src/elements/store/sql-session.test.ts +8 -10
  224. package/src/elements/store/sql-session.ts +10 -2
  225. package/src/elements/store.test.ts +13 -13
  226. package/src/elements/vault/audit.ts +233 -0
  227. package/src/elements/vault/boot-chain.test.ts +78 -0
  228. package/src/elements/vault/boot-chain.ts +43 -39
  229. package/src/elements/vault/builtin-adapter.test.ts +479 -0
  230. package/src/elements/vault/builtin-adapter.ts +1184 -0
  231. package/src/elements/vault/crypto.test.ts +237 -0
  232. package/src/elements/vault/crypto.ts +431 -0
  233. package/src/elements/vault/declare.ts +164 -3
  234. package/src/elements/vault/errors.ts +56 -0
  235. package/src/elements/vault/kms-unseal.test.ts +76 -0
  236. package/src/elements/vault/kms-unseal.ts +181 -0
  237. package/src/elements/vault/path.ts +90 -0
  238. package/src/elements/vault/resilience.test.ts +102 -0
  239. package/src/elements/vault/resilience.ts +107 -0
  240. package/src/elements/vault/runtime.ts +36 -4
  241. package/src/elements/vault/security-checklist.test.ts +130 -0
  242. package/src/elements/vault/storage.ts +448 -0
  243. package/src/elements/vault/types.ts +288 -0
  244. package/src/elements/vault/unseal.ts +150 -0
  245. package/src/elements/vault.test.ts +72 -4
  246. package/src/elements/vault.ts +91 -2
  247. package/src/full.ts +248 -0
  248. package/src/http.ts +41 -0
  249. package/src/i18n/interpolate-simple.ts +37 -0
  250. package/src/i18n/messages.ts +51 -4
  251. package/src/i18n-entry.ts +7 -0
  252. package/src/index.ts +35 -145
  253. package/src/journal-entry.ts +20 -0
  254. package/src/kernel/adopt-barrel-fresh.test.ts +25 -10
  255. package/src/kernel/app.ts +40 -42
  256. package/src/kernel/auto-registry.test.ts +2 -2
  257. package/src/kernel/boot-bind/ai.test.ts +5 -5
  258. package/src/kernel/boot-bind/ai.ts +2 -1
  259. package/src/kernel/boot-bind/channel.test.ts +6 -6
  260. package/src/kernel/boot-bind/channel.ts +5 -4
  261. package/src/kernel/boot-bind/clock.ts +5 -5
  262. package/src/kernel/boot-bind/gate.ts +6 -7
  263. package/src/kernel/boot-bind/honor-config.test.ts +42 -42
  264. package/src/kernel/boot-bind/journal.ts +5 -5
  265. package/src/kernel/boot-bind/signal.ts +3 -1
  266. package/src/kernel/boot-bind/store.test.ts +55 -37
  267. package/src/kernel/boot-bind/store.ts +24 -34
  268. package/src/kernel/boot-bind/vault.ts +19 -4
  269. package/src/kernel/boot.test.ts +12 -4
  270. package/src/kernel/boot.ts +31 -19
  271. package/src/kernel/budget-entry.ts +1 -1
  272. package/src/kernel/compensate.test.ts +288 -1
  273. package/src/kernel/compensate.ts +126 -0
  274. package/src/kernel/concurrency.test.ts +10 -9
  275. package/src/kernel/effects-stamping.test.ts +27 -2
  276. package/src/kernel/element-registries.ts +5 -3
  277. package/src/kernel/flow.ts +4 -2
  278. package/src/kernel/fx.test.ts +119 -3
  279. package/src/kernel/fx.ts +215 -40
  280. package/src/kernel/index.ts +14 -0
  281. package/src/kernel/journal.ts +130 -7
  282. package/src/kernel/pipeline.test.ts +20 -6
  283. package/src/kernel/redacted.test.ts +35 -0
  284. package/src/kernel/redacted.ts +24 -2
  285. package/src/kernel/router/create-default.ts +16 -0
  286. package/src/kernel/router/create-edge.ts +18 -0
  287. package/src/kernel/router/create.ts +23 -0
  288. package/src/kernel/router/index.ts +23 -0
  289. package/src/kernel/router/linear.ts +88 -0
  290. package/src/kernel/router/regexp.ts +188 -0
  291. package/src/kernel/router/smart.ts +120 -0
  292. package/src/kernel/router/trie.ts +108 -0
  293. package/src/kernel/router/types.ts +56 -0
  294. package/src/kernel/router.ts +18 -532
  295. package/src/kernel-entry.ts +43 -0
  296. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  297. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  298. package/src/plugins/config-source.test.ts +7 -7
  299. package/src/release/build-lib.ts +58 -0
  300. package/src/release/exports.test.ts +4 -2
  301. package/src/release/exports.ts +30 -4
  302. package/src/release/measure.ts +101 -22
  303. package/src/runs/duckdb.ts +7 -2
  304. package/src/shared/optional-peer.ts +41 -0
  305. package/src/term.test.ts +3 -44
  306. package/src/test/create-test-app.ts +12 -0
  307. package/src/test/provisions.integration.test.ts +1 -1
  308. package/src/test/reset-element-registries.ts +2 -0
  309. package/src/upgrade/codemods.test.ts +7 -20
  310. package/src/upgrade/codemods.ts +93 -60
  311. package/site/content/docs/console/access.mdx +0 -29
  312. package/site/content/docs/console/ai.mdx +0 -35
  313. package/site/content/docs/console/architecture.mdx +0 -35
  314. package/site/content/docs/console/channels.mdx +0 -37
  315. package/site/content/docs/console/clock.mdx +0 -51
  316. package/site/content/docs/console/flows.mdx +0 -31
  317. package/site/content/docs/console/gates.mdx +0 -73
  318. package/site/content/docs/console/index.mdx +0 -54
  319. package/site/content/docs/console/manifest-diff.mdx +0 -34
  320. package/site/content/docs/console/meta.json +0 -24
  321. package/site/content/docs/console/overview.mdx +0 -40
  322. package/site/content/docs/console/plugins.mdx +0 -41
  323. package/site/content/docs/console/privacy.mdx +0 -32
  324. package/site/content/docs/console/runs.mdx +0 -50
  325. package/site/content/docs/console/signals.mdx +0 -40
  326. package/site/content/docs/console/store.mdx +0 -32
  327. package/site/content/docs/console/tenancy.mdx +0 -32
  328. package/site/content/docs/console/traces.mdx +0 -38
  329. package/site/content/docs/console/vault.mdx +0 -41
  330. package/site/content/docs/recipes/openbao.mdx +0 -141
  331. package/src/cli/ask-dev-mode.ts +0 -40
  332. package/src/cli/dev-mode.test.ts +0 -45
  333. package/src/cli/dev-mode.ts +0 -78
  334. package/src/cli/openbao-bootstrap.test.ts +0 -150
  335. package/src/cli/openbao-bootstrap.ts +0 -321
  336. package/src/cli/openbao-restart.integration.test.ts +0 -145
  337. package/src/console/ui/dist/assets/panel-vault-Za1GyJfM.js +0 -1
  338. package/src/console/ui/dist/assets/style-C8MxEWPd.css +0 -3
  339. package/src/docker/recipes/openbao.ts +0 -47
  340. package/src/drivers/libsql.ts +0 -179
  341. package/src/drivers/sqlite.ts +0 -44
  342. package/src/drivers/vault-openbao.test.ts +0 -97
  343. package/src/drivers/vault-openbao.ts +0 -143
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Dev panel — Not Running / Running states, inline shortcuts, live logs.
3
+ */
4
+
5
+ import { Box, Text, useInput } from "ink";
6
+ import { useEffect, useMemo, useState, type ReactElement } from "react";
7
+ import { DevController } from "../../dev-controller.ts";
8
+ import type { DevOwnership } from "../../dev-session-lock.ts";
9
+ import type { FooterStatus } from "../components/StatusBar.tsx";
10
+ import { StatusDot } from "../components/StatusDot.tsx";
11
+ import { formatUptime } from "../format.ts";
12
+ import { mapDevControlInput } from "../keys.ts";
13
+ import { pollPortSnapshot, type PortSnapshot } from "../state/ports.ts";
14
+ import { TUI_ACCENT, TUI_MUTED, TUI_OK, TUI_WARN } from "../theme.ts";
15
+
16
+ /** Props for {@link DevPanel}. */
17
+ export type DevPanelProps = {
18
+ readonly cwd: string;
19
+ readonly controller: DevController;
20
+ readonly onLog: (line: string) => void;
21
+ readonly active: boolean;
22
+ readonly logs: readonly string[];
23
+ readonly onStatus: (status: FooterStatus) => void;
24
+ };
25
+
26
+ type ActionId = "start" | "stop" | "restart" | "attach";
27
+
28
+ /**
29
+ * Dev controls panel.
30
+ *
31
+ * @param props - Controller + cwd
32
+ */
33
+ export function DevPanel(props: DevPanelProps): ReactElement {
34
+ const [ownership, setOwnership] = useState<DevOwnership>("stopped");
35
+ const [snap, setSnap] = useState<PortSnapshot | null>(null);
36
+ const [busy, setBusy] = useState(false);
37
+ const [startedAt, setStartedAt] = useState<number | null>(null);
38
+ const [now, setNow] = useState(Date.now());
39
+ const [selected, setSelected] = useState(0);
40
+
41
+ useEffect(() => {
42
+ const off = props.controller.on((e) => {
43
+ if (e.type === "ownership") setOwnership(e.ownership);
44
+ if (e.type === "ready") {
45
+ setOwnership(e.ownership);
46
+ if (e.ownership === "managed") setStartedAt(Date.now());
47
+ }
48
+ if (e.type === "log") props.onLog(e.text);
49
+ if (e.type === "error") {
50
+ props.onLog(`${e.message}\n`);
51
+ props.onStatus({ kind: "done", label: e.message });
52
+ }
53
+ if (e.type === "exit") setStartedAt(null);
54
+ });
55
+ void props.controller.attach(props.cwd).then((r) => {
56
+ setOwnership(r.ownership);
57
+ if (r.ownership === "managed" && r.lock) {
58
+ setStartedAt(Date.parse(r.lock.startedAt) || Date.now());
59
+ }
60
+ });
61
+ return off;
62
+ }, [props.controller, props.cwd, props.onLog, props.onStatus]);
63
+
64
+ useEffect(() => {
65
+ const id = setInterval(() => {
66
+ void pollPortSnapshot(props.cwd).then((s) => {
67
+ setOwnership(s.ownership);
68
+ setSnap(s);
69
+ });
70
+ setNow(Date.now());
71
+ }, 1500);
72
+ return () => clearInterval(id);
73
+ }, [props.cwd]);
74
+
75
+ const actions = useMemo(() => {
76
+ const running = ownership === "managed";
77
+ const external = ownership === "external";
78
+ return [
79
+ {
80
+ id: "start" as const,
81
+ label: "[S]tart",
82
+ enabled: ownership === "stopped",
83
+ },
84
+ {
85
+ id: "restart" as const,
86
+ label: "[R]estart",
87
+ enabled: running,
88
+ },
89
+ {
90
+ id: "stop" as const,
91
+ label: "[X]top",
92
+ enabled: running,
93
+ },
94
+ {
95
+ id: "attach" as const,
96
+ label: "[A]ttach",
97
+ enabled: true,
98
+ },
99
+ ].filter((a) => !(external && (a.id === "start" || a.id === "restart" || a.id === "stop")));
100
+ }, [ownership]);
101
+
102
+ useInput(
103
+ (input, key) => {
104
+ if (!props.active || busy) return;
105
+ const ctrl = mapDevControlInput(input, key);
106
+ if (ctrl === "q") return; // Esc handled by App
107
+ if (ctrl === "r" || input === "r" || input === "R") {
108
+ void runAction("restart");
109
+ return;
110
+ }
111
+ if (input === "s" || input === "S") {
112
+ void runAction("start");
113
+ return;
114
+ }
115
+ if (input === "x" || input === "X") {
116
+ void runAction("stop");
117
+ return;
118
+ }
119
+ if (input === "a" || input === "A") {
120
+ void runAction("attach");
121
+ return;
122
+ }
123
+ if (key.upArrow) {
124
+ setSelected((i) => Math.max(0, i - 1));
125
+ return;
126
+ }
127
+ if (key.downArrow) {
128
+ setSelected((i) => Math.min(actions.length - 1, i + 1));
129
+ return;
130
+ }
131
+ if (key.return) {
132
+ const action = actions[selected];
133
+ if (action) void runAction(action.id);
134
+ }
135
+ },
136
+ { isActive: props.active },
137
+ );
138
+
139
+ const runAction = async (id: ActionId): Promise<void> => {
140
+ if (busy) return;
141
+ setBusy(true);
142
+ const labels: Record<ActionId, string> = {
143
+ start: "Starting oke dev…",
144
+ stop: "Stopping…",
145
+ restart: "Restarting…",
146
+ attach: "Attaching…",
147
+ };
148
+ props.onStatus({ kind: "running", label: labels[id] });
149
+ try {
150
+ if (id === "start") {
151
+ const code = await props.controller.start({ cwd: props.cwd });
152
+ props.onStatus({
153
+ kind: "done",
154
+ label: code === 0 ? "Started" : "Start failed",
155
+ });
156
+ if (code === 0) setStartedAt(Date.now());
157
+ } else if (id === "stop") {
158
+ await props.controller.stop();
159
+ props.onStatus({ kind: "done", label: "Stopped" });
160
+ setStartedAt(null);
161
+ } else if (id === "restart") {
162
+ if (ownership !== "managed") {
163
+ props.onStatus({ kind: "done", label: "Nothing to restart" });
164
+ } else {
165
+ const code = await props.controller.restart({ cwd: props.cwd });
166
+ props.onStatus({
167
+ kind: "done",
168
+ label: code === 0 ? "Restarted" : "Restart failed",
169
+ });
170
+ if (code === 0) setStartedAt(Date.now());
171
+ }
172
+ } else if (id === "attach") {
173
+ const r = await props.controller.attach(props.cwd);
174
+ setOwnership(r.ownership);
175
+ props.onStatus({ kind: "done", label: `Ownership: ${r.ownership}` });
176
+ }
177
+ } finally {
178
+ setBusy(false);
179
+ }
180
+ };
181
+
182
+ const running = ownership === "managed" || ownership === "external";
183
+ const uptime = startedAt && ownership === "managed" ? formatUptime(now - startedAt) : null;
184
+ const app = snap?.surfaces.find((s) => s.id === "app");
185
+ const consoleSurf = snap?.surfaces.find((s) => s.id === "console");
186
+ const logTail = props.logs.slice(-Math.max(6, 12));
187
+
188
+ return (
189
+ <Box flexDirection="column" gap={1} flexGrow={1}>
190
+ <Box flexDirection="column">
191
+ <Text bold color={TUI_ACCENT}>
192
+ Dev Server
193
+ </Text>
194
+ {!running ? (
195
+ <Box flexDirection="column">
196
+ <Box gap={1}>
197
+ <Text color={TUI_WARN} bold>
198
+
199
+ </Text>
200
+ <Text bold color={TUI_WARN}>
201
+ Not Running
202
+ </Text>
203
+ </Box>
204
+ <Text color={TUI_MUTED}>Press [S] to start</Text>
205
+ </Box>
206
+ ) : (
207
+ <Box flexDirection="column">
208
+ <Box gap={1}>
209
+ <StatusDot status="ready" />
210
+ <Text bold color={TUI_OK}>
211
+ {ownership === "external" ? "Running (external)" : "Running"}
212
+ </Text>
213
+ {uptime ? <Text color={TUI_MUTED}>· uptime {uptime}</Text> : null}
214
+ </Box>
215
+ <Box gap={2}>
216
+ <Text color={TUI_MUTED}>Ports</Text>
217
+ <Text color={TUI_ACCENT}>App :{app?.port ?? "—"}</Text>
218
+ <Text color={TUI_ACCENT}>Console :{consoleSurf?.port ?? "—"}</Text>
219
+ </Box>
220
+ {ownership === "external" ? (
221
+ <Text color={TUI_WARN}>Read-only — will not start a second stack</Text>
222
+ ) : null}
223
+ </Box>
224
+ )}
225
+ </Box>
226
+
227
+ <Box gap={2}>
228
+ {actions.map((a, i) => (
229
+ <Text
230
+ key={a.id}
231
+ bold={props.active && i === selected}
232
+ color={!a.enabled ? TUI_MUTED : props.active && i === selected ? TUI_ACCENT : undefined}
233
+ dimColor={!a.enabled}
234
+ >
235
+ {props.active && i === selected ? "› " : " "}
236
+ {a.label}
237
+ </Text>
238
+ ))}
239
+ </Box>
240
+
241
+ <Box flexDirection="column" flexGrow={1} minHeight={6}>
242
+ <Text bold color={TUI_ACCENT}>
243
+ Live Logs
244
+ </Text>
245
+ {logTail.length === 0 ? (
246
+ <Text color={TUI_MUTED}>Start the dev server to see logs</Text>
247
+ ) : (
248
+ logTail.map((line, i) => (
249
+ <Text key={`${i}-${line.slice(0, 24)}`} wrap="truncate">
250
+ {line.replace(/\n$/, "")}
251
+ </Text>
252
+ ))
253
+ )}
254
+ </Box>
255
+ </Box>
256
+ );
257
+ }
@@ -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
+ });