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,58 @@
1
+ /**
2
+ * `oke test` — run `bun test` with PGLite test posture (no Docker Compose).
3
+ */
4
+
5
+ /**
6
+ * CLI entry for `oke test`.
7
+ *
8
+ * Sets `NODE_ENV=test` and a default in-memory PGLite URL, verifies
9
+ * `@electric-sql/pglite` resolves, then forwards remaining argv to `bun test`.
10
+ *
11
+ * @param args - Args after `test` (forwarded to `bun test`)
12
+ */
13
+ export async function testCli(args: readonly string[]): Promise<number> {
14
+ for (const a of args) {
15
+ if (a === "--help" || a === "-h") {
16
+ console.log(`oke test [...bun test args]
17
+
18
+ Run bun test with PGLite test posture (NODE_ENV=test).
19
+ Does not start Docker Compose.
20
+
21
+ Environment (set if unset):
22
+ NODE_ENV=test
23
+ OKE_PGLITE_URL=memory://
24
+ `);
25
+ return 0;
26
+ }
27
+ }
28
+
29
+ if (process.env.NODE_ENV === undefined || process.env.NODE_ENV === "") {
30
+ process.env.NODE_ENV = "test";
31
+ }
32
+ if (process.env.OKE_PGLITE_URL === undefined || process.env.OKE_PGLITE_URL === "") {
33
+ process.env.OKE_PGLITE_URL = "memory://";
34
+ }
35
+
36
+ try {
37
+ await import("@electric-sql/pglite");
38
+ } catch {
39
+ try {
40
+ Bun.resolveSync("@electric-sql/pglite", process.cwd());
41
+ } catch {
42
+ console.error(
43
+ "oke test: `@electric-sql/pglite` is not installed — add it as a dependency (okengine peers it for the pglite SQL driver)",
44
+ );
45
+ return 1;
46
+ }
47
+ }
48
+
49
+ const proc = Bun.spawn(["bun", "test", ...args], {
50
+ cwd: process.cwd(),
51
+ env: process.env as Record<string, string>,
52
+ stdin: "inherit",
53
+ stdout: "inherit",
54
+ stderr: "inherit",
55
+ });
56
+ const code = await proc.exited;
57
+ return code ?? 1;
58
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Interactive `oke` shell — brand chrome, focus layers, `/` palette.
3
+ */
4
+
5
+ import { Box, useApp, useInput } from "ink";
6
+ import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from "react";
7
+ import { DevController } from "../dev-controller.ts";
8
+ import type { DevOwnership } from "../dev-session-lock.ts";
9
+ import { ConfirmDialog } from "./components/ConfirmDialog.tsx";
10
+ import { HeaderBar } from "./components/HeaderBar.tsx";
11
+ import { SlashPalette } from "./components/SlashPalette.tsx";
12
+ import { StatusBar, type FooterStatus } from "./components/StatusBar.tsx";
13
+ import { TabBar } from "./components/TabBar.tsx";
14
+ import { DashboardPanel } from "./panels/Dashboard.tsx";
15
+ import { DatabasePanel } from "./panels/Database.tsx";
16
+ import { DevPanel } from "./panels/Dev.tsx";
17
+ import { DockerPanel } from "./panels/Docker.tsx";
18
+ import { NavigatorPanel } from "./panels/Navigator.tsx";
19
+ import { runSlashCli } from "./slash-run.ts";
20
+ import { pollPortSnapshot } from "./state/ports.ts";
21
+ import { TUI_MUTED } from "./theme.ts";
22
+
23
+ const PANELS = ["dashboard", "dev", "database", "docker", "navigator"] as const;
24
+ type PanelId = (typeof PANELS)[number];
25
+
26
+ const TABS = [
27
+ { id: "dashboard", label: "Dashboard" },
28
+ { id: "dev", label: "Dev" },
29
+ { id: "database", label: "Database" },
30
+ { id: "docker", label: "Docker" },
31
+ { id: "navigator", label: "Navigator" },
32
+ ] as const;
33
+
34
+ /** Props for {@link OkeTuiApp}. */
35
+ export type OkeTuiAppProps = {
36
+ readonly cwd: string;
37
+ };
38
+
39
+ /**
40
+ * Root Ink application.
41
+ *
42
+ * @param props - cwd
43
+ */
44
+ export function OkeTuiApp(props: OkeTuiAppProps): ReactElement {
45
+ const { exit } = useApp();
46
+ const [panelIndex, setPanelIndex] = useState(0);
47
+ const [focusLayer, setFocusLayer] = useState<"tabs" | "panel">("tabs");
48
+ const [logs, setLogs] = useState<string[]>([]);
49
+ const [slashOpen, setSlashOpen] = useState(false);
50
+ const [slashBusy, setSlashBusy] = useState(false);
51
+ const [quitConfirm, setQuitConfirm] = useState(false);
52
+ const [ownership, setOwnership] = useState<DevOwnership>("stopped");
53
+ const [footerStatus, setFooterStatus] = useState<FooterStatus>({ kind: "idle" });
54
+ const doneTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
55
+ const controller = useMemo(() => new DevController(), []);
56
+
57
+ const onLog = useCallback((line: string) => {
58
+ setLogs((prev) => {
59
+ const next = [...prev, ...line.split("\n").filter((l) => l.length > 0)];
60
+ return next.slice(-200);
61
+ });
62
+ }, []);
63
+
64
+ const onStatus = useCallback((status: FooterStatus) => {
65
+ if (doneTimer.current) {
66
+ clearTimeout(doneTimer.current);
67
+ doneTimer.current = null;
68
+ }
69
+ setFooterStatus(status);
70
+ if (status.kind === "done") {
71
+ doneTimer.current = setTimeout(() => {
72
+ setFooterStatus({ kind: "idle" });
73
+ doneTimer.current = null;
74
+ }, 2000);
75
+ }
76
+ }, []);
77
+
78
+ useEffect(() => {
79
+ const tick = (): void => {
80
+ void pollPortSnapshot(props.cwd).then((s) => setOwnership(s.ownership));
81
+ };
82
+ tick();
83
+ const id = setInterval(tick, 2000);
84
+ const off = controller.on((e) => {
85
+ if (e.type === "ownership" || e.type === "ready") {
86
+ setOwnership(e.ownership);
87
+ }
88
+ if (e.type === "exit") setOwnership("stopped");
89
+ });
90
+ return () => {
91
+ clearInterval(id);
92
+ off();
93
+ if (doneTimer.current) clearTimeout(doneTimer.current);
94
+ };
95
+ }, [controller, props.cwd]);
96
+
97
+ const panel = (PANELS[panelIndex] ?? "dashboard") as PanelId;
98
+ const panelActive = !slashOpen && !quitConfirm && focusLayer === "panel";
99
+
100
+ const jumpPanel = useCallback((panelId: string) => {
101
+ const idx = PANELS.indexOf(panelId as PanelId);
102
+ if (idx >= 0) {
103
+ setPanelIndex(idx);
104
+ setFocusLayer("panel");
105
+ }
106
+ }, []);
107
+
108
+ const runCli = useCallback(
109
+ async (argv: readonly string[]) => {
110
+ setSlashBusy(true);
111
+ onStatus({ kind: "running", label: `Running oke ${argv.join(" ")}…` });
112
+ try {
113
+ await runSlashCli(props.cwd, argv, onLog);
114
+ onStatus({ kind: "done", label: "Done" });
115
+ } catch {
116
+ onStatus({ kind: "done", label: "Failed" });
117
+ } finally {
118
+ setSlashBusy(false);
119
+ }
120
+ },
121
+ [props.cwd, onLog, onStatus],
122
+ );
123
+
124
+ const requestQuit = useCallback(() => {
125
+ if (ownership === "managed") {
126
+ setQuitConfirm(true);
127
+ return;
128
+ }
129
+ exit();
130
+ }, [ownership, exit]);
131
+
132
+ useInput((input, key) => {
133
+ if (slashOpen || quitConfirm) return;
134
+
135
+ if (key.escape) {
136
+ if (focusLayer === "panel") {
137
+ setFocusLayer("tabs");
138
+ return;
139
+ }
140
+ requestQuit();
141
+ return;
142
+ }
143
+ if (input === "/") {
144
+ setSlashOpen(true);
145
+ return;
146
+ }
147
+ const n = Number(input);
148
+ if (n >= 1 && n <= PANELS.length) {
149
+ setPanelIndex(n - 1);
150
+ return;
151
+ }
152
+ // Global panel switch: Tab always; ←→ when focus is on the tab strip.
153
+ if (key.tab) {
154
+ setPanelIndex((i) => (i + 1) % PANELS.length);
155
+ setFocusLayer("tabs");
156
+ return;
157
+ }
158
+ if (focusLayer === "tabs") {
159
+ if (key.leftArrow) {
160
+ setPanelIndex((i) => (i - 1 + PANELS.length) % PANELS.length);
161
+ return;
162
+ }
163
+ if (key.rightArrow) {
164
+ setPanelIndex((i) => (i + 1) % PANELS.length);
165
+ return;
166
+ }
167
+ if (key.downArrow || key.return) {
168
+ setFocusLayer("panel");
169
+ }
170
+ }
171
+ });
172
+
173
+ if (quitConfirm) {
174
+ return (
175
+ <Box
176
+ flexDirection="column"
177
+ borderStyle="round"
178
+ borderColor={TUI_MUTED}
179
+ width="100%"
180
+ padding={1}
181
+ >
182
+ <ConfirmDialog
183
+ message="Stop managed oke dev and quit TUI?"
184
+ onConfirm={() => {
185
+ void controller.stop().then(() => exit());
186
+ }}
187
+ onCancel={() => setQuitConfirm(false)}
188
+ />
189
+ </Box>
190
+ );
191
+ }
192
+
193
+ return (
194
+ <Box
195
+ flexDirection="column"
196
+ borderStyle="round"
197
+ borderColor={TUI_MUTED}
198
+ width="100%"
199
+ paddingX={1}
200
+ paddingY={0}
201
+ >
202
+ <Box paddingY={1}>
203
+ <HeaderBar cwd={props.cwd} />
204
+ </Box>
205
+ <TabBar tabs={TABS} activeIndex={panelIndex} focused={focusLayer === "tabs"} />
206
+
207
+ {slashOpen ? (
208
+ <Box marginTop={1} flexGrow={1} minHeight={10}>
209
+ <SlashPalette
210
+ cwd={props.cwd}
211
+ busy={slashBusy}
212
+ onClose={() => setSlashOpen(false)}
213
+ onJumpPanel={jumpPanel}
214
+ onRunCli={runCli}
215
+ />
216
+ </Box>
217
+ ) : (
218
+ <Box flexDirection="column" flexGrow={1} marginTop={1} minHeight={10} gap={1}>
219
+ {panel === "dashboard" ? (
220
+ <DashboardPanel
221
+ cwd={props.cwd}
222
+ active={panelActive}
223
+ onJumpPanel={jumpPanel}
224
+ onStatus={onStatus}
225
+ />
226
+ ) : null}
227
+ {panel === "dev" ? (
228
+ <DevPanel
229
+ cwd={props.cwd}
230
+ controller={controller}
231
+ onLog={onLog}
232
+ logs={logs}
233
+ active={panelActive}
234
+ onStatus={onStatus}
235
+ />
236
+ ) : null}
237
+ {panel === "database" ? (
238
+ <DatabasePanel cwd={props.cwd} active={panelActive} onLog={onLog} onStatus={onStatus} />
239
+ ) : null}
240
+ {panel === "docker" ? (
241
+ <DockerPanel cwd={props.cwd} active={panelActive} onLog={onLog} onStatus={onStatus} />
242
+ ) : null}
243
+ {panel === "navigator" ? <NavigatorPanel cwd={props.cwd} active={panelActive} /> : null}
244
+ </Box>
245
+ )}
246
+
247
+ <Box marginTop={0}>
248
+ <StatusBar status={footerStatus} />
249
+ </Box>
250
+ </Box>
251
+ );
252
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Ink live controls for an already-running `oke dev` session.
3
+ *
4
+ * Flat single-line chrome (no nested borders / width="100%") so Ink does not
5
+ * remount at a second column count and leave a ghost frame above the bar.
6
+ */
7
+
8
+ import { Box, Text, useApp, useInput } from "ink";
9
+ import { useState, type ReactElement } from "react";
10
+ import { mapDevControlInput } from "./keys.ts";
11
+ import { TUI_BRAND, TUI_HINT, TUI_MUTED, TUI_WARN } from "./theme.ts";
12
+
13
+ /** Props for {@link DevLiveControls}. */
14
+ export type DevLiveControlsProps = {
15
+ readonly onRefresh: () => void | Promise<void>;
16
+ readonly onComposeUp: () => void | Promise<void>;
17
+ readonly onComposeStop: () => void | Promise<void>;
18
+ readonly onQuit: () => void;
19
+ };
20
+
21
+ /**
22
+ * Compact key-driven controls mounted after `oke dev` boot.
23
+ *
24
+ * @param props - Actions
25
+ */
26
+ export function DevLiveControls(props: DevLiveControlsProps): ReactElement {
27
+ const { exit } = useApp();
28
+ const [help, setHelp] = useState(false);
29
+ const [busy, setBusy] = useState(false);
30
+ const [status, setStatus] = useState("");
31
+
32
+ useInput((input, key) => {
33
+ const ctrl = mapDevControlInput(input, key);
34
+ if (!ctrl || busy) return;
35
+ if (ctrl === "?") {
36
+ setHelp((h) => !h);
37
+ return;
38
+ }
39
+ if (ctrl === "q") {
40
+ props.onQuit();
41
+ exit();
42
+ return;
43
+ }
44
+ if (ctrl === "r") {
45
+ setBusy(true);
46
+ void Promise.resolve(props.onRefresh())
47
+ .then(() => setStatus("refreshed"))
48
+ .catch((err: unknown) => setStatus(err instanceof Error ? err.message : String(err)))
49
+ .finally(() => setBusy(false));
50
+ return;
51
+ }
52
+ if (ctrl === "u" || ctrl === "x") {
53
+ setBusy(true);
54
+ const run = ctrl === "u" ? props.onComposeUp : props.onComposeStop;
55
+ void Promise.resolve(run())
56
+ .then(() => setStatus(ctrl === "u" ? "compose up" : "compose stop"))
57
+ .catch((err: unknown) => setStatus(err instanceof Error ? err.message : String(err)))
58
+ .finally(() => setBusy(false));
59
+ }
60
+ });
61
+
62
+ return (
63
+ <Box flexDirection="column" marginTop={1}>
64
+ <Text>
65
+ <Text bold color={TUI_BRAND}>
66
+ oke dev
67
+ </Text>
68
+ <Text color={TUI_MUTED}> · controls · </Text>
69
+ <Text color={TUI_HINT}>? help · r refresh · q quit · u up · x stop</Text>
70
+ {busy ? <Text color={TUI_WARN}> · working…</Text> : null}
71
+ {status ? <Text color={TUI_MUTED}>{` · ${status}`}</Text> : null}
72
+ </Text>
73
+ {help ? (
74
+ <Text color={TUI_MUTED}>
75
+ r clear/refresh board · q quit · u compose up · x compose stop
76
+ </Text>
77
+ ) : null}
78
+ </Box>
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Render {@link DevLiveControls} until quit.
84
+ *
85
+ * Clears Ink's frame before `onRefresh` reprints the hero so stdout + Ink
86
+ * never stack two control boxes.
87
+ *
88
+ * @param props - Actions
89
+ */
90
+ export async function launchDevLiveControls(props: DevLiveControlsProps): Promise<void> {
91
+ const React = await import("react");
92
+ const { render } = await import("ink");
93
+
94
+ type InkInstance = ReturnType<typeof render>;
95
+ let instance: InkInstance | null = null;
96
+
97
+ const afterStdout = async (run: () => void | Promise<void>): Promise<void> => {
98
+ instance?.clear();
99
+ await run();
100
+ if (instance) {
101
+ instance.rerender(React.createElement(DevLiveControls, bound));
102
+ }
103
+ };
104
+
105
+ const bound: DevLiveControlsProps = {
106
+ onRefresh: () => afterStdout(props.onRefresh),
107
+ onComposeUp: () => afterStdout(props.onComposeUp),
108
+ onComposeStop: () => afterStdout(props.onComposeStop),
109
+ onQuit: props.onQuit,
110
+ };
111
+
112
+ instance = render(React.createElement(DevLiveControls, bound), {
113
+ exitOnCtrlC: false,
114
+ });
115
+ await instance.waitUntilExit();
116
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Simple y/n confirm overlay for destructive TUI actions.
3
+ */
4
+
5
+ import { Box, Text, useInput } from "ink";
6
+ import type { ReactElement } from "react";
7
+ import { TUI_HINT, TUI_OK, TUI_WARN } from "../theme.ts";
8
+
9
+ /** Props for {@link ConfirmDialog}. */
10
+ export type ConfirmDialogProps = {
11
+ readonly message: string;
12
+ readonly onConfirm: () => void;
13
+ readonly onCancel: () => void;
14
+ };
15
+
16
+ /**
17
+ * Confirm with y / n / Enter / Esc.
18
+ *
19
+ * @param props - Message + callbacks
20
+ */
21
+ export function ConfirmDialog(props: ConfirmDialogProps): ReactElement {
22
+ useInput((input, key) => {
23
+ if (input === "y" || input === "Y" || key.return) {
24
+ props.onConfirm();
25
+ return;
26
+ }
27
+ if (input === "n" || input === "N" || key.escape) {
28
+ props.onCancel();
29
+ }
30
+ });
31
+ return (
32
+ <Box
33
+ flexDirection="column"
34
+ borderStyle="round"
35
+ borderColor={TUI_WARN}
36
+ paddingX={1}
37
+ paddingY={0}
38
+ >
39
+ <Text bold color={TUI_WARN}>
40
+ Confirm
41
+ </Text>
42
+ <Text>{props.message}</Text>
43
+ <Text color={TUI_HINT}>
44
+ <Text color={TUI_OK}>y</Text> confirm · <Text color={TUI_WARN}>n</Text> cancel
45
+ </Text>
46
+ </Box>
47
+ );
48
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Compact project chrome — no border (spacing separates).
3
+ */
4
+
5
+ import { Box, Text } from "ink";
6
+ import { useEffect, useState, type ReactElement } from "react";
7
+ import { basename } from "node:path";
8
+ import { TUI_ACCENT, TUI_MUTED, TUI_WARN } from "../theme.ts";
9
+
10
+ /** Props for {@link HeaderBar}. */
11
+ export type HeaderBarProps = {
12
+ readonly cwd: string;
13
+ };
14
+
15
+ async function resolveBranch(cwd: string): Promise<string> {
16
+ try {
17
+ const proc = Bun.spawn(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
18
+ cwd,
19
+ stdout: "pipe",
20
+ stderr: "pipe",
21
+ });
22
+ const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
23
+ if (code !== 0) return "dev";
24
+ const name = out.trim();
25
+ return name.length > 0 && name !== "HEAD" ? name : "dev";
26
+ } catch {
27
+ return "dev";
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Project · branch · path.
33
+ *
34
+ * @param props - cwd
35
+ */
36
+ export function HeaderBar(props: HeaderBarProps): ReactElement {
37
+ const [branch, setBranch] = useState("dev");
38
+ const project = basename(props.cwd) || props.cwd;
39
+
40
+ useEffect(() => {
41
+ let cancelled = false;
42
+ void resolveBranch(props.cwd).then((b) => {
43
+ if (!cancelled) setBranch(b);
44
+ });
45
+ return () => {
46
+ cancelled = true;
47
+ };
48
+ }, [props.cwd]);
49
+
50
+ const home = process.env["HOME"] ?? "";
51
+ const shortCwd =
52
+ home && props.cwd.startsWith(home) ? `~${props.cwd.slice(home.length)}` : props.cwd;
53
+
54
+ return (
55
+ <Box paddingX={1} paddingY={0} marginBottom={0}>
56
+ <Text bold color={TUI_ACCENT}>
57
+ {project}
58
+ </Text>
59
+ <Text color={TUI_MUTED}> · </Text>
60
+ <Text color={TUI_WARN}>{branch}</Text>
61
+ <Text color={TUI_MUTED}> · </Text>
62
+ <Text color={TUI_MUTED} wrap="truncate">
63
+ {shortCwd}
64
+ </Text>
65
+ </Box>
66
+ );
67
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Persistent log footer — last N lines from the active service.
3
+ */
4
+
5
+ import { Box, Text } from "ink";
6
+ import type { ReactElement } from "react";
7
+ import { TUI_MUTED } from "../theme.ts";
8
+
9
+ /** Props for {@link LogStream}. */
10
+ export type LogStreamProps = {
11
+ readonly lines: readonly string[];
12
+ readonly max?: number;
13
+ readonly title?: string;
14
+ };
15
+
16
+ /**
17
+ * Scrollable-ish log tail (shows last `max` lines).
18
+ *
19
+ * @param props - Lines + title
20
+ */
21
+ export function LogStream(props: LogStreamProps): ReactElement {
22
+ const max = props.max ?? 6;
23
+ const tail = props.lines.slice(-max);
24
+ return (
25
+ <Box
26
+ flexDirection="column"
27
+ borderStyle="single"
28
+ borderColor={TUI_MUTED}
29
+ paddingX={1}
30
+ minHeight={4}
31
+ flexGrow={0}
32
+ >
33
+ <Text bold color={TUI_MUTED}>
34
+ {props.title ?? "Logs"}
35
+ </Text>
36
+ {tail.length === 0 ? (
37
+ <Text color={TUI_MUTED}>(quiet)</Text>
38
+ ) : (
39
+ tail.map((line, i) => (
40
+ <Text key={`${i}-${line.slice(0, 24)}`} wrap="truncate">
41
+ {line.replace(/\n$/, "")}
42
+ </Text>
43
+ ))
44
+ )}
45
+ </Box>
46
+ );
47
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Content card — cyan title, single border for distinct sections only.
3
+ */
4
+
5
+ import { Box, Text } from "ink";
6
+ import type { ReactElement, ReactNode } from "react";
7
+ import { TUI_ACCENT, TUI_MUTED } from "../theme.ts";
8
+
9
+ /** Props for {@link PanelCard}. */
10
+ export type PanelCardProps = {
11
+ readonly title: string;
12
+ readonly children: ReactNode;
13
+ readonly flexGrow?: number;
14
+ readonly accent?: boolean;
15
+ };
16
+
17
+ /**
18
+ * Single bordered panel section.
19
+ *
20
+ * @param props - Title + body
21
+ */
22
+ export function PanelCard(props: PanelCardProps): ReactElement {
23
+ return (
24
+ <Box
25
+ flexDirection="column"
26
+ borderStyle="single"
27
+ borderColor={props.accent ? TUI_ACCENT : TUI_MUTED}
28
+ paddingX={1}
29
+ paddingY={0}
30
+ flexGrow={props.flexGrow}
31
+ minHeight={3}
32
+ >
33
+ <Text bold color={TUI_ACCENT}>
34
+ {props.title}
35
+ </Text>
36
+ <Box flexDirection="column">{props.children}</Box>
37
+ </Box>
38
+ );
39
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Selectable list row with › marker + cyan focus.
3
+ */
4
+
5
+ import { Text } from "ink";
6
+ import type { ReactElement } from "react";
7
+ import { TUI_ACCENT, TUI_MUTED } from "../theme.ts";
8
+
9
+ /** Props for {@link SelectRow}. */
10
+ export type SelectRowProps = {
11
+ readonly label: string;
12
+ readonly selected: boolean;
13
+ readonly enabled?: boolean;
14
+ };
15
+
16
+ /**
17
+ * One navigable action/list row.
18
+ *
19
+ * @param props - Label + selection
20
+ */
21
+ export function SelectRow(props: SelectRowProps): ReactElement {
22
+ const enabled = props.enabled !== false;
23
+ if (!enabled) {
24
+ return (
25
+ <Text dimColor color={TUI_MUTED}>
26
+ {props.selected ? "› " : " "}
27
+ {props.label}
28
+ </Text>
29
+ );
30
+ }
31
+ return (
32
+ <Text bold={props.selected} color={props.selected ? TUI_ACCENT : undefined}>
33
+ {props.selected ? "› " : " "}
34
+ {props.label}
35
+ </Text>
36
+ );
37
+ }