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,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
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * `/` command palette — autofill over the full `oke` CLI + panel jumps.
3
+ */
4
+
5
+ import { Box, Text, useInput } from "ink";
6
+ import { useMemo, useState, type ReactElement } from "react";
7
+ import {
8
+ commonPrefix,
9
+ filterSlashActions,
10
+ parseSlashArgv,
11
+ type SlashAction,
12
+ } from "../slash-catalog.ts";
13
+ import { TUI_BRAND, TUI_HINT, TUI_MUTED, TUI_OK, TUI_WARN } from "../theme.ts";
14
+
15
+ /** Props for {@link SlashPalette}. */
16
+ export type SlashPaletteProps = {
17
+ readonly cwd: string;
18
+ readonly onClose: () => void;
19
+ readonly onJumpPanel: (panelId: string) => void;
20
+ readonly onRunCli: (argv: readonly string[]) => void | Promise<void>;
21
+ readonly busy?: boolean;
22
+ };
23
+
24
+ /**
25
+ * Slash palette overlay — type to filter, Tab autofills, Enter runs.
26
+ *
27
+ * @param props - Callbacks
28
+ */
29
+ export function SlashPalette(props: SlashPaletteProps): ReactElement {
30
+ const [query, setQuery] = useState("");
31
+ const [selected, setSelected] = useState(0);
32
+ const [status, setStatus] = useState("");
33
+
34
+ const matches = useMemo(() => filterSlashActions(query, 14), [query]);
35
+ const sel = Math.min(selected, Math.max(0, matches.length - 1));
36
+ const active = matches[sel] ?? null;
37
+
38
+ useInput(
39
+ (input, key) => {
40
+ if (props.busy) return;
41
+
42
+ if (key.escape) {
43
+ props.onClose();
44
+ return;
45
+ }
46
+
47
+ if (key.upArrow) {
48
+ setSelected((i) => Math.max(0, i - 1));
49
+ return;
50
+ }
51
+ if (key.downArrow) {
52
+ setSelected((i) => Math.min(Math.max(0, matches.length - 1), i + 1));
53
+ return;
54
+ }
55
+
56
+ if (key.tab) {
57
+ if (active) {
58
+ const ids = matches.map((m) => m.id);
59
+ const prefix = commonPrefix(ids);
60
+ const next =
61
+ prefix.length > query.length && ids.every((id) => id.startsWith(prefix))
62
+ ? prefix
63
+ : active.id;
64
+ setQuery(next);
65
+ setSelected(0);
66
+ setStatus(`autofill → /${next}`);
67
+ }
68
+ return;
69
+ }
70
+
71
+ if (key.return) {
72
+ void submit(active, query);
73
+ return;
74
+ }
75
+
76
+ if (key.backspace || key.delete) {
77
+ setQuery((q) => q.slice(0, -1));
78
+ setSelected(0);
79
+ setStatus("");
80
+ return;
81
+ }
82
+
83
+ if (!input || key.ctrl || key.meta) return;
84
+ // Ignore a second leading `/`; otherwise append printable chars.
85
+ if (input === "/" && query.length === 0) return;
86
+ setQuery((q) => q + input);
87
+ setSelected(0);
88
+ setStatus("");
89
+ },
90
+ { isActive: true },
91
+ );
92
+
93
+ const submit = async (action: SlashAction | null, raw: string): Promise<void> => {
94
+ if (action?.kind === "panel" && action.panel) {
95
+ props.onJumpPanel(action.panel);
96
+ props.onClose();
97
+ return;
98
+ }
99
+
100
+ if (action?.kind === "cli" && action.argv) {
101
+ const typed = parseSlashArgv(raw);
102
+ const base = action.argv;
103
+ // Append trailing free tokens the user typed beyond the suggestion.
104
+ let argv: readonly string[] = base;
105
+ if (typed.length > base.length && typed.slice(0, base.length).join(" ") === base.join(" ")) {
106
+ argv = typed;
107
+ }
108
+ setStatus(`running oke ${argv.join(" ")}…`);
109
+ await props.onRunCli(argv);
110
+ props.onClose();
111
+ return;
112
+ }
113
+
114
+ const argv = parseSlashArgv(raw);
115
+ if (argv.length === 0) {
116
+ setStatus("type a command · Tab to autofill");
117
+ return;
118
+ }
119
+ setStatus(`running oke ${argv.join(" ")}…`);
120
+ await props.onRunCli(argv);
121
+ props.onClose();
122
+ };
123
+
124
+ return (
125
+ <Box
126
+ flexDirection="column"
127
+ borderStyle="round"
128
+ borderColor={TUI_BRAND}
129
+ paddingX={1}
130
+ width="100%"
131
+ flexGrow={1}
132
+ >
133
+ <Box>
134
+ <Text bold color={TUI_BRAND}>
135
+ /
136
+ </Text>
137
+ <Text bold>{query}</Text>
138
+ <Text color={TUI_BRAND}>█</Text>
139
+ <Text color={TUI_MUTED}> Tab autofill · ↑↓ · Enter run · Esc</Text>
140
+ </Box>
141
+
142
+ <Box flexDirection="column" marginTop={1} flexGrow={1}>
143
+ {matches.length === 0 ? (
144
+ <Text color={TUI_WARN}>No matches — Enter runs free-form `oke …`</Text>
145
+ ) : (
146
+ matches.map((m, i) => {
147
+ const on = i === sel;
148
+ return (
149
+ <Box key={m.id} gap={1}>
150
+ <Text bold={on} color={on ? TUI_BRAND : TUI_MUTED}>
151
+ {on ? "›" : " "}
152
+ </Text>
153
+ <Text bold={on} color={on ? TUI_BRAND : undefined}>
154
+ /{m.label}
155
+ </Text>
156
+ <Text color={m.kind === "panel" ? TUI_OK : TUI_MUTED}>
157
+ {m.kind === "panel" ? "panel" : "cli"}
158
+ </Text>
159
+ <Text color={TUI_MUTED} wrap="truncate">
160
+ {m.summary}
161
+ </Text>
162
+ </Box>
163
+ );
164
+ })
165
+ )}
166
+ </Box>
167
+
168
+ {status ? <Text color={TUI_HINT}>{status}</Text> : null}
169
+ {props.busy ? <Text color={TUI_WARN}>Running…</Text> : null}
170
+ </Box>
171
+ );
172
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Lightweight terminal spinner (no extra deps).
3
+ */
4
+
5
+ import { Text } from "ink";
6
+ import { useEffect, useState, type ReactElement } from "react";
7
+ import { TUI_WARN } from "../theme.ts";
8
+
9
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
10
+
11
+ /** Props for {@link Spinner}. */
12
+ export type SpinnerProps = {
13
+ readonly label?: string;
14
+ };
15
+
16
+ /**
17
+ * Animated spinner + optional label.
18
+ *
19
+ * @param props - Label
20
+ */
21
+ export function Spinner(props: SpinnerProps): ReactElement {
22
+ const [i, setI] = useState(0);
23
+ useEffect(() => {
24
+ const id = setInterval(() => setI((n) => (n + 1) % FRAMES.length), 80);
25
+ return () => clearInterval(id);
26
+ }, []);
27
+ return (
28
+ <Text color={TUI_WARN}>
29
+ {FRAMES[i]} {props.label ?? "Working…"}
30
+ </Text>
31
+ );
32
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Single global footer — shortcuts + transient action status (no own border;
3
+ * the app chrome already frames the shell).
4
+ */
5
+
6
+ import { Box, Text } from "ink";
7
+ import type { ReactElement } from "react";
8
+ import { Spinner } from "./Spinner.tsx";
9
+ import { TUI_HINT, TUI_MUTED, TUI_OK } from "../theme.ts";
10
+
11
+ /** Footer mode. */
12
+ export type FooterStatus =
13
+ | { readonly kind: "idle" }
14
+ | { readonly kind: "running"; readonly label: string }
15
+ | { readonly kind: "done"; readonly label?: string };
16
+
17
+ /** Props for {@link StatusBar}. */
18
+ export type StatusBarProps = {
19
+ readonly status?: FooterStatus;
20
+ readonly hints?: string;
21
+ };
22
+
23
+ /**
24
+ * Shortcut strip + optional running/done feedback.
25
+ *
26
+ * @param props - Status + hints
27
+ */
28
+ export function StatusBar(props: StatusBarProps): ReactElement {
29
+ const hints =
30
+ props.hints ?? "/ command · Tab panels · ↑↓ navigate · Enter action · 1-5 jump · Esc";
31
+ const status = props.status ?? { kind: "idle" as const };
32
+
33
+ return (
34
+ <Box paddingX={1} paddingY={0} marginTop={0} justifyContent="space-between">
35
+ <Box>
36
+ {status.kind === "running" ? (
37
+ <Spinner label={status.label} />
38
+ ) : status.kind === "done" ? (
39
+ <Text color={TUI_OK}>✓ {status.label ?? "Done"}</Text>
40
+ ) : (
41
+ <Text color={TUI_HINT}>{hints}</Text>
42
+ )}
43
+ </Box>
44
+ {status.kind !== "idle" ? (
45
+ <Text color={TUI_MUTED} dimColor>
46
+ {hints}
47
+ </Text>
48
+ ) : null}
49
+ </Box>
50
+ );
51
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Ink readiness glyph for {@link DevStatus}.
3
+ */
4
+
5
+ import { Text } from "ink";
6
+ import type { ReactElement } from "react";
7
+ import type { DevStatus } from "../../../term.ts";
8
+ import { TUI_ERR, TUI_OK, TUI_WARN } from "../theme.ts";
9
+
10
+ /** Props for {@link StatusDot}. */
11
+ export type StatusDotProps = {
12
+ readonly status: DevStatus;
13
+ };
14
+
15
+ /**
16
+ * ● online/pending/error · ○ offline (yellow/red, not gray).
17
+ *
18
+ * @param props - Status
19
+ */
20
+ export function StatusDot(props: StatusDotProps): ReactElement {
21
+ if (props.status === "idle") {
22
+ return <Text color={TUI_WARN}>○</Text>;
23
+ }
24
+ const color = props.status === "ready" ? TUI_OK : props.status === "pending" ? TUI_WARN : TUI_ERR;
25
+ return <Text color={color}>●</Text>;
26
+ }
27
+
28
+ /** Fixed-width status word. */
29
+ export function statusWord(status: DevStatus): {
30
+ readonly word: string;
31
+ readonly color: typeof TUI_OK | typeof TUI_WARN | typeof TUI_ERR;
32
+ } {
33
+ if (status === "ready") return { word: "online ", color: TUI_OK };
34
+ if (status === "pending") return { word: "start ", color: TUI_WARN };
35
+ if (status === "error") return { word: "error ", color: TUI_ERR };
36
+ return { word: "offline", color: TUI_WARN };
37
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Top-level panel tabs — green active chip, no enclosing border.
3
+ */
4
+
5
+ import { Box, Text } from "ink";
6
+ import type { ReactElement } from "react";
7
+ import { TUI_BRAND, TUI_MUTED } from "../theme.ts";
8
+
9
+ /** One tab descriptor. */
10
+ export type TabItem = {
11
+ readonly id: string;
12
+ readonly label: string;
13
+ };
14
+
15
+ /** Props for {@link TabBar}. */
16
+ export type TabBarProps = {
17
+ readonly tabs: readonly TabItem[];
18
+ readonly activeIndex: number;
19
+ /** When true, tab strip owns focus (reserved for future emphasis). */
20
+ readonly focused?: boolean;
21
+ };
22
+
23
+ /**
24
+ * Horizontal tab bar.
25
+ *
26
+ * @param props - Tabs + selection
27
+ */
28
+ export function TabBar(props: TabBarProps): ReactElement {
29
+ return (
30
+ <Box paddingX={1} gap={0}>
31
+ {props.tabs.map((tab, i) => {
32
+ const active = i === props.activeIndex;
33
+ const label = ` ${i + 1} ${tab.label} `;
34
+ if (active) {
35
+ return (
36
+ <Text key={tab.id} backgroundColor={TUI_BRAND} color="black" bold>
37
+ {label}
38
+ </Text>
39
+ );
40
+ }
41
+ return (
42
+ <Text key={tab.id} color={TUI_MUTED}>
43
+ {label}
44
+ </Text>
45
+ );
46
+ })}
47
+ </Box>
48
+ );
49
+ }