@soederpop/luca 0.0.2

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 (358) hide show
  1. package/CLAUDE.md +71 -0
  2. package/README.md +78 -0
  3. package/bun.lock +2928 -0
  4. package/bunfig.toml +3 -0
  5. package/commands/audit-docs.ts +740 -0
  6. package/commands/build-scaffolds.ts +154 -0
  7. package/commands/generate-api-docs.ts +114 -0
  8. package/commands/update-introspection.ts +67 -0
  9. package/docs/CLI.md +335 -0
  10. package/docs/README.md +88 -0
  11. package/docs/TABLE-OF-CONTENTS.md +157 -0
  12. package/docs/apis/clients/elevenlabs.md +84 -0
  13. package/docs/apis/clients/graph.md +56 -0
  14. package/docs/apis/clients/openai.md +69 -0
  15. package/docs/apis/clients/rest.md +41 -0
  16. package/docs/apis/clients/websocket.md +107 -0
  17. package/docs/apis/features/agi/assistant.md +471 -0
  18. package/docs/apis/features/agi/assistants-manager.md +154 -0
  19. package/docs/apis/features/agi/claude-code.md +602 -0
  20. package/docs/apis/features/agi/conversation-history.md +352 -0
  21. package/docs/apis/features/agi/conversation.md +333 -0
  22. package/docs/apis/features/agi/docs-reader.md +121 -0
  23. package/docs/apis/features/agi/openai-codex.md +318 -0
  24. package/docs/apis/features/agi/openapi.md +138 -0
  25. package/docs/apis/features/agi/semantic-search.md +387 -0
  26. package/docs/apis/features/agi/skills-library.md +216 -0
  27. package/docs/apis/features/node/container-link.md +133 -0
  28. package/docs/apis/features/node/content-db.md +313 -0
  29. package/docs/apis/features/node/disk-cache.md +379 -0
  30. package/docs/apis/features/node/dns.md +651 -0
  31. package/docs/apis/features/node/docker.md +705 -0
  32. package/docs/apis/features/node/downloader.md +81 -0
  33. package/docs/apis/features/node/esbuild.md +59 -0
  34. package/docs/apis/features/node/file-manager.md +182 -0
  35. package/docs/apis/features/node/fs.md +581 -0
  36. package/docs/apis/features/node/git.md +330 -0
  37. package/docs/apis/features/node/google-auth.md +174 -0
  38. package/docs/apis/features/node/google-calendar.md +187 -0
  39. package/docs/apis/features/node/google-docs.md +151 -0
  40. package/docs/apis/features/node/google-drive.md +225 -0
  41. package/docs/apis/features/node/google-sheets.md +179 -0
  42. package/docs/apis/features/node/grep.md +290 -0
  43. package/docs/apis/features/node/helpers.md +135 -0
  44. package/docs/apis/features/node/ink.md +334 -0
  45. package/docs/apis/features/node/ipc-socket.md +260 -0
  46. package/docs/apis/features/node/json-tree.md +86 -0
  47. package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
  48. package/docs/apis/features/node/networking.md +281 -0
  49. package/docs/apis/features/node/nlp.md +133 -0
  50. package/docs/apis/features/node/opener.md +97 -0
  51. package/docs/apis/features/node/os.md +118 -0
  52. package/docs/apis/features/node/package-finder.md +402 -0
  53. package/docs/apis/features/node/postgres.md +212 -0
  54. package/docs/apis/features/node/proc.md +430 -0
  55. package/docs/apis/features/node/process-manager.md +210 -0
  56. package/docs/apis/features/node/python.md +278 -0
  57. package/docs/apis/features/node/repl.md +88 -0
  58. package/docs/apis/features/node/runpod.md +673 -0
  59. package/docs/apis/features/node/secure-shell.md +169 -0
  60. package/docs/apis/features/node/semantic-search.md +401 -0
  61. package/docs/apis/features/node/sqlite.md +211 -0
  62. package/docs/apis/features/node/telegram.md +254 -0
  63. package/docs/apis/features/node/tts.md +118 -0
  64. package/docs/apis/features/node/ui.md +703 -0
  65. package/docs/apis/features/node/vault.md +64 -0
  66. package/docs/apis/features/node/vm.md +84 -0
  67. package/docs/apis/features/node/window-manager.md +337 -0
  68. package/docs/apis/features/node/yaml-tree.md +85 -0
  69. package/docs/apis/features/node/yaml.md +176 -0
  70. package/docs/apis/features/web/asset-loader.md +47 -0
  71. package/docs/apis/features/web/container-link.md +133 -0
  72. package/docs/apis/features/web/esbuild.md +59 -0
  73. package/docs/apis/features/web/helpers.md +135 -0
  74. package/docs/apis/features/web/network.md +30 -0
  75. package/docs/apis/features/web/speech.md +55 -0
  76. package/docs/apis/features/web/vault.md +64 -0
  77. package/docs/apis/features/web/vm.md +84 -0
  78. package/docs/apis/features/web/voice.md +67 -0
  79. package/docs/apis/servers/express.md +127 -0
  80. package/docs/apis/servers/mcp.md +213 -0
  81. package/docs/apis/servers/websocket.md +99 -0
  82. package/docs/documentation-audit.md +134 -0
  83. package/docs/examples/content-db.md +77 -0
  84. package/docs/examples/disk-cache.md +83 -0
  85. package/docs/examples/docker.md +101 -0
  86. package/docs/examples/downloader.md +70 -0
  87. package/docs/examples/esbuild.md +80 -0
  88. package/docs/examples/file-manager.md +82 -0
  89. package/docs/examples/fs.md +83 -0
  90. package/docs/examples/git.md +85 -0
  91. package/docs/examples/google-auth.md +88 -0
  92. package/docs/examples/google-calendar.md +94 -0
  93. package/docs/examples/google-docs.md +82 -0
  94. package/docs/examples/google-drive.md +96 -0
  95. package/docs/examples/google-sheets.md +95 -0
  96. package/docs/examples/grep.md +85 -0
  97. package/docs/examples/ink-blocks.md +75 -0
  98. package/docs/examples/ink-renderer.md +41 -0
  99. package/docs/examples/ink.md +103 -0
  100. package/docs/examples/ipc-socket.md +103 -0
  101. package/docs/examples/json-tree.md +91 -0
  102. package/docs/examples/launcher-app-command-listener.md +120 -0
  103. package/docs/examples/networking.md +58 -0
  104. package/docs/examples/nlp.md +91 -0
  105. package/docs/examples/opener.md +78 -0
  106. package/docs/examples/os.md +72 -0
  107. package/docs/examples/package-finder.md +89 -0
  108. package/docs/examples/port-exposer.md +89 -0
  109. package/docs/examples/postgres.md +91 -0
  110. package/docs/examples/proc.md +81 -0
  111. package/docs/examples/process-manager.md +79 -0
  112. package/docs/examples/python.md +91 -0
  113. package/docs/examples/repl.md +93 -0
  114. package/docs/examples/runpod.md +119 -0
  115. package/docs/examples/secure-shell.md +92 -0
  116. package/docs/examples/sqlite.md +86 -0
  117. package/docs/examples/telegram.md +77 -0
  118. package/docs/examples/tts.md +86 -0
  119. package/docs/examples/ui.md +80 -0
  120. package/docs/examples/vault.md +70 -0
  121. package/docs/examples/vm.md +86 -0
  122. package/docs/examples/window-manager.md +125 -0
  123. package/docs/examples/yaml-tree.md +93 -0
  124. package/docs/examples/yaml.md +104 -0
  125. package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
  126. package/docs/ideas/container-use-api.md +9 -0
  127. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  128. package/docs/ideas/feature-stacks.md +22 -0
  129. package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
  130. package/docs/ideas/mcp-design.md +9 -0
  131. package/docs/ideas/web-container-debugging-feature.md +13 -0
  132. package/docs/introspection-audit.md +49 -0
  133. package/docs/introspection.md +154 -0
  134. package/docs/mcp/readme.md +162 -0
  135. package/docs/models.ts +38 -0
  136. package/docs/philosophy.md +85 -0
  137. package/docs/principles.md +7 -0
  138. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  139. package/docs/prompts/mcp-test-easy-command.md +27 -0
  140. package/docs/reports/assistant-bugs.md +38 -0
  141. package/docs/reports/attach-pattern-usage.md +18 -0
  142. package/docs/reports/code-audit-results.md +391 -0
  143. package/docs/reports/introspection-audit-tasks.md +378 -0
  144. package/docs/reports/luca-mcp-improvements.md +128 -0
  145. package/docs/scaffolds/client.md +140 -0
  146. package/docs/scaffolds/command.md +106 -0
  147. package/docs/scaffolds/endpoint.md +176 -0
  148. package/docs/scaffolds/feature.md +148 -0
  149. package/docs/scaffolds/server.md +187 -0
  150. package/docs/tasks/web-container-helper-discovery.md +71 -0
  151. package/docs/todos.md +1 -0
  152. package/docs/tutorials/01-getting-started.md +106 -0
  153. package/docs/tutorials/02-container.md +210 -0
  154. package/docs/tutorials/03-scripts.md +194 -0
  155. package/docs/tutorials/04-features-overview.md +196 -0
  156. package/docs/tutorials/05-state-and-events.md +171 -0
  157. package/docs/tutorials/06-servers.md +157 -0
  158. package/docs/tutorials/07-endpoints.md +198 -0
  159. package/docs/tutorials/08-commands.md +171 -0
  160. package/docs/tutorials/09-clients.md +162 -0
  161. package/docs/tutorials/10-creating-features.md +198 -0
  162. package/docs/tutorials/11-contentbase.md +191 -0
  163. package/docs/tutorials/12-assistants.md +215 -0
  164. package/docs/tutorials/13-introspection.md +147 -0
  165. package/docs/tutorials/14-type-system.md +174 -0
  166. package/docs/tutorials/15-project-patterns.md +222 -0
  167. package/docs/tutorials/16-google-features.md +534 -0
  168. package/docs/tutorials/17-tui-blocks.md +530 -0
  169. package/docs/tutorials/18-semantic-search.md +334 -0
  170. package/index.ts +1 -0
  171. package/luca.console.ts +9 -0
  172. package/main.py +6 -0
  173. package/package.json +154 -0
  174. package/pyproject.toml +7 -0
  175. package/scripts/animations/chrome-glitch.ts +55 -0
  176. package/scripts/animations/index.ts +16 -0
  177. package/scripts/animations/neon-pulse.ts +64 -0
  178. package/scripts/animations/types.ts +6 -0
  179. package/scripts/build-web.ts +28 -0
  180. package/scripts/examples/ask-luca-expert.ts +42 -0
  181. package/scripts/examples/assistant-questions.ts +12 -0
  182. package/scripts/examples/excalidraw-expert.ts +75 -0
  183. package/scripts/examples/expert-chat.ts +0 -0
  184. package/scripts/examples/file-manager.ts +14 -0
  185. package/scripts/examples/ideas.ts +12 -0
  186. package/scripts/examples/interactive-chat.ts +20 -0
  187. package/scripts/examples/openai-tool-calls.ts +113 -0
  188. package/scripts/examples/opening-a-web-browser.ts +5 -0
  189. package/scripts/examples/telegram-bot.ts +79 -0
  190. package/scripts/examples/telegram-ink-ui.ts +302 -0
  191. package/scripts/examples/using-assistant-with-mcp.ts +560 -0
  192. package/scripts/examples/using-claude-code.ts +10 -0
  193. package/scripts/examples/using-contentdb.ts +35 -0
  194. package/scripts/examples/using-conversations.ts +35 -0
  195. package/scripts/examples/using-disk-cache.ts +10 -0
  196. package/scripts/examples/using-docker-shell.ts +75 -0
  197. package/scripts/examples/using-elevenlabs.ts +25 -0
  198. package/scripts/examples/using-google-calendar.ts +57 -0
  199. package/scripts/examples/using-google-docs.ts +74 -0
  200. package/scripts/examples/using-google-drive.ts +74 -0
  201. package/scripts/examples/using-google-sheets.ts +89 -0
  202. package/scripts/examples/using-nlp.ts +55 -0
  203. package/scripts/examples/using-ollama.ts +10 -0
  204. package/scripts/examples/using-openai-codex.ts +23 -0
  205. package/scripts/examples/using-postgres.ts +55 -0
  206. package/scripts/examples/using-runpod.ts +32 -0
  207. package/scripts/examples/using-tts.ts +40 -0
  208. package/scripts/examples/vm-loading-esm-modules.ts +16 -0
  209. package/scripts/scaffold.ts +391 -0
  210. package/scripts/scratch.ts +15 -0
  211. package/scripts/test-command-listener.ts +123 -0
  212. package/scripts/test-window-manager-lifecycle.ts +86 -0
  213. package/scripts/test-window-manager.ts +43 -0
  214. package/scripts/update-introspection-data.ts +58 -0
  215. package/src/agi/README.md +14 -0
  216. package/src/agi/container.server.ts +114 -0
  217. package/src/agi/endpoints/ask.ts +60 -0
  218. package/src/agi/endpoints/conversations/[id].ts +45 -0
  219. package/src/agi/endpoints/conversations.ts +31 -0
  220. package/src/agi/endpoints/experts.ts +37 -0
  221. package/src/agi/features/assistant.ts +767 -0
  222. package/src/agi/features/assistants-manager.ts +260 -0
  223. package/src/agi/features/claude-code.ts +1111 -0
  224. package/src/agi/features/conversation-history.ts +497 -0
  225. package/src/agi/features/conversation.ts +799 -0
  226. package/src/agi/features/openai-codex.ts +631 -0
  227. package/src/agi/features/openapi.ts +438 -0
  228. package/src/agi/features/skills-library.ts +425 -0
  229. package/src/agi/index.ts +6 -0
  230. package/src/agi/lib/token-counter.ts +122 -0
  231. package/src/browser.ts +25 -0
  232. package/src/bus.ts +100 -0
  233. package/src/cli/cli.ts +70 -0
  234. package/src/client.ts +461 -0
  235. package/src/clients/civitai/index.ts +541 -0
  236. package/src/clients/client-template.ts +41 -0
  237. package/src/clients/comfyui/index.ts +597 -0
  238. package/src/clients/elevenlabs/index.ts +291 -0
  239. package/src/clients/openai/index.ts +451 -0
  240. package/src/clients/supabase/index.ts +366 -0
  241. package/src/command.ts +164 -0
  242. package/src/commands/chat.ts +182 -0
  243. package/src/commands/console.ts +192 -0
  244. package/src/commands/describe.ts +433 -0
  245. package/src/commands/eval.ts +116 -0
  246. package/src/commands/help.ts +214 -0
  247. package/src/commands/index.ts +14 -0
  248. package/src/commands/mcp.ts +64 -0
  249. package/src/commands/prompt.ts +807 -0
  250. package/src/commands/run.ts +257 -0
  251. package/src/commands/sandbox-mcp.ts +439 -0
  252. package/src/commands/scaffold.ts +79 -0
  253. package/src/commands/serve.ts +172 -0
  254. package/src/container.ts +781 -0
  255. package/src/endpoint.ts +340 -0
  256. package/src/feature.ts +75 -0
  257. package/src/hash-object.ts +97 -0
  258. package/src/helper.ts +543 -0
  259. package/src/introspection/generated.agi.ts +23388 -0
  260. package/src/introspection/generated.node.ts +18899 -0
  261. package/src/introspection/generated.web.ts +2021 -0
  262. package/src/introspection/index.ts +256 -0
  263. package/src/introspection/scan.ts +912 -0
  264. package/src/node/container.ts +354 -0
  265. package/src/node/feature.ts +13 -0
  266. package/src/node/features/container-link.ts +558 -0
  267. package/src/node/features/content-db.ts +475 -0
  268. package/src/node/features/disk-cache.ts +382 -0
  269. package/src/node/features/dns.ts +655 -0
  270. package/src/node/features/docker.ts +912 -0
  271. package/src/node/features/downloader.ts +92 -0
  272. package/src/node/features/esbuild.ts +68 -0
  273. package/src/node/features/file-manager.ts +357 -0
  274. package/src/node/features/fs.ts +534 -0
  275. package/src/node/features/git.ts +492 -0
  276. package/src/node/features/google-auth.ts +502 -0
  277. package/src/node/features/google-calendar.ts +300 -0
  278. package/src/node/features/google-docs.ts +404 -0
  279. package/src/node/features/google-drive.ts +339 -0
  280. package/src/node/features/google-sheets.ts +279 -0
  281. package/src/node/features/grep.ts +406 -0
  282. package/src/node/features/helpers.ts +374 -0
  283. package/src/node/features/ink.ts +490 -0
  284. package/src/node/features/ipc-socket.ts +459 -0
  285. package/src/node/features/json-tree.ts +188 -0
  286. package/src/node/features/launcher-app-command-listener.ts +388 -0
  287. package/src/node/features/networking.ts +925 -0
  288. package/src/node/features/nlp.ts +211 -0
  289. package/src/node/features/opener.ts +166 -0
  290. package/src/node/features/os.ts +157 -0
  291. package/src/node/features/package-finder.ts +539 -0
  292. package/src/node/features/port-exposer.ts +342 -0
  293. package/src/node/features/postgres.ts +273 -0
  294. package/src/node/features/proc.ts +502 -0
  295. package/src/node/features/process-manager.ts +542 -0
  296. package/src/node/features/python.ts +444 -0
  297. package/src/node/features/repl.ts +194 -0
  298. package/src/node/features/runpod.ts +802 -0
  299. package/src/node/features/secure-shell.ts +248 -0
  300. package/src/node/features/semantic-search.ts +924 -0
  301. package/src/node/features/sqlite.ts +289 -0
  302. package/src/node/features/telegram.ts +342 -0
  303. package/src/node/features/tts.ts +184 -0
  304. package/src/node/features/ui.ts +857 -0
  305. package/src/node/features/vault.ts +164 -0
  306. package/src/node/features/vm.ts +312 -0
  307. package/src/node/features/window-manager.ts +804 -0
  308. package/src/node/features/yaml-tree.ts +149 -0
  309. package/src/node/features/yaml.ts +132 -0
  310. package/src/node.ts +70 -0
  311. package/src/react/index.ts +175 -0
  312. package/src/registry.ts +199 -0
  313. package/src/scaffolds/generated.ts +1613 -0
  314. package/src/scaffolds/template.ts +37 -0
  315. package/src/schemas/base.ts +255 -0
  316. package/src/server.ts +135 -0
  317. package/src/servers/express.ts +209 -0
  318. package/src/servers/mcp.ts +805 -0
  319. package/src/servers/socket.ts +120 -0
  320. package/src/state.ts +101 -0
  321. package/src/web/clients/socket.ts +82 -0
  322. package/src/web/container.ts +74 -0
  323. package/src/web/extension.ts +30 -0
  324. package/src/web/feature.ts +12 -0
  325. package/src/web/features/asset-loader.ts +64 -0
  326. package/src/web/features/container-link.ts +385 -0
  327. package/src/web/features/esbuild.ts +79 -0
  328. package/src/web/features/helpers.ts +267 -0
  329. package/src/web/features/network.ts +61 -0
  330. package/src/web/features/speech.ts +87 -0
  331. package/src/web/features/vault.ts +189 -0
  332. package/src/web/features/vm.ts +78 -0
  333. package/src/web/features/voice-recognition.ts +129 -0
  334. package/src/web/shims/isomorphic-vm.ts +149 -0
  335. package/test/bus.test.ts +134 -0
  336. package/test/clients-servers.test.ts +216 -0
  337. package/test/container-link.test.ts +274 -0
  338. package/test/features.test.ts +160 -0
  339. package/test/integration.test.ts +787 -0
  340. package/test/node-container.test.ts +121 -0
  341. package/test/rate-limit.test.ts +272 -0
  342. package/test/semantic-search.test.ts +550 -0
  343. package/test/state.test.ts +121 -0
  344. package/test-integration/assistant.test.ts +138 -0
  345. package/test-integration/assistants-manager.test.ts +123 -0
  346. package/test-integration/claude-code.test.ts +98 -0
  347. package/test-integration/conversation-history.test.ts +205 -0
  348. package/test-integration/conversation.test.ts +137 -0
  349. package/test-integration/elevenlabs.test.ts +55 -0
  350. package/test-integration/google-services.test.ts +80 -0
  351. package/test-integration/helpers.ts +89 -0
  352. package/test-integration/openai-codex.test.ts +93 -0
  353. package/test-integration/runpod.test.ts +58 -0
  354. package/test-integration/server-endpoints.test.ts +97 -0
  355. package/test-integration/skills-library.test.ts +157 -0
  356. package/test-integration/telegram.test.ts +46 -0
  357. package/tsconfig.json +58 -0
  358. package/uv.lock +8 -0
@@ -0,0 +1,542 @@
1
+ import { z } from 'zod'
2
+ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
+ import { Feature, features } from '../feature.js'
4
+ import { State } from '../../state.js'
5
+ import { Bus, type EventMap } from '../../bus.js'
6
+
7
+ // ─── Schemas ────────────────────────────────────────────────────────────────
8
+
9
+ const ProcessMetadataSchema = z.object({
10
+ id: z.string().describe('Unique process identifier'),
11
+ tag: z.string().optional().describe('User-defined tag for lookups'),
12
+ command: z.string().describe('The command that was spawned'),
13
+ args: z.array(z.string()).describe('Arguments passed to the command'),
14
+ pid: z.number().optional().describe('OS process ID'),
15
+ status: z.enum(['running', 'exited', 'crashed', 'killed']).describe('Current process lifecycle status'),
16
+ exitCode: z.number().optional().describe('Exit code after process ends'),
17
+ startedAt: z.number().describe('Timestamp when the process was spawned'),
18
+ endedAt: z.number().optional().describe('Timestamp when the process ended'),
19
+ })
20
+
21
+ export const ProcessManagerStateSchema = FeatureStateSchema.extend({
22
+ processes: z.record(z.string(), ProcessMetadataSchema)
23
+ .describe('Map of process ID to metadata'),
24
+ totalSpawned: z.number().default(0)
25
+ .describe('Total number of processes spawned since feature creation'),
26
+ })
27
+ export type ProcessManagerState = z.infer<typeof ProcessManagerStateSchema>
28
+
29
+ export const ProcessManagerOptionsSchema = FeatureOptionsSchema.extend({
30
+ autoCleanup: z.boolean().default(true)
31
+ .describe('Register process.on exit/SIGINT/SIGTERM handlers to kill all tracked processes'),
32
+ })
33
+ export type ProcessManagerOptions = z.infer<typeof ProcessManagerOptionsSchema>
34
+
35
+ export const ProcessManagerEventsSchema = FeatureEventsSchema.extend({
36
+ spawned: z.tuple([z.string().describe('process ID'), z.any().describe('process metadata')])
37
+ .describe('Emitted when a new process is spawned'),
38
+ exited: z.tuple([z.string().describe('process ID'), z.number().describe('exit code')])
39
+ .describe('Emitted when a process exits normally'),
40
+ crashed: z.tuple([z.string().describe('process ID'), z.number().describe('exit code'), z.any().describe('error info')])
41
+ .describe('Emitted when a process exits with non-zero code'),
42
+ killed: z.tuple([z.string().describe('process ID')])
43
+ .describe('Emitted when a process is killed'),
44
+ allStopped: z.tuple([])
45
+ .describe('Emitted when all tracked processes have stopped'),
46
+ })
47
+
48
+ // ─── SpawnHandler ───────────────────────────────────────────────────────────
49
+
50
+ interface SpawnHandlerState {
51
+ id: string
52
+ tag?: string
53
+ command: string
54
+ args: string[]
55
+ pid?: number
56
+ status: 'running' | 'exited' | 'crashed' | 'killed'
57
+ exitCode?: number
58
+ startedAt: number
59
+ endedAt?: number
60
+ }
61
+
62
+ interface SpawnHandlerEvents extends EventMap {
63
+ stdout: [data: string]
64
+ stderr: [data: string]
65
+ exit: [code: number]
66
+ crash: [code: number]
67
+ killed: []
68
+ }
69
+
70
+ export interface SpawnOptions {
71
+ /** User-defined tag for later lookups via getByTag() */
72
+ tag?: string
73
+ /** Working directory for the spawned process (defaults to container cwd) */
74
+ cwd?: string
75
+ /** Additional environment variables merged with process.env */
76
+ env?: Record<string, string>
77
+ /** stdin mode: 'pipe' to write to the process, 'inherit', or 'ignore' (default: 'ignore') */
78
+ stdin?: 'pipe' | 'inherit' | 'ignore' | null
79
+ /** stdout mode: 'pipe' to capture output, 'inherit', or 'ignore' (default: 'pipe') */
80
+ stdout?: 'pipe' | 'inherit' | 'ignore' | null
81
+ /** stderr mode: 'pipe' to capture errors, 'inherit', or 'ignore' (default: 'pipe') */
82
+ stderr?: 'pipe' | 'inherit' | 'ignore' | null
83
+ }
84
+
85
+ /**
86
+ * A handle to a spawned long-running process.
87
+ *
88
+ * Provides observable state, events, and methods to interact with
89
+ * the running process. Returned immediately from `ProcessManager.spawn()`
90
+ * without blocking.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const handler = pm.spawn('node', ['server.js'], { tag: 'api' })
95
+ * handler.on('stdout', (data) => console.log(data))
96
+ * handler.on('crash', (code) => console.error('crashed:', code))
97
+ * const exitCode = await handler.await()
98
+ * ```
99
+ */
100
+ export class SpawnHandler {
101
+ readonly state: State<SpawnHandlerState>
102
+ readonly events = new Bus<SpawnHandlerEvents>()
103
+
104
+ private _process!: ReturnType<typeof Bun.spawn>
105
+ private _manager: ProcessManager
106
+ private _exitPromise: Promise<number> | null = null
107
+ private _exitResolve: ((code: number) => void) | null = null
108
+
109
+ constructor(
110
+ id: string,
111
+ command: string,
112
+ args: string[],
113
+ manager: ProcessManager,
114
+ options: SpawnOptions = {}
115
+ ) {
116
+ this._manager = manager
117
+ this.state = new State<SpawnHandlerState>({
118
+ initialState: {
119
+ id,
120
+ command,
121
+ args,
122
+ tag: options.tag,
123
+ status: 'running',
124
+ startedAt: Date.now(),
125
+ }
126
+ })
127
+
128
+ this._exitPromise = new Promise<number>((resolve) => {
129
+ this._exitResolve = resolve
130
+ })
131
+ }
132
+
133
+ /** The unique process identifier */
134
+ get id() { return this.state.get('id')! }
135
+
136
+ /** The user-defined tag, if any */
137
+ get tag() { return this.state.get('tag') }
138
+
139
+ /** The OS process ID */
140
+ get pid() { return this.state.get('pid') }
141
+
142
+ /** Whether the process is still running */
143
+ get isRunning() { return this.state.get('status') === 'running' }
144
+
145
+ /** Whether the process has finished (exited, crashed, or killed) */
146
+ get isDone() {
147
+ const s = this.state.get('status')
148
+ return s === 'exited' || s === 'crashed' || s === 'killed'
149
+ }
150
+
151
+ /** Current process lifecycle status */
152
+ get status() { return this.state.get('status')! }
153
+
154
+ /** Exit code after process ends */
155
+ get exitCode() { return this.state.get('exitCode') }
156
+
157
+ /**
158
+ * Start the process. Called internally by `ProcessManager.spawn()`.
159
+ */
160
+ _start(spawnOptions: SpawnOptions = {}): void {
161
+ const command = this.state.get('command')!
162
+ const args = this.state.get('args')!
163
+
164
+ const proc = Bun.spawn([command, ...args], {
165
+ cwd: spawnOptions.cwd ?? this._manager.container.cwd,
166
+ env: { ...process.env, ...spawnOptions.env },
167
+ stdin: spawnOptions.stdin ?? 'ignore',
168
+ stdout: spawnOptions.stdout ?? 'pipe',
169
+ stderr: spawnOptions.stderr ?? 'pipe',
170
+ })
171
+
172
+ this._process = proc
173
+ this.state.set('pid', proc.pid)
174
+
175
+ // Stream stdout
176
+ if (proc.stdout && typeof proc.stdout === 'object' && 'getReader' in proc.stdout) {
177
+ this._readStream(proc.stdout as ReadableStream<Uint8Array>, 'stdout')
178
+ }
179
+
180
+ // Stream stderr
181
+ if (proc.stderr && typeof proc.stderr === 'object' && 'getReader' in proc.stderr) {
182
+ this._readStream(proc.stderr as ReadableStream<Uint8Array>, 'stderr')
183
+ }
184
+
185
+ // Wait for exit
186
+ proc.exited.then((code: number) => {
187
+ this._onExit(code)
188
+ })
189
+ }
190
+
191
+ /**
192
+ * Kill the process.
193
+ *
194
+ * @param signal - Signal to send (default: SIGTERM)
195
+ */
196
+ kill(signal: NodeJS.Signals | number = 'SIGTERM'): void {
197
+ if (this.isDone) return
198
+
199
+ try {
200
+ const pid = this.state.get('pid')
201
+ if (pid) {
202
+ process.kill(pid, signal)
203
+ }
204
+ } catch (err: any) {
205
+ if (err.code !== 'ESRCH') throw err
206
+ }
207
+
208
+ this.state.set('status', 'killed')
209
+ this.state.set('endedAt', Date.now())
210
+ this.events.emit('killed')
211
+ this._manager._onHandlerDone(this, 'killed')
212
+
213
+ if (this._exitResolve) {
214
+ this._exitResolve(-1)
215
+ this._exitResolve = null
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Returns a promise that resolves with the exit code when the process finishes.
221
+ */
222
+ async await(): Promise<number> {
223
+ if (this.isDone) {
224
+ return this.state.get('exitCode') ?? -1
225
+ }
226
+ return this._exitPromise!
227
+ }
228
+
229
+ /**
230
+ * Write data to the process's stdin (requires `stdin: 'pipe'` in spawn options).
231
+ *
232
+ * @param data - String or Uint8Array to write
233
+ */
234
+ write(data: string | Uint8Array): void {
235
+ const stdin = this._process?.stdin
236
+ if (!stdin || typeof stdin === 'number') {
237
+ throw new Error('stdin is not piped — pass { stdin: "pipe" } in spawn options')
238
+ }
239
+ ;(stdin as import('bun').FileSink).write(data)
240
+ }
241
+
242
+ /** Subscribe to handler events */
243
+ on<E extends string & keyof SpawnHandlerEvents>(event: E, listener: (...args: SpawnHandlerEvents[E]) => void) {
244
+ this.events.on(event, listener)
245
+ return this
246
+ }
247
+
248
+ /** Subscribe to a handler event once */
249
+ once<E extends string & keyof SpawnHandlerEvents>(event: E, listener: (...args: SpawnHandlerEvents[E]) => void) {
250
+ this.events.once(event, listener)
251
+ return this
252
+ }
253
+
254
+ /** Unsubscribe from a handler event */
255
+ off<E extends string & keyof SpawnHandlerEvents>(event: E, listener: (...args: SpawnHandlerEvents[E]) => void) {
256
+ this.events.off(event, listener)
257
+ return this
258
+ }
259
+
260
+ // ─── Internal ─────────────────────────────────────────────────────────────
261
+
262
+ private async _readStream(stream: ReadableStream<Uint8Array>, type: 'stdout' | 'stderr') {
263
+ const reader = stream.getReader()
264
+ const decoder = new TextDecoder()
265
+
266
+ try {
267
+ while (true) {
268
+ const { done, value } = await reader.read()
269
+ if (done) break
270
+ const text = decoder.decode(value, { stream: true })
271
+ this.events.emit(type, text)
272
+ }
273
+ } catch {
274
+ // Stream closed — process is ending
275
+ }
276
+ }
277
+
278
+ private _onExit(code: number): void {
279
+ if (this.isDone) return
280
+
281
+ const isCrash = code !== 0
282
+ this.state.set('exitCode', code)
283
+ this.state.set('endedAt', Date.now())
284
+
285
+ if (isCrash) {
286
+ this.state.set('status', 'crashed')
287
+ this.events.emit('crash', code)
288
+ this._manager._onHandlerDone(this, 'crashed', code)
289
+ } else {
290
+ this.state.set('status', 'exited')
291
+ this.events.emit('exit', code)
292
+ this._manager._onHandlerDone(this, 'exited', code)
293
+ }
294
+
295
+ if (this._exitResolve) {
296
+ this._exitResolve(code)
297
+ this._exitResolve = null
298
+ }
299
+ }
300
+ }
301
+
302
+ // ─── ProcessManager Feature ─────────────────────────────────────────────────
303
+
304
+ /**
305
+ * Manages long-running child processes with tracking, events, and automatic cleanup.
306
+ *
307
+ * Unlike the `proc` feature whose spawn methods block until the child exits,
308
+ * ProcessManager returns a SpawnHandler immediately — a handle object with its own
309
+ * state, events, and lifecycle methods. The feature tracks all spawned processes,
310
+ * maintains observable state, and can automatically kill them on parent exit.
311
+ *
312
+ * @example
313
+ * ```typescript
314
+ * const pm = container.feature('processManager', { enable: true })
315
+ *
316
+ * const server = pm.spawn('node', ['server.js'], { tag: 'api', cwd: '/app' })
317
+ * server.on('stdout', (data) => console.log('[api]', data))
318
+ * server.on('crash', (code) => console.error('API crashed:', code))
319
+ *
320
+ * // Kill one
321
+ * server.kill()
322
+ *
323
+ * // Kill all tracked processes
324
+ * pm.killAll()
325
+ *
326
+ * // List and lookup
327
+ * pm.list() // SpawnHandler[]
328
+ * pm.getByTag('api') // SpawnHandler | undefined
329
+ * ```
330
+ *
331
+ * @extends Feature
332
+ */
333
+ export class ProcessManager extends Feature {
334
+ static override shortcut = 'features.processManager' as const
335
+ static override stateSchema = ProcessManagerStateSchema
336
+ static override optionsSchema = ProcessManagerOptionsSchema
337
+ static override eventsSchema = ProcessManagerEventsSchema
338
+
339
+ private _handlers = new Map<string, SpawnHandler>()
340
+ private _cleanupRegistered = false
341
+ private _cleanupHandlers: Array<() => void> = []
342
+
343
+ /**
344
+ * Spawn a long-running process and return a handle immediately.
345
+ *
346
+ * The returned SpawnHandler provides events for stdout/stderr streaming,
347
+ * exit/crash notifications, and methods to kill or await the process.
348
+ *
349
+ * @param command - The command to execute (e.g. 'node', 'bun', 'python')
350
+ * @param args - Arguments to pass to the command
351
+ * @param options - Spawn configuration
352
+ * @param options.tag - User-defined tag for later lookups via getByTag()
353
+ * @param options.cwd - Working directory (defaults to container cwd)
354
+ * @param options.env - Additional environment variables
355
+ * @param options.stdin - stdin mode: 'pipe', 'inherit', 'ignore' (default: 'ignore')
356
+ * @param options.stdout - stdout mode: 'pipe', 'inherit', 'ignore' (default: 'pipe')
357
+ * @param options.stderr - stderr mode: 'pipe', 'inherit', 'ignore' (default: 'pipe')
358
+ * @returns SpawnHandler — a non-blocking handle to the process
359
+ */
360
+ spawn(command: string, args: string[] = [], options: SpawnOptions = {}): SpawnHandler {
361
+ const id = crypto.randomUUID()
362
+ const handler = new SpawnHandler(id, command, args, this, options)
363
+
364
+ this._handlers.set(id, handler)
365
+
366
+ // Register cleanup on first spawn
367
+ if (!this._cleanupRegistered && (this.options as ProcessManagerOptions).autoCleanup !== false) {
368
+ this._registerCleanup()
369
+ }
370
+
371
+ handler._start(options)
372
+
373
+ // Update feature-level state
374
+ const totalSpawned = ((this.state.get('totalSpawned' as any) as number) ?? 0) + 1
375
+ this.state.set('totalSpawned' as any, totalSpawned)
376
+
377
+ const processes = { ...(this.state.get('processes' as any) as Record<string, any> ?? {}) }
378
+ processes[id] = {
379
+ id,
380
+ tag: options.tag,
381
+ command,
382
+ args,
383
+ pid: handler.pid,
384
+ status: 'running',
385
+ startedAt: Date.now(),
386
+ }
387
+ this.state.set('processes' as any, processes)
388
+
389
+ this.emit('spawned', id, processes[id])
390
+
391
+ return handler
392
+ }
393
+
394
+ /**
395
+ * Get a SpawnHandler by its unique ID.
396
+ *
397
+ * @param id - The process ID returned by spawn
398
+ * @returns The SpawnHandler, or undefined if not found
399
+ */
400
+ get(id: string): SpawnHandler | undefined {
401
+ return this._handlers.get(id)
402
+ }
403
+
404
+ /**
405
+ * Find a SpawnHandler by its user-defined tag.
406
+ *
407
+ * @param tag - The tag passed to spawn()
408
+ * @returns The first matching SpawnHandler, or undefined
409
+ */
410
+ getByTag(tag: string): SpawnHandler | undefined {
411
+ for (const handler of this._handlers.values()) {
412
+ if (handler.tag === tag) return handler
413
+ }
414
+ return undefined
415
+ }
416
+
417
+ /**
418
+ * List all tracked SpawnHandlers (running and finished).
419
+ *
420
+ * @returns Array of all SpawnHandlers
421
+ */
422
+ list(): SpawnHandler[] {
423
+ return Array.from(this._handlers.values())
424
+ }
425
+
426
+ /**
427
+ * Kill all running processes.
428
+ *
429
+ * @param signal - Signal to send (default: SIGTERM)
430
+ */
431
+ killAll(signal?: NodeJS.Signals | number): void {
432
+ for (const handler of this._handlers.values()) {
433
+ if (handler.isRunning) {
434
+ handler.kill(signal)
435
+ }
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Stop the process manager: kill all running processes and remove cleanup handlers.
441
+ */
442
+ async stop(): Promise<void> {
443
+ this.killAll()
444
+ this._removeCleanup()
445
+ }
446
+
447
+ /**
448
+ * Remove a finished handler from tracking.
449
+ *
450
+ * @param id - The process ID to remove
451
+ * @returns True if the handler was found and removed
452
+ */
453
+ remove(id: string): boolean {
454
+ const handler = this._handlers.get(id)
455
+ if (!handler) return false
456
+ if (handler.isRunning) {
457
+ throw new Error(`Cannot remove running process ${id} — kill it first`)
458
+ }
459
+
460
+ this._handlers.delete(id)
461
+
462
+ const processes = { ...(this.state.get('processes') as any ?? {}) }
463
+ delete processes[id]
464
+ this.state.set('processes' as any, processes)
465
+
466
+ return true
467
+ }
468
+
469
+ override async enable(options: any = {}): Promise<this> {
470
+ await super.enable(options)
471
+ return this
472
+ }
473
+
474
+ // ─── Internal ─────────────────────────────────────────────────────────────
475
+
476
+ /** Called by SpawnHandler when a process finishes. Updates feature-level state. */
477
+ _onHandlerDone(handler: SpawnHandler, status: 'exited' | 'crashed' | 'killed', exitCode?: number): void {
478
+ const id = handler.id
479
+
480
+ // Update feature-level process record
481
+ const processes = { ...(this.state.get('processes') as any ?? {}) }
482
+ if (processes[id]) {
483
+ processes[id] = {
484
+ ...processes[id],
485
+ status,
486
+ exitCode: exitCode ?? (status === 'killed' ? -1 : undefined),
487
+ endedAt: Date.now(),
488
+ }
489
+ this.state.set('processes' as any, processes)
490
+ }
491
+
492
+ // Emit feature-level events
493
+ if (status === 'exited') {
494
+ this.emit('exited', id, exitCode ?? 0)
495
+ } else if (status === 'crashed') {
496
+ this.emit('crashed', id, exitCode ?? 1, { command: handler.state.get('command'), args: handler.state.get('args') })
497
+ } else if (status === 'killed') {
498
+ this.emit('killed', id)
499
+ }
500
+
501
+ // Check if all processes are done
502
+ const allDone = Array.from(this._handlers.values()).every(h => h.isDone)
503
+ if (allDone && this._handlers.size > 0) {
504
+ this.emit('allStopped')
505
+ }
506
+ }
507
+
508
+ private _registerCleanup(): void {
509
+ if (this._cleanupRegistered) return
510
+ this._cleanupRegistered = true
511
+
512
+ const onExit = () => { this.killAll() }
513
+ const onSignal = (signal: NodeJS.Signals) => {
514
+ this.killAll()
515
+ process.removeListener(signal, onSignal as any)
516
+ process.kill(process.pid, signal)
517
+ }
518
+
519
+ const onSigInt = () => onSignal('SIGINT')
520
+ const onSigTerm = () => onSignal('SIGTERM')
521
+
522
+ process.on('exit', onExit)
523
+ process.on('SIGINT', onSigInt)
524
+ process.on('SIGTERM', onSigTerm)
525
+
526
+ this._cleanupHandlers = [
527
+ () => process.removeListener('exit', onExit),
528
+ () => process.removeListener('SIGINT', onSigInt),
529
+ () => process.removeListener('SIGTERM', onSigTerm),
530
+ ]
531
+ }
532
+
533
+ private _removeCleanup(): void {
534
+ for (const remove of this._cleanupHandlers) {
535
+ remove()
536
+ }
537
+ this._cleanupHandlers = []
538
+ this._cleanupRegistered = false
539
+ }
540
+ }
541
+
542
+ export default features.register('processManager', ProcessManager)