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,106 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ clearDevSessionLock,
7
+ isPidAlive,
8
+ parseDevSessionLock,
9
+ readDevSessionLock,
10
+ resolveDevOwnership,
11
+ writeDevSessionLock,
12
+ type DevSessionLock,
13
+ } from "./dev-session-lock.ts";
14
+
15
+ describe("dev-session-lock", () => {
16
+ let dir: string | undefined;
17
+
18
+ afterEach(async () => {
19
+ if (dir) await rm(dir, { recursive: true, force: true });
20
+ dir = undefined;
21
+ });
22
+
23
+ test("parseDevSessionLock accepts well-formed payloads", () => {
24
+ const lock: DevSessionLock = {
25
+ pid: 1,
26
+ startedAt: "2026-01-01T00:00:00.000Z",
27
+ cwd: "/tmp/app",
28
+ ports: { app: 6530, console: 6533, mcp: 6535, docsMcp: 6536 },
29
+ };
30
+ expect(parseDevSessionLock(lock)).toEqual(lock);
31
+ expect(parseDevSessionLock(null)).toBeNull();
32
+ expect(parseDevSessionLock({ pid: "x" })).toBeNull();
33
+ });
34
+
35
+ test("isPidAlive recognizes this process", () => {
36
+ expect(isPidAlive(process.pid)).toBe(true);
37
+ expect(isPidAlive(999_999_999)).toBe(false);
38
+ });
39
+
40
+ test("write/read/clear round-trip", async () => {
41
+ dir = await mkdtemp(join(tmpdir(), "oke-dev-lock-"));
42
+ const lock: DevSessionLock = {
43
+ pid: process.pid,
44
+ startedAt: new Date().toISOString(),
45
+ cwd: dir,
46
+ ports: { app: 6530, console: 6533, mcp: 6535, docsMcp: 6536 },
47
+ };
48
+ await writeDevSessionLock(dir, lock);
49
+ expect(await readDevSessionLock(dir)).toEqual(lock);
50
+ await clearDevSessionLock(dir);
51
+ expect(await readDevSessionLock(dir)).toBeNull();
52
+ });
53
+
54
+ test("resolveDevOwnership: managed when lock pid alive and cwd matches", async () => {
55
+ dir = await mkdtemp(join(tmpdir(), "oke-dev-lock-"));
56
+ await writeDevSessionLock(dir, {
57
+ pid: process.pid,
58
+ startedAt: new Date().toISOString(),
59
+ cwd: dir,
60
+ ports: { app: 6530, console: 6533, mcp: 6535, docsMcp: 6536 },
61
+ });
62
+ const resolved = await resolveDevOwnership(
63
+ dir,
64
+ { app: 6530, console: 6533, mcp: 6535, docsMcp: 6536 },
65
+ async () => false,
66
+ );
67
+ expect(resolved.ownership).toBe("managed");
68
+ });
69
+
70
+ test("resolveDevOwnership: external when ports busy without our lock", async () => {
71
+ dir = await mkdtemp(join(tmpdir(), "oke-dev-lock-"));
72
+ const resolved = await resolveDevOwnership(
73
+ dir,
74
+ { app: 6530, console: 6533, mcp: 6535, docsMcp: 6536 },
75
+ async (port) => port === 6530,
76
+ );
77
+ expect(resolved.ownership).toBe("external");
78
+ });
79
+
80
+ test("resolveDevOwnership: stopped when free and no lock", async () => {
81
+ dir = await mkdtemp(join(tmpdir(), "oke-dev-lock-"));
82
+ const resolved = await resolveDevOwnership(
83
+ dir,
84
+ { app: 6530, console: 6533, mcp: 6535, docsMcp: 6536 },
85
+ async () => false,
86
+ );
87
+ expect(resolved.ownership).toBe("stopped");
88
+ });
89
+
90
+ test("stale lock (dead pid) is cleared and treated as stopped when ports free", async () => {
91
+ dir = await mkdtemp(join(tmpdir(), "oke-dev-lock-"));
92
+ await writeDevSessionLock(dir, {
93
+ pid: 999_999_999,
94
+ startedAt: new Date().toISOString(),
95
+ cwd: dir,
96
+ ports: { app: 6530, console: 6533, mcp: 6535, docsMcp: 6536 },
97
+ });
98
+ const resolved = await resolveDevOwnership(
99
+ dir,
100
+ { app: 6530, console: 6533, mcp: 6535, docsMcp: 6536 },
101
+ async () => false,
102
+ );
103
+ expect(resolved.ownership).toBe("stopped");
104
+ expect(await readDevSessionLock(dir)).toBeNull();
105
+ });
106
+ });
@@ -0,0 +1,209 @@
1
+ /**
2
+ * `.oke/dev.json` — session lock for owned `oke dev` / TUI sessions.
3
+ *
4
+ * Distinguishes managed (our pid) vs external (ports live, not our lock)
5
+ * so stop/restart never kills a foreign process.
6
+ */
7
+
8
+ import { mkdir } from "node:fs/promises";
9
+ import { dirname, resolve } from "node:path";
10
+ import { isPortInUse } from "./ports.ts";
11
+
12
+ /** Relative path under the project root. */
13
+ export const DEV_SESSION_LOCK_REL = ".oke/dev.json";
14
+
15
+ /** Bound ports recorded in the session lock. */
16
+ export type DevSessionPorts = {
17
+ readonly app: number;
18
+ readonly console: number;
19
+ readonly mcp: number;
20
+ readonly docsMcp: number;
21
+ };
22
+
23
+ /** On-disk lock written when this process owns the dev session. */
24
+ export type DevSessionLock = {
25
+ readonly pid: number;
26
+ readonly ports: DevSessionPorts;
27
+ readonly startedAt: string;
28
+ readonly cwd: string;
29
+ };
30
+
31
+ /** How the TUI / controller sees the live stack. */
32
+ export type DevOwnership = "managed" | "external" | "stopped";
33
+
34
+ /**
35
+ * Absolute path to the lock file for a project root.
36
+ *
37
+ * @param cwd - Project root
38
+ */
39
+ export function devSessionLockPath(cwd: string): string {
40
+ return resolve(cwd, DEV_SESSION_LOCK_REL);
41
+ }
42
+
43
+ /**
44
+ * True when `pid` looks alive on this host (signal 0).
45
+ *
46
+ * @param pid - Process id
47
+ */
48
+ export function isPidAlive(pid: number): boolean {
49
+ if (!Number.isFinite(pid) || pid <= 0) return false;
50
+ try {
51
+ process.kill(pid, 0);
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Parse and validate a lock object from JSON.
60
+ *
61
+ * @param raw - Unknown JSON value
62
+ */
63
+ export function parseDevSessionLock(raw: unknown): DevSessionLock | null {
64
+ if (raw === null || typeof raw !== "object") return null;
65
+ const o = raw as Record<string, unknown>;
66
+ if (typeof o["pid"] !== "number" || !Number.isFinite(o["pid"])) return null;
67
+ if (typeof o["startedAt"] !== "string" || typeof o["cwd"] !== "string") return null;
68
+ const ports = o["ports"];
69
+ if (ports === null || typeof ports !== "object") return null;
70
+ const p = ports as Record<string, unknown>;
71
+ for (const key of ["app", "console", "mcp", "docsMcp"] as const) {
72
+ if (typeof p[key] !== "number" || !Number.isFinite(p[key])) return null;
73
+ }
74
+ return {
75
+ pid: o["pid"],
76
+ startedAt: o["startedAt"],
77
+ cwd: o["cwd"],
78
+ ports: {
79
+ app: p["app"] as number,
80
+ console: p["console"] as number,
81
+ mcp: p["mcp"] as number,
82
+ docsMcp: p["docsMcp"] as number,
83
+ },
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Read the session lock if present and well-formed.
89
+ *
90
+ * @param cwd - Project root
91
+ */
92
+ export async function readDevSessionLock(cwd: string): Promise<DevSessionLock | null> {
93
+ const path = devSessionLockPath(cwd);
94
+ if (!(await Bun.file(path).exists())) return null;
95
+ try {
96
+ const raw: unknown = await Bun.file(path).json();
97
+ return parseDevSessionLock(raw);
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Write the session lock (creates `.oke/` as needed).
105
+ *
106
+ * @param cwd - Project root
107
+ * @param lock - Lock payload
108
+ */
109
+ export async function writeDevSessionLock(cwd: string, lock: DevSessionLock): Promise<void> {
110
+ const path = devSessionLockPath(cwd);
111
+ await mkdir(dirname(path), { recursive: true });
112
+ await Bun.write(path, `${JSON.stringify(lock, null, 2)}\n`);
113
+ }
114
+
115
+ /**
116
+ * Remove the lock file if present (best-effort).
117
+ *
118
+ * @param cwd - Project root
119
+ */
120
+ export async function clearDevSessionLock(cwd: string): Promise<void> {
121
+ const path = devSessionLockPath(cwd);
122
+ try {
123
+ await Bun.file(path)
124
+ .exists()
125
+ .then(async (ok) => {
126
+ if (ok) await unlinkQuiet(path);
127
+ });
128
+ } catch {
129
+ // ignore
130
+ }
131
+ }
132
+
133
+ /**
134
+ * @param path - Absolute path
135
+ */
136
+ async function unlinkQuiet(path: string): Promise<void> {
137
+ const { unlink } = await import("node:fs/promises");
138
+ try {
139
+ await unlink(path);
140
+ } catch {
141
+ // already gone
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Probe whether preferred ports are in use.
147
+ *
148
+ * @param ports - Ports to check
149
+ * @param probe - Injectable busy check
150
+ */
151
+ export async function probeDevPortsBusy(
152
+ ports: DevSessionPorts,
153
+ probe: (port: number) => Promise<boolean> = isPortInUse,
154
+ ): Promise<{
155
+ readonly app: boolean;
156
+ readonly console: boolean;
157
+ readonly mcp: boolean;
158
+ readonly docsMcp: boolean;
159
+ readonly any: boolean;
160
+ }> {
161
+ const app = ports.app !== 0 && (await probe(ports.app));
162
+ const consolePort = ports.console !== 0 && (await probe(ports.console));
163
+ const mcp = ports.mcp !== 0 && (await probe(ports.mcp));
164
+ const docsMcp = ports.docsMcp !== 0 && (await probe(ports.docsMcp));
165
+ return {
166
+ app,
167
+ console: consolePort,
168
+ mcp,
169
+ docsMcp,
170
+ any: app || consolePort || mcp || docsMcp,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Resolve ownership for a project: managed / external / stopped.
176
+ *
177
+ * @param cwd - Project root
178
+ * @param preferredPorts - Ports to probe when lock is missing/stale
179
+ * @param probe - Injectable busy check
180
+ */
181
+ export async function resolveDevOwnership(
182
+ cwd: string,
183
+ preferredPorts: DevSessionPorts,
184
+ probe: (port: number) => Promise<boolean> = isPortInUse,
185
+ ): Promise<{
186
+ readonly ownership: DevOwnership;
187
+ readonly lock: DevSessionLock | null;
188
+ readonly busy: Awaited<ReturnType<typeof probeDevPortsBusy>>;
189
+ }> {
190
+ const root = resolve(cwd);
191
+ const lock = await readDevSessionLock(root);
192
+ const ports = lock?.ports ?? preferredPorts;
193
+ const busy = await probeDevPortsBusy(ports, probe);
194
+
195
+ if (lock && isPidAlive(lock.pid) && resolve(lock.cwd) === root) {
196
+ return { ownership: "managed", lock, busy };
197
+ }
198
+
199
+ // Stale lock (dead pid) — ignore for ownership; treat as free or external by ports.
200
+ if (lock && !isPidAlive(lock.pid)) {
201
+ await clearDevSessionLock(root);
202
+ }
203
+
204
+ if (busy.app || busy.console) {
205
+ return { ownership: "external", lock: null, busy };
206
+ }
207
+
208
+ return { ownership: "stopped", lock: null, busy };
209
+ }