@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.
- package/CLAUDE.md +71 -0
- package/README.md +78 -0
- package/bun.lock +2928 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-scaffolds.ts +154 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/update-introspection.ts +67 -0
- package/docs/CLI.md +335 -0
- package/docs/README.md +88 -0
- package/docs/TABLE-OF-CONTENTS.md +157 -0
- package/docs/apis/clients/elevenlabs.md +84 -0
- package/docs/apis/clients/graph.md +56 -0
- package/docs/apis/clients/openai.md +69 -0
- package/docs/apis/clients/rest.md +41 -0
- package/docs/apis/clients/websocket.md +107 -0
- package/docs/apis/features/agi/assistant.md +471 -0
- package/docs/apis/features/agi/assistants-manager.md +154 -0
- package/docs/apis/features/agi/claude-code.md +602 -0
- package/docs/apis/features/agi/conversation-history.md +352 -0
- package/docs/apis/features/agi/conversation.md +333 -0
- package/docs/apis/features/agi/docs-reader.md +121 -0
- package/docs/apis/features/agi/openai-codex.md +318 -0
- package/docs/apis/features/agi/openapi.md +138 -0
- package/docs/apis/features/agi/semantic-search.md +387 -0
- package/docs/apis/features/agi/skills-library.md +216 -0
- package/docs/apis/features/node/container-link.md +133 -0
- package/docs/apis/features/node/content-db.md +313 -0
- package/docs/apis/features/node/disk-cache.md +379 -0
- package/docs/apis/features/node/dns.md +651 -0
- package/docs/apis/features/node/docker.md +705 -0
- package/docs/apis/features/node/downloader.md +81 -0
- package/docs/apis/features/node/esbuild.md +59 -0
- package/docs/apis/features/node/file-manager.md +182 -0
- package/docs/apis/features/node/fs.md +581 -0
- package/docs/apis/features/node/git.md +330 -0
- package/docs/apis/features/node/google-auth.md +174 -0
- package/docs/apis/features/node/google-calendar.md +187 -0
- package/docs/apis/features/node/google-docs.md +151 -0
- package/docs/apis/features/node/google-drive.md +225 -0
- package/docs/apis/features/node/google-sheets.md +179 -0
- package/docs/apis/features/node/grep.md +290 -0
- package/docs/apis/features/node/helpers.md +135 -0
- package/docs/apis/features/node/ink.md +334 -0
- package/docs/apis/features/node/ipc-socket.md +260 -0
- package/docs/apis/features/node/json-tree.md +86 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
- package/docs/apis/features/node/networking.md +281 -0
- package/docs/apis/features/node/nlp.md +133 -0
- package/docs/apis/features/node/opener.md +97 -0
- package/docs/apis/features/node/os.md +118 -0
- package/docs/apis/features/node/package-finder.md +402 -0
- package/docs/apis/features/node/postgres.md +212 -0
- package/docs/apis/features/node/proc.md +430 -0
- package/docs/apis/features/node/process-manager.md +210 -0
- package/docs/apis/features/node/python.md +278 -0
- package/docs/apis/features/node/repl.md +88 -0
- package/docs/apis/features/node/runpod.md +673 -0
- package/docs/apis/features/node/secure-shell.md +169 -0
- package/docs/apis/features/node/semantic-search.md +401 -0
- package/docs/apis/features/node/sqlite.md +211 -0
- package/docs/apis/features/node/telegram.md +254 -0
- package/docs/apis/features/node/tts.md +118 -0
- package/docs/apis/features/node/ui.md +703 -0
- package/docs/apis/features/node/vault.md +64 -0
- package/docs/apis/features/node/vm.md +84 -0
- package/docs/apis/features/node/window-manager.md +337 -0
- package/docs/apis/features/node/yaml-tree.md +85 -0
- package/docs/apis/features/node/yaml.md +176 -0
- package/docs/apis/features/web/asset-loader.md +47 -0
- package/docs/apis/features/web/container-link.md +133 -0
- package/docs/apis/features/web/esbuild.md +59 -0
- package/docs/apis/features/web/helpers.md +135 -0
- package/docs/apis/features/web/network.md +30 -0
- package/docs/apis/features/web/speech.md +55 -0
- package/docs/apis/features/web/vault.md +64 -0
- package/docs/apis/features/web/vm.md +84 -0
- package/docs/apis/features/web/voice.md +67 -0
- package/docs/apis/servers/express.md +127 -0
- package/docs/apis/servers/mcp.md +213 -0
- package/docs/apis/servers/websocket.md +99 -0
- package/docs/documentation-audit.md +134 -0
- package/docs/examples/content-db.md +77 -0
- package/docs/examples/disk-cache.md +83 -0
- package/docs/examples/docker.md +101 -0
- package/docs/examples/downloader.md +70 -0
- package/docs/examples/esbuild.md +80 -0
- package/docs/examples/file-manager.md +82 -0
- package/docs/examples/fs.md +83 -0
- package/docs/examples/git.md +85 -0
- package/docs/examples/google-auth.md +88 -0
- package/docs/examples/google-calendar.md +94 -0
- package/docs/examples/google-docs.md +82 -0
- package/docs/examples/google-drive.md +96 -0
- package/docs/examples/google-sheets.md +95 -0
- package/docs/examples/grep.md +85 -0
- package/docs/examples/ink-blocks.md +75 -0
- package/docs/examples/ink-renderer.md +41 -0
- package/docs/examples/ink.md +103 -0
- package/docs/examples/ipc-socket.md +103 -0
- package/docs/examples/json-tree.md +91 -0
- package/docs/examples/launcher-app-command-listener.md +120 -0
- package/docs/examples/networking.md +58 -0
- package/docs/examples/nlp.md +91 -0
- package/docs/examples/opener.md +78 -0
- package/docs/examples/os.md +72 -0
- package/docs/examples/package-finder.md +89 -0
- package/docs/examples/port-exposer.md +89 -0
- package/docs/examples/postgres.md +91 -0
- package/docs/examples/proc.md +81 -0
- package/docs/examples/process-manager.md +79 -0
- package/docs/examples/python.md +91 -0
- package/docs/examples/repl.md +93 -0
- package/docs/examples/runpod.md +119 -0
- package/docs/examples/secure-shell.md +92 -0
- package/docs/examples/sqlite.md +86 -0
- package/docs/examples/telegram.md +77 -0
- package/docs/examples/tts.md +86 -0
- package/docs/examples/ui.md +80 -0
- package/docs/examples/vault.md +70 -0
- package/docs/examples/vm.md +86 -0
- package/docs/examples/window-manager.md +125 -0
- package/docs/examples/yaml-tree.md +93 -0
- package/docs/examples/yaml.md +104 -0
- package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
- package/docs/ideas/container-use-api.md +9 -0
- package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
- package/docs/ideas/feature-stacks.md +22 -0
- package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
- package/docs/ideas/mcp-design.md +9 -0
- package/docs/ideas/web-container-debugging-feature.md +13 -0
- package/docs/introspection-audit.md +49 -0
- package/docs/introspection.md +154 -0
- package/docs/mcp/readme.md +162 -0
- package/docs/models.ts +38 -0
- package/docs/philosophy.md +85 -0
- package/docs/principles.md +7 -0
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
- package/docs/prompts/mcp-test-easy-command.md +27 -0
- package/docs/reports/assistant-bugs.md +38 -0
- package/docs/reports/attach-pattern-usage.md +18 -0
- package/docs/reports/code-audit-results.md +391 -0
- package/docs/reports/introspection-audit-tasks.md +378 -0
- package/docs/reports/luca-mcp-improvements.md +128 -0
- package/docs/scaffolds/client.md +140 -0
- package/docs/scaffolds/command.md +106 -0
- package/docs/scaffolds/endpoint.md +176 -0
- package/docs/scaffolds/feature.md +148 -0
- package/docs/scaffolds/server.md +187 -0
- package/docs/tasks/web-container-helper-discovery.md +71 -0
- package/docs/todos.md +1 -0
- package/docs/tutorials/01-getting-started.md +106 -0
- package/docs/tutorials/02-container.md +210 -0
- package/docs/tutorials/03-scripts.md +194 -0
- package/docs/tutorials/04-features-overview.md +196 -0
- package/docs/tutorials/05-state-and-events.md +171 -0
- package/docs/tutorials/06-servers.md +157 -0
- package/docs/tutorials/07-endpoints.md +198 -0
- package/docs/tutorials/08-commands.md +171 -0
- package/docs/tutorials/09-clients.md +162 -0
- package/docs/tutorials/10-creating-features.md +198 -0
- package/docs/tutorials/11-contentbase.md +191 -0
- package/docs/tutorials/12-assistants.md +215 -0
- package/docs/tutorials/13-introspection.md +147 -0
- package/docs/tutorials/14-type-system.md +174 -0
- package/docs/tutorials/15-project-patterns.md +222 -0
- package/docs/tutorials/16-google-features.md +534 -0
- package/docs/tutorials/17-tui-blocks.md +530 -0
- package/docs/tutorials/18-semantic-search.md +334 -0
- package/index.ts +1 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +154 -0
- package/pyproject.toml +7 -0
- package/scripts/animations/chrome-glitch.ts +55 -0
- package/scripts/animations/index.ts +16 -0
- package/scripts/animations/neon-pulse.ts +64 -0
- package/scripts/animations/types.ts +6 -0
- package/scripts/build-web.ts +28 -0
- package/scripts/examples/ask-luca-expert.ts +42 -0
- package/scripts/examples/assistant-questions.ts +12 -0
- package/scripts/examples/excalidraw-expert.ts +75 -0
- package/scripts/examples/expert-chat.ts +0 -0
- package/scripts/examples/file-manager.ts +14 -0
- package/scripts/examples/ideas.ts +12 -0
- package/scripts/examples/interactive-chat.ts +20 -0
- package/scripts/examples/openai-tool-calls.ts +113 -0
- package/scripts/examples/opening-a-web-browser.ts +5 -0
- package/scripts/examples/telegram-bot.ts +79 -0
- package/scripts/examples/telegram-ink-ui.ts +302 -0
- package/scripts/examples/using-assistant-with-mcp.ts +560 -0
- package/scripts/examples/using-claude-code.ts +10 -0
- package/scripts/examples/using-contentdb.ts +35 -0
- package/scripts/examples/using-conversations.ts +35 -0
- package/scripts/examples/using-disk-cache.ts +10 -0
- package/scripts/examples/using-docker-shell.ts +75 -0
- package/scripts/examples/using-elevenlabs.ts +25 -0
- package/scripts/examples/using-google-calendar.ts +57 -0
- package/scripts/examples/using-google-docs.ts +74 -0
- package/scripts/examples/using-google-drive.ts +74 -0
- package/scripts/examples/using-google-sheets.ts +89 -0
- package/scripts/examples/using-nlp.ts +55 -0
- package/scripts/examples/using-ollama.ts +10 -0
- package/scripts/examples/using-openai-codex.ts +23 -0
- package/scripts/examples/using-postgres.ts +55 -0
- package/scripts/examples/using-runpod.ts +32 -0
- package/scripts/examples/using-tts.ts +40 -0
- package/scripts/examples/vm-loading-esm-modules.ts +16 -0
- package/scripts/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/test-command-listener.ts +123 -0
- package/scripts/test-window-manager-lifecycle.ts +86 -0
- package/scripts/test-window-manager.ts +43 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +114 -0
- package/src/agi/endpoints/ask.ts +60 -0
- package/src/agi/endpoints/conversations/[id].ts +45 -0
- package/src/agi/endpoints/conversations.ts +31 -0
- package/src/agi/endpoints/experts.ts +37 -0
- package/src/agi/features/assistant.ts +767 -0
- package/src/agi/features/assistants-manager.ts +260 -0
- package/src/agi/features/claude-code.ts +1111 -0
- package/src/agi/features/conversation-history.ts +497 -0
- package/src/agi/features/conversation.ts +799 -0
- package/src/agi/features/openai-codex.ts +631 -0
- package/src/agi/features/openapi.ts +438 -0
- package/src/agi/features/skills-library.ts +425 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/token-counter.ts +122 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +100 -0
- package/src/cli/cli.ts +70 -0
- package/src/client.ts +461 -0
- package/src/clients/civitai/index.ts +541 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +597 -0
- package/src/clients/elevenlabs/index.ts +291 -0
- package/src/clients/openai/index.ts +451 -0
- package/src/clients/supabase/index.ts +366 -0
- package/src/command.ts +164 -0
- package/src/commands/chat.ts +182 -0
- package/src/commands/console.ts +192 -0
- package/src/commands/describe.ts +433 -0
- package/src/commands/eval.ts +116 -0
- package/src/commands/help.ts +214 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +807 -0
- package/src/commands/run.ts +257 -0
- package/src/commands/sandbox-mcp.ts +439 -0
- package/src/commands/scaffold.ts +79 -0
- package/src/commands/serve.ts +172 -0
- package/src/container.ts +781 -0
- package/src/endpoint.ts +340 -0
- package/src/feature.ts +75 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +543 -0
- package/src/introspection/generated.agi.ts +23388 -0
- package/src/introspection/generated.node.ts +18899 -0
- package/src/introspection/generated.web.ts +2021 -0
- package/src/introspection/index.ts +256 -0
- package/src/introspection/scan.ts +912 -0
- package/src/node/container.ts +354 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +558 -0
- package/src/node/features/content-db.ts +475 -0
- package/src/node/features/disk-cache.ts +382 -0
- package/src/node/features/dns.ts +655 -0
- package/src/node/features/docker.ts +912 -0
- package/src/node/features/downloader.ts +92 -0
- package/src/node/features/esbuild.ts +68 -0
- package/src/node/features/file-manager.ts +357 -0
- package/src/node/features/fs.ts +534 -0
- package/src/node/features/git.ts +492 -0
- package/src/node/features/google-auth.ts +502 -0
- package/src/node/features/google-calendar.ts +300 -0
- package/src/node/features/google-docs.ts +404 -0
- package/src/node/features/google-drive.ts +339 -0
- package/src/node/features/google-sheets.ts +279 -0
- package/src/node/features/grep.ts +406 -0
- package/src/node/features/helpers.ts +374 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +459 -0
- package/src/node/features/json-tree.ts +188 -0
- package/src/node/features/launcher-app-command-listener.ts +388 -0
- package/src/node/features/networking.ts +925 -0
- package/src/node/features/nlp.ts +211 -0
- package/src/node/features/opener.ts +166 -0
- package/src/node/features/os.ts +157 -0
- package/src/node/features/package-finder.ts +539 -0
- package/src/node/features/port-exposer.ts +342 -0
- package/src/node/features/postgres.ts +273 -0
- package/src/node/features/proc.ts +502 -0
- package/src/node/features/process-manager.ts +542 -0
- package/src/node/features/python.ts +444 -0
- package/src/node/features/repl.ts +194 -0
- package/src/node/features/runpod.ts +802 -0
- package/src/node/features/secure-shell.ts +248 -0
- package/src/node/features/semantic-search.ts +924 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +342 -0
- package/src/node/features/tts.ts +184 -0
- package/src/node/features/ui.ts +857 -0
- package/src/node/features/vault.ts +164 -0
- package/src/node/features/vm.ts +312 -0
- package/src/node/features/window-manager.ts +804 -0
- package/src/node/features/yaml-tree.ts +149 -0
- package/src/node/features/yaml.ts +132 -0
- package/src/node.ts +70 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +199 -0
- package/src/scaffolds/generated.ts +1613 -0
- package/src/scaffolds/template.ts +37 -0
- package/src/schemas/base.ts +255 -0
- package/src/server.ts +135 -0
- package/src/servers/express.ts +209 -0
- package/src/servers/mcp.ts +805 -0
- package/src/servers/socket.ts +120 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +82 -0
- package/src/web/container.ts +74 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +64 -0
- package/src/web/features/container-link.ts +385 -0
- package/src/web/features/esbuild.ts +79 -0
- package/src/web/features/helpers.ts +267 -0
- package/src/web/features/network.ts +61 -0
- package/src/web/features/speech.ts +87 -0
- package/src/web/features/vault.ts +189 -0
- package/src/web/features/vm.ts +78 -0
- package/src/web/features/voice-recognition.ts +129 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/test/bus.test.ts +134 -0
- package/test/clients-servers.test.ts +216 -0
- package/test/container-link.test.ts +274 -0
- package/test/features.test.ts +160 -0
- package/test/integration.test.ts +787 -0
- package/test/node-container.test.ts +121 -0
- package/test/rate-limit.test.ts +272 -0
- package/test/semantic-search.test.ts +550 -0
- package/test/state.test.ts +121 -0
- package/test-integration/assistant.test.ts +138 -0
- package/test-integration/assistants-manager.test.ts +123 -0
- package/test-integration/claude-code.test.ts +98 -0
- package/test-integration/conversation-history.test.ts +205 -0
- package/test-integration/conversation.test.ts +137 -0
- package/test-integration/elevenlabs.test.ts +55 -0
- package/test-integration/google-services.test.ts +80 -0
- package/test-integration/helpers.ts +89 -0
- package/test-integration/openai-codex.test.ts +93 -0
- package/test-integration/runpod.test.ts +58 -0
- package/test-integration/server-endpoints.test.ts +97 -0
- package/test-integration/skills-library.test.ts +157 -0
- package/test-integration/telegram.test.ts +46 -0
- package/tsconfig.json +58 -0
- 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)
|