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,201 @@
1
+ /**
2
+ * Landing panel — OKE wordmark, session, aligned services, quick actions.
3
+ */
4
+
5
+ import { Box, Text, useInput } from "ink";
6
+ import { useEffect, useState, type ReactElement } from "react";
7
+ import type { FooterStatus } from "../components/StatusBar.tsx";
8
+ import { StatusDot, statusWord } from "../components/StatusDot.tsx";
9
+ import { Wordmark } from "../components/Wordmark.tsx";
10
+ import { formatUptime, openUrl } from "../format.ts";
11
+ import { pollPortSnapshot, type PortSnapshot } from "../state/ports.ts";
12
+ import { TUI_ACCENT, TUI_MUTED, TUI_OK, TUI_WARN } from "../theme.ts";
13
+
14
+ /** Props for {@link DashboardPanel}. */
15
+ export type DashboardPanelProps = {
16
+ readonly cwd: string;
17
+ readonly active: boolean;
18
+ readonly onJumpPanel: (panelId: string) => void;
19
+ readonly onStatus: (status: FooterStatus) => void;
20
+ };
21
+
22
+ const ACTIONS = [
23
+ { id: "open", label: "[O]pen Console", key: "o" },
24
+ { id: "mcp", label: "[M]CP Docs", key: "m" },
25
+ { id: "dev", label: "[D]ev Panel", key: "d" },
26
+ ] as const;
27
+
28
+ /**
29
+ * Dashboard panel — polls ports every 1.5s.
30
+ *
31
+ * @param props - cwd + focus/actions
32
+ */
33
+ export function DashboardPanel(props: DashboardPanelProps): ReactElement {
34
+ const [snap, setSnap] = useState<PortSnapshot | null>(null);
35
+ const [startedAt, setStartedAt] = useState<number | null>(null);
36
+ const [now, setNow] = useState(Date.now());
37
+ const [selected, setSelected] = useState(0);
38
+
39
+ useEffect(() => {
40
+ let cancelled = false;
41
+ const tick = async (): Promise<void> => {
42
+ const next = await pollPortSnapshot(props.cwd);
43
+ if (cancelled) return;
44
+ setSnap(next);
45
+ if (next.ownership === "managed") {
46
+ setStartedAt((prev) => prev ?? Date.now());
47
+ } else {
48
+ setStartedAt(null);
49
+ }
50
+ setNow(Date.now());
51
+ };
52
+ void tick();
53
+ const id = setInterval(() => void tick(), 1500);
54
+ return () => {
55
+ cancelled = true;
56
+ clearInterval(id);
57
+ };
58
+ }, [props.cwd]);
59
+
60
+ // Prefer lock startedAt when available via re-read
61
+ useEffect(() => {
62
+ let cancelled = false;
63
+ void (async () => {
64
+ const { readDevSessionLock } = await import("../../dev-session-lock.ts");
65
+ const lock = await readDevSessionLock(props.cwd);
66
+ if (!cancelled && lock) {
67
+ const t = Date.parse(lock.startedAt);
68
+ if (Number.isFinite(t)) setStartedAt(t);
69
+ }
70
+ })();
71
+ return () => {
72
+ cancelled = true;
73
+ };
74
+ }, [props.cwd, snap?.ownership]);
75
+
76
+ useInput(
77
+ (input, key) => {
78
+ if (!props.active) return;
79
+ if (key.upArrow) setSelected((i) => Math.max(0, i - 1));
80
+ if (key.downArrow) setSelected((i) => Math.min(ACTIONS.length - 1, i + 1));
81
+ const lower = input.toLowerCase();
82
+ if (lower === "o") void runAction("open");
83
+ else if (lower === "m") void runAction("mcp");
84
+ else if (lower === "d") void runAction("dev");
85
+ else if (key.return) {
86
+ const a = ACTIONS[selected];
87
+ if (a) void runAction(a.id);
88
+ }
89
+ },
90
+ { isActive: props.active },
91
+ );
92
+
93
+ const runAction = async (id: (typeof ACTIONS)[number]["id"]): Promise<void> => {
94
+ if (id === "dev") {
95
+ props.onJumpPanel("dev");
96
+ return;
97
+ }
98
+ const consoleSurf = snap?.surfaces.find((s) => s.id === "console");
99
+ const docsSurf = snap?.surfaces.find((s) => s.id === "docsMcp");
100
+ if (id === "open") {
101
+ const url = consoleSurf?.url ?? "http://127.0.0.1:6533";
102
+ props.onStatus({ kind: "running", label: `Opening ${url}…` });
103
+ await openUrl(url);
104
+ props.onStatus({ kind: "done", label: "Opened Console" });
105
+ return;
106
+ }
107
+ const url = docsSurf?.url ?? "http://127.0.0.1:6536";
108
+ props.onStatus({ kind: "running", label: `Opening ${url}…` });
109
+ await openUrl(url);
110
+ props.onStatus({ kind: "done", label: "Opened MCP Docs" });
111
+ };
112
+
113
+ const ownership = snap?.ownership ?? "stopped";
114
+ const ownershipLabel =
115
+ ownership === "managed"
116
+ ? "Managed"
117
+ : ownership === "external"
118
+ ? "Connected (external)"
119
+ : "Stopped";
120
+ const ownershipColor = ownership === "managed" || ownership === "external" ? TUI_OK : TUI_WARN;
121
+ const uptime = startedAt && ownership === "managed" ? formatUptime(now - startedAt) : null;
122
+
123
+ return (
124
+ <Box flexDirection="column" gap={1} flexGrow={1}>
125
+ <Box justifyContent="space-between" alignItems="flex-start">
126
+ <Wordmark version="0.10.3" />
127
+ </Box>
128
+
129
+ <Box flexDirection="column">
130
+ <Text bold color={TUI_ACCENT}>
131
+ Session
132
+ </Text>
133
+ <Box gap={1}>
134
+ <StatusDot
135
+ status={ownership === "managed" || ownership === "external" ? "ready" : "idle"}
136
+ />
137
+ <Text bold color={ownershipColor}>
138
+ {ownershipLabel}
139
+ </Text>
140
+ {uptime ? <Text color={TUI_MUTED}>· uptime {uptime}</Text> : null}
141
+ </Box>
142
+ {ownership === "external" ? (
143
+ <Text color={TUI_WARN}>Read-only — will not start a second stack</Text>
144
+ ) : null}
145
+ {ownership === "stopped" ? (
146
+ <Text color={TUI_MUTED}>No live oke dev — press [D] or open Dev panel</Text>
147
+ ) : null}
148
+ </Box>
149
+
150
+ <Box flexDirection="column" flexGrow={1}>
151
+ <Box>
152
+ <Box width={14}>
153
+ <Text bold color={TUI_ACCENT}>
154
+ Services
155
+ </Text>
156
+ </Box>
157
+ <Box width={8}>
158
+ <Text color={TUI_MUTED}>Port</Text>
159
+ </Box>
160
+ <Box width={10}>
161
+ <Text color={TUI_MUTED}>Status</Text>
162
+ </Box>
163
+ <Text color={TUI_MUTED}>URL</Text>
164
+ </Box>
165
+ <Text color={TUI_MUTED}>{"─".repeat(56)}</Text>
166
+ {(snap?.surfaces ?? []).map((s) => {
167
+ const sw = statusWord(s.status);
168
+ return (
169
+ <Box key={s.id}>
170
+ <Box width={14} gap={1}>
171
+ <StatusDot status={s.status} />
172
+ <Text>{s.label.padEnd(9)}</Text>
173
+ </Box>
174
+ <Box width={8}>
175
+ <Text color={TUI_ACCENT}>{`:${String(s.port).padEnd(5)}`}</Text>
176
+ </Box>
177
+ <Box width={10}>
178
+ <Text color={sw.color}>{sw.word}</Text>
179
+ </Box>
180
+ <Text color={TUI_MUTED}>{s.url}</Text>
181
+ </Box>
182
+ );
183
+ })}
184
+ {!snap ? <Text color={TUI_MUTED}>Probing ports…</Text> : null}
185
+ </Box>
186
+
187
+ <Box gap={2}>
188
+ {ACTIONS.map((a, i) => (
189
+ <Text
190
+ key={a.id}
191
+ bold={props.active && i === selected}
192
+ color={props.active && i === selected ? TUI_ACCENT : TUI_MUTED}
193
+ >
194
+ {props.active && i === selected ? "› " : " "}
195
+ {a.label}
196
+ </Text>
197
+ ))}
198
+ </Box>
199
+ </Box>
200
+ );
201
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Database panel — 3-column action cards, no nested footer.
3
+ */
4
+
5
+ import { Box, Text, useInput } from "ink";
6
+ import { useState, type ReactElement } from "react";
7
+ import { ConfirmDialog } from "../components/ConfirmDialog.tsx";
8
+ import type { FooterStatus } from "../components/StatusBar.tsx";
9
+ import { TUI_ACCENT, TUI_MUTED, TUI_WARN } from "../theme.ts";
10
+
11
+ /** Props for {@link DatabasePanel}. */
12
+ export type DatabasePanelProps = {
13
+ readonly cwd: string;
14
+ readonly active: boolean;
15
+ readonly onLog: (line: string) => void;
16
+ readonly onStatus: (status: FooterStatus) => void;
17
+ };
18
+
19
+ type DbAction = "push" | "generate" | "migrate" | "seed" | "studio";
20
+
21
+ type Card = {
22
+ readonly id: DbAction;
23
+ readonly key: string;
24
+ readonly title: string;
25
+ readonly blurb: string;
26
+ readonly confirm: boolean;
27
+ };
28
+
29
+ const CARDS: readonly Card[] = [
30
+ {
31
+ id: "push",
32
+ key: "P",
33
+ title: "Push",
34
+ blurb: "sync schema → live DB",
35
+ confirm: true,
36
+ },
37
+ {
38
+ id: "generate",
39
+ key: "G",
40
+ title: "Generate",
41
+ blurb: "write SQL under drizzle/",
42
+ confirm: false,
43
+ },
44
+ {
45
+ id: "migrate",
46
+ key: "M",
47
+ title: "Migrate",
48
+ blurb: "apply pending migrations",
49
+ confirm: true,
50
+ },
51
+ {
52
+ id: "seed",
53
+ key: "E",
54
+ title: "Seed",
55
+ blurb: "run defineSeed",
56
+ confirm: true,
57
+ },
58
+ {
59
+ id: "studio",
60
+ key: "T",
61
+ title: "Studio",
62
+ blurb: "drizzle-kit (browser)",
63
+ confirm: false,
64
+ },
65
+ ];
66
+
67
+ /**
68
+ * Database actions as a selectable card grid.
69
+ *
70
+ * @param props - cwd + log sink
71
+ */
72
+ export function DatabasePanel(props: DatabasePanelProps): ReactElement {
73
+ const [selected, setSelected] = useState(0);
74
+ const [pending, setPending] = useState<DbAction | null>(null);
75
+ const [busy, setBusy] = useState(false);
76
+
77
+ useInput(
78
+ (input, key) => {
79
+ if (!props.active || pending || busy) return;
80
+ if (key.leftArrow) setSelected((i) => Math.max(0, i - 1));
81
+ if (key.rightArrow) setSelected((i) => Math.min(CARDS.length - 1, i + 1));
82
+ if (key.upArrow) setSelected((i) => Math.max(0, i - 3));
83
+ if (key.downArrow) setSelected((i) => Math.min(CARDS.length - 1, i + 3));
84
+ const upper = input.toUpperCase();
85
+ const byKey = CARDS.findIndex((c) => c.key === upper);
86
+ if (byKey >= 0) {
87
+ setSelected(byKey);
88
+ const card = CARDS[byKey]!;
89
+ if (card.confirm) setPending(card.id);
90
+ else void run(card.id);
91
+ return;
92
+ }
93
+ if (key.return) {
94
+ const action = CARDS[selected];
95
+ if (!action) return;
96
+ if (action.confirm) setPending(action.id);
97
+ else void run(action.id);
98
+ }
99
+ },
100
+ { isActive: props.active && !pending && !busy },
101
+ );
102
+
103
+ const run = async (id: DbAction): Promise<void> => {
104
+ setBusy(true);
105
+ setPending(null);
106
+ props.onStatus({ kind: "running", label: `Running oke db ${id}…` });
107
+ props.onLog(`oke db ${id}\n`);
108
+ const prevCwd = process.cwd();
109
+ try {
110
+ process.chdir(props.cwd);
111
+ const { dbCli } = await import("../../db.ts");
112
+ const writes: string[] = [];
113
+ const origLog = console.log;
114
+ const origErr = console.error;
115
+ console.log = (...args: unknown[]) => {
116
+ writes.push(args.map(String).join(" ") + "\n");
117
+ };
118
+ console.error = (...args: unknown[]) => {
119
+ writes.push(args.map(String).join(" ") + "\n");
120
+ };
121
+ let code = 1;
122
+ try {
123
+ code = await dbCli([id, "--force"]);
124
+ } finally {
125
+ console.log = origLog;
126
+ console.error = origErr;
127
+ }
128
+ for (const w of writes) props.onLog(w);
129
+ props.onStatus({
130
+ kind: "done",
131
+ label: code === 0 ? `db ${id} ok` : `db ${id} exited ${code}`,
132
+ });
133
+ } catch (err) {
134
+ const m = err instanceof Error ? err.message : String(err);
135
+ props.onLog(`${m}\n`);
136
+ props.onStatus({ kind: "done", label: m });
137
+ } finally {
138
+ process.chdir(prevCwd);
139
+ setBusy(false);
140
+ }
141
+ };
142
+
143
+ if (pending) {
144
+ return (
145
+ <ConfirmDialog
146
+ message={`Run oke db ${pending}? This may change the live database.`}
147
+ onConfirm={() => void run(pending)}
148
+ onCancel={() => setPending(null)}
149
+ />
150
+ );
151
+ }
152
+
153
+ const row1 = CARDS.slice(0, 3);
154
+ const row2 = CARDS.slice(3);
155
+
156
+ return (
157
+ <Box flexDirection="column" gap={1} flexGrow={1}>
158
+ <Text bold color={TUI_ACCENT}>
159
+ Database
160
+ </Text>
161
+ <Text color={TUI_MUTED} dimColor>
162
+ Domain schema via drizzle-kit
163
+ </Text>
164
+ <Box gap={1}>
165
+ {row1.map((card, i) => (
166
+ <ActionCard key={card.id} card={card} selected={props.active && selected === i} />
167
+ ))}
168
+ </Box>
169
+ <Box gap={1}>
170
+ {row2.map((card, i) => (
171
+ <ActionCard key={card.id} card={card} selected={props.active && selected === i + 3} />
172
+ ))}
173
+ </Box>
174
+ <Text color={TUI_WARN}>! Push & Migrate modify the database — confirmation required</Text>
175
+ </Box>
176
+ );
177
+ }
178
+
179
+ /**
180
+ * One database action card.
181
+ */
182
+ function ActionCard(props: { readonly card: Card; readonly selected: boolean }): ReactElement {
183
+ const { card, selected } = props;
184
+ return (
185
+ <Box
186
+ flexDirection="column"
187
+ borderStyle="single"
188
+ borderColor={selected ? TUI_ACCENT : TUI_MUTED}
189
+ width="32%"
190
+ paddingX={1}
191
+ >
192
+ <Text bold color={selected ? TUI_ACCENT : undefined}>
193
+ {selected ? "› " : " "}[{card.key}] {card.title}
194
+ </Text>
195
+ <Text color={TUI_MUTED}>{card.blurb}</Text>
196
+ </Box>
197
+ );
198
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Dev panel — Not Running / Running states, inline shortcuts, live logs.
3
+ */
4
+
5
+ import { Box, Text, useInput } from "ink";
6
+ import { useEffect, useMemo, useState, type ReactElement } from "react";
7
+ import { DevController } from "../../dev-controller.ts";
8
+ import type { DevOwnership } from "../../dev-session-lock.ts";
9
+ import type { FooterStatus } from "../components/StatusBar.tsx";
10
+ import { StatusDot } from "../components/StatusDot.tsx";
11
+ import { formatUptime } from "../format.ts";
12
+ import { mapDevControlInput } from "../keys.ts";
13
+ import { pollPortSnapshot, type PortSnapshot } from "../state/ports.ts";
14
+ import { TUI_ACCENT, TUI_MUTED, TUI_OK, TUI_WARN } from "../theme.ts";
15
+
16
+ /** Props for {@link DevPanel}. */
17
+ export type DevPanelProps = {
18
+ readonly cwd: string;
19
+ readonly controller: DevController;
20
+ readonly onLog: (line: string) => void;
21
+ readonly active: boolean;
22
+ readonly logs: readonly string[];
23
+ readonly onStatus: (status: FooterStatus) => void;
24
+ };
25
+
26
+ type ActionId = "start" | "stop" | "restart" | "attach";
27
+
28
+ /**
29
+ * Dev controls panel.
30
+ *
31
+ * @param props - Controller + cwd
32
+ */
33
+ export function DevPanel(props: DevPanelProps): ReactElement {
34
+ const [ownership, setOwnership] = useState<DevOwnership>("stopped");
35
+ const [snap, setSnap] = useState<PortSnapshot | null>(null);
36
+ const [busy, setBusy] = useState(false);
37
+ const [startedAt, setStartedAt] = useState<number | null>(null);
38
+ const [now, setNow] = useState(Date.now());
39
+ const [selected, setSelected] = useState(0);
40
+
41
+ useEffect(() => {
42
+ const off = props.controller.on((e) => {
43
+ if (e.type === "ownership") setOwnership(e.ownership);
44
+ if (e.type === "ready") {
45
+ setOwnership(e.ownership);
46
+ if (e.ownership === "managed") setStartedAt(Date.now());
47
+ }
48
+ if (e.type === "log") props.onLog(e.text);
49
+ if (e.type === "error") {
50
+ props.onLog(`${e.message}\n`);
51
+ props.onStatus({ kind: "done", label: e.message });
52
+ }
53
+ if (e.type === "exit") setStartedAt(null);
54
+ });
55
+ void props.controller.attach(props.cwd).then((r) => {
56
+ setOwnership(r.ownership);
57
+ if (r.ownership === "managed" && r.lock) {
58
+ setStartedAt(Date.parse(r.lock.startedAt) || Date.now());
59
+ }
60
+ });
61
+ return off;
62
+ }, [props.controller, props.cwd, props.onLog, props.onStatus]);
63
+
64
+ useEffect(() => {
65
+ const id = setInterval(() => {
66
+ void pollPortSnapshot(props.cwd).then((s) => {
67
+ setOwnership(s.ownership);
68
+ setSnap(s);
69
+ });
70
+ setNow(Date.now());
71
+ }, 1500);
72
+ return () => clearInterval(id);
73
+ }, [props.cwd]);
74
+
75
+ const actions = useMemo(() => {
76
+ const running = ownership === "managed";
77
+ const external = ownership === "external";
78
+ return [
79
+ {
80
+ id: "start" as const,
81
+ label: "[S]tart",
82
+ enabled: ownership === "stopped",
83
+ },
84
+ {
85
+ id: "restart" as const,
86
+ label: "[R]estart",
87
+ enabled: running,
88
+ },
89
+ {
90
+ id: "stop" as const,
91
+ label: "[X]top",
92
+ enabled: running,
93
+ },
94
+ {
95
+ id: "attach" as const,
96
+ label: "[A]ttach",
97
+ enabled: true,
98
+ },
99
+ ].filter((a) => !(external && (a.id === "start" || a.id === "restart" || a.id === "stop")));
100
+ }, [ownership]);
101
+
102
+ useInput(
103
+ (input, key) => {
104
+ if (!props.active || busy) return;
105
+ const ctrl = mapDevControlInput(input, key);
106
+ if (ctrl === "q") return; // Esc handled by App
107
+ if (ctrl === "r" || input === "r" || input === "R") {
108
+ void runAction("restart");
109
+ return;
110
+ }
111
+ if (input === "s" || input === "S") {
112
+ void runAction("start");
113
+ return;
114
+ }
115
+ if (input === "x" || input === "X") {
116
+ void runAction("stop");
117
+ return;
118
+ }
119
+ if (input === "a" || input === "A") {
120
+ void runAction("attach");
121
+ return;
122
+ }
123
+ if (key.upArrow) {
124
+ setSelected((i) => Math.max(0, i - 1));
125
+ return;
126
+ }
127
+ if (key.downArrow) {
128
+ setSelected((i) => Math.min(actions.length - 1, i + 1));
129
+ return;
130
+ }
131
+ if (key.return) {
132
+ const action = actions[selected];
133
+ if (action) void runAction(action.id);
134
+ }
135
+ },
136
+ { isActive: props.active },
137
+ );
138
+
139
+ const runAction = async (id: ActionId): Promise<void> => {
140
+ if (busy) return;
141
+ setBusy(true);
142
+ const labels: Record<ActionId, string> = {
143
+ start: "Starting oke dev…",
144
+ stop: "Stopping…",
145
+ restart: "Restarting…",
146
+ attach: "Attaching…",
147
+ };
148
+ props.onStatus({ kind: "running", label: labels[id] });
149
+ try {
150
+ if (id === "start") {
151
+ const code = await props.controller.start({ cwd: props.cwd });
152
+ props.onStatus({
153
+ kind: "done",
154
+ label: code === 0 ? "Started" : "Start failed",
155
+ });
156
+ if (code === 0) setStartedAt(Date.now());
157
+ } else if (id === "stop") {
158
+ await props.controller.stop();
159
+ props.onStatus({ kind: "done", label: "Stopped" });
160
+ setStartedAt(null);
161
+ } else if (id === "restart") {
162
+ if (ownership !== "managed") {
163
+ props.onStatus({ kind: "done", label: "Nothing to restart" });
164
+ } else {
165
+ const code = await props.controller.restart({ cwd: props.cwd });
166
+ props.onStatus({
167
+ kind: "done",
168
+ label: code === 0 ? "Restarted" : "Restart failed",
169
+ });
170
+ if (code === 0) setStartedAt(Date.now());
171
+ }
172
+ } else if (id === "attach") {
173
+ const r = await props.controller.attach(props.cwd);
174
+ setOwnership(r.ownership);
175
+ props.onStatus({ kind: "done", label: `Ownership: ${r.ownership}` });
176
+ }
177
+ } finally {
178
+ setBusy(false);
179
+ }
180
+ };
181
+
182
+ const running = ownership === "managed" || ownership === "external";
183
+ const uptime = startedAt && ownership === "managed" ? formatUptime(now - startedAt) : null;
184
+ const app = snap?.surfaces.find((s) => s.id === "app");
185
+ const consoleSurf = snap?.surfaces.find((s) => s.id === "console");
186
+ const logTail = props.logs.slice(-Math.max(6, 12));
187
+
188
+ return (
189
+ <Box flexDirection="column" gap={1} flexGrow={1}>
190
+ <Box flexDirection="column">
191
+ <Text bold color={TUI_ACCENT}>
192
+ Dev Server
193
+ </Text>
194
+ {!running ? (
195
+ <Box flexDirection="column">
196
+ <Box gap={1}>
197
+ <Text color={TUI_WARN} bold>
198
+
199
+ </Text>
200
+ <Text bold color={TUI_WARN}>
201
+ Not Running
202
+ </Text>
203
+ </Box>
204
+ <Text color={TUI_MUTED}>Press [S] to start</Text>
205
+ </Box>
206
+ ) : (
207
+ <Box flexDirection="column">
208
+ <Box gap={1}>
209
+ <StatusDot status="ready" />
210
+ <Text bold color={TUI_OK}>
211
+ {ownership === "external" ? "Running (external)" : "Running"}
212
+ </Text>
213
+ {uptime ? <Text color={TUI_MUTED}>· uptime {uptime}</Text> : null}
214
+ </Box>
215
+ <Box gap={2}>
216
+ <Text color={TUI_MUTED}>Ports</Text>
217
+ <Text color={TUI_ACCENT}>App :{app?.port ?? "—"}</Text>
218
+ <Text color={TUI_ACCENT}>Console :{consoleSurf?.port ?? "—"}</Text>
219
+ </Box>
220
+ {ownership === "external" ? (
221
+ <Text color={TUI_WARN}>Read-only — will not start a second stack</Text>
222
+ ) : null}
223
+ </Box>
224
+ )}
225
+ </Box>
226
+
227
+ <Box gap={2}>
228
+ {actions.map((a, i) => (
229
+ <Text
230
+ key={a.id}
231
+ bold={props.active && i === selected}
232
+ color={!a.enabled ? TUI_MUTED : props.active && i === selected ? TUI_ACCENT : undefined}
233
+ dimColor={!a.enabled}
234
+ >
235
+ {props.active && i === selected ? "› " : " "}
236
+ {a.label}
237
+ </Text>
238
+ ))}
239
+ </Box>
240
+
241
+ <Box flexDirection="column" flexGrow={1} minHeight={6}>
242
+ <Text bold color={TUI_ACCENT}>
243
+ Live Logs
244
+ </Text>
245
+ {logTail.length === 0 ? (
246
+ <Text color={TUI_MUTED}>Start the dev server to see logs</Text>
247
+ ) : (
248
+ logTail.map((line, i) => (
249
+ <Text key={`${i}-${line.slice(0, 24)}`} wrap="truncate">
250
+ {line.replace(/\n$/, "")}
251
+ </Text>
252
+ ))
253
+ )}
254
+ </Box>
255
+ </Box>
256
+ );
257
+ }