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,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
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * OKE ASCII wordmark — same art as `oke dev` / {@link formatOkeWordmark}.
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
+ /** Plain wordmark lines (no ANSI) — matches `formatOkeWordmark`. */
10
+ export const OKE_WORDMARK_LINES: readonly string[] = [
11
+ " ██████╗ ██╗ ██╗███████╗",
12
+ " ██╔═══██╗██║ ██╔╝██╔════╝",
13
+ " ██║ ██║█████╔╝ █████╗ ",
14
+ " ██║ ██║██╔═██╗ ██╔══╝ ",
15
+ " ╚██████╔╝██║ ██╗███████╗",
16
+ " ╚═════╝ ╚═╝ ╚═╝╚══════╝",
17
+ ];
18
+
19
+ /** Props for {@link Wordmark}. */
20
+ export type WordmarkProps = {
21
+ readonly version?: string;
22
+ };
23
+
24
+ /**
25
+ * Green block-letter OKE + version.
26
+ *
27
+ * @param props - Optional pinned version
28
+ */
29
+ export function Wordmark(props: WordmarkProps): ReactElement {
30
+ const version = props.version ?? "0.10.3";
31
+ return (
32
+ <Box flexDirection="column">
33
+ {OKE_WORDMARK_LINES.map((line) => (
34
+ <Text key={line} bold color={TUI_BRAND}>
35
+ {line}
36
+ </Text>
37
+ ))}
38
+ <Text color={TUI_MUTED}>
39
+ {" "}v{version}
40
+ </Text>
41
+ </Box>
42
+ );
43
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared TUI format helpers.
3
+ */
4
+
5
+ /**
6
+ * Format ms as compact uptime (`4m 21s`).
7
+ *
8
+ * @param ms - Elapsed milliseconds
9
+ */
10
+ export function formatUptime(ms: number): string {
11
+ const s = Math.max(0, Math.floor(ms / 1000));
12
+ const m = Math.floor(s / 60);
13
+ const h = Math.floor(m / 60);
14
+ if (h > 0) return `${h}h ${m % 60}m`;
15
+ if (m > 0) return `${m}m ${s % 60}s`;
16
+ return `${s}s`;
17
+ }
18
+
19
+ /**
20
+ * Open a URL in the default browser (best-effort).
21
+ *
22
+ * @param url - http(s) URL
23
+ */
24
+ export async function openUrl(url: string): Promise<void> {
25
+ const platform = process.platform;
26
+ const cmd =
27
+ platform === "darwin"
28
+ ? ["open", url]
29
+ : platform === "win32"
30
+ ? ["cmd", "/c", "start", "", url]
31
+ : ["xdg-open", url];
32
+ try {
33
+ const proc = Bun.spawn(cmd, { stdout: "ignore", stderr: "ignore" });
34
+ await proc.exited;
35
+ } catch {
36
+ // ignore — TUI still shows the link
37
+ }
38
+ }
@@ -0,0 +1,18 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mapDevControlInput } from "./keys.ts";
3
+
4
+ describe("mapDevControlInput", () => {
5
+ test("maps ? r q u x and aliases", () => {
6
+ expect(mapDevControlInput("?", {})).toBe("?");
7
+ expect(mapDevControlInput("h", {})).toBe("?");
8
+ expect(mapDevControlInput("r", {})).toBe("r");
9
+ expect(mapDevControlInput("q", {})).toBe("q");
10
+ expect(mapDevControlInput("u", {})).toBe("u");
11
+ expect(mapDevControlInput("x", {})).toBe("x");
12
+ expect(mapDevControlInput("c", {})).toBeNull();
13
+ });
14
+
15
+ test("maps Ctrl+C to quit", () => {
16
+ expect(mapDevControlInput("c", { ctrl: true })).toBe("q");
17
+ });
18
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared keyboard mapping for Ink `useInput` and pure tests.
3
+ * Parity with legacy `parseDevControlKey` (`? r q u x`).
4
+ */
5
+
6
+ /** Dev control action from a keypress. */
7
+ export type DevControlKey = "?" | "r" | "q" | "u" | "x";
8
+
9
+ /**
10
+ * Map Ink input (+ ctrl) to a control key.
11
+ *
12
+ * @param input - Character from useInput
13
+ * @param key - Ink key modifiers
14
+ */
15
+ export function mapDevControlInput(
16
+ input: string,
17
+ key: { readonly ctrl?: boolean },
18
+ ): DevControlKey | null {
19
+ if (key.ctrl && input === "c") return "q";
20
+ if (input === "?" || input === "h" || input === "H") return "?";
21
+ if (input === "q" || input === "Q") return "q";
22
+ if (input === "r" || input === "R") return "r";
23
+ if (input === "u" || input === "U") return "u";
24
+ if (input === "x" || input === "X") return "x";
25
+ return null;
26
+ }
@@ -0,0 +1,18 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { clearTerminalScreen } from "../../term.ts";
3
+ import { canRenderTui, clearTerminalViewport } from "./launch.ts";
4
+
5
+ describe("canRenderTui", () => {
6
+ test("requires stdout.isTTY", () => {
7
+ expect(canRenderTui({ isTTY: true })).toBe(true);
8
+ expect(canRenderTui({ isTTY: false })).toBe(false);
9
+ expect(canRenderTui({})).toBe(false);
10
+ });
11
+ });
12
+
13
+ describe("clearTerminalViewport", () => {
14
+ test("writes the same clear sequence as oke dev", () => {
15
+ expect(clearTerminalScreen()).toContain("\u001b[2J");
16
+ expect(typeof clearTerminalViewport).toBe("function");
17
+ });
18
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Launch the interactive Ink TUI (dynamic-imported from the bin).
3
+ */
4
+
5
+ import { clearTerminalScreen } from "../../term.ts";
6
+ import { EXIT_OK, EXIT_RUNTIME, EXIT_USAGE } from "../exit.ts";
7
+ import { formatOkeHelp } from "../registry.ts";
8
+
9
+ /**
10
+ * True when stdout can host an interactive Ink app.
11
+ *
12
+ * @param stream - stdout-like
13
+ */
14
+ export function canRenderTui(stream: { readonly isTTY?: boolean } = process.stdout): boolean {
15
+ return stream.isTTY === true;
16
+ }
17
+
18
+ /**
19
+ * Wipe the viewport so boot logs never sit above the Ink frame.
20
+ */
21
+ export function clearTerminalViewport(): void {
22
+ process.stdout.write(clearTerminalScreen());
23
+ }
24
+
25
+ /**
26
+ * Start the TUI. Call only after {@link canRenderTui} is true.
27
+ *
28
+ * @param cwd - Project root
29
+ */
30
+ export async function launchTui(cwd: string = process.cwd()): Promise<number> {
31
+ try {
32
+ clearTerminalViewport();
33
+ const React = await import("react");
34
+ const { render } = await import("ink");
35
+ const { OkeTuiApp } = await import("./App.tsx");
36
+ const instance = render(React.createElement(OkeTuiApp, { cwd }), {
37
+ exitOnCtrlC: false,
38
+ });
39
+ await instance.waitUntilExit();
40
+ return EXIT_OK;
41
+ } catch (err) {
42
+ const msg = err instanceof Error ? err.message : String(err);
43
+ const missing =
44
+ /Cannot find (module|package)|ERR_MODULE_NOT_FOUND|Cannot find package/i.test(msg) ||
45
+ msg.includes("'ink'") ||
46
+ msg.includes('"ink"');
47
+ if (missing) {
48
+ console.error("oke: interactive TUI requires optional deps `ink` and `react`.");
49
+ console.error(" bun add ink react");
50
+ console.error(" Or run `oke --help` / `oke <command>` without the TUI.");
51
+ console.log(formatOkeHelp());
52
+ return EXIT_USAGE;
53
+ }
54
+ console.error(`oke: TUI failed — ${msg}`);
55
+ return EXIT_RUNTIME;
56
+ }
57
+ }