@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,912 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
3
|
+
import { Feature, features } from '../feature.js'
|
|
4
|
+
|
|
5
|
+
export const DockerContainerSchema = z.object({
|
|
6
|
+
/** Container ID */
|
|
7
|
+
id: z.string().describe('Container ID'),
|
|
8
|
+
/** Container name */
|
|
9
|
+
name: z.string().describe('Container name'),
|
|
10
|
+
/** Image used to create the container */
|
|
11
|
+
image: z.string().describe('Image used to create the container'),
|
|
12
|
+
/** Current container status (e.g. running, exited) */
|
|
13
|
+
status: z.string().describe('Current container status (e.g. running, exited)'),
|
|
14
|
+
/** Published port mappings */
|
|
15
|
+
ports: z.array(z.string()).describe('Published port mappings'),
|
|
16
|
+
/** Container creation timestamp */
|
|
17
|
+
created: z.string().describe('Container creation timestamp'),
|
|
18
|
+
})
|
|
19
|
+
export type DockerContainer = z.infer<typeof DockerContainerSchema>
|
|
20
|
+
|
|
21
|
+
export const DockerImageSchema = z.object({
|
|
22
|
+
/** Image ID */
|
|
23
|
+
id: z.string().describe('Image ID'),
|
|
24
|
+
/** Image repository name */
|
|
25
|
+
repository: z.string().describe('Image repository name'),
|
|
26
|
+
/** Image tag */
|
|
27
|
+
tag: z.string().describe('Image tag'),
|
|
28
|
+
/** Image size */
|
|
29
|
+
size: z.string().describe('Image size'),
|
|
30
|
+
/** Image creation timestamp */
|
|
31
|
+
created: z.string().describe('Image creation timestamp'),
|
|
32
|
+
})
|
|
33
|
+
export type DockerImage = z.infer<typeof DockerImageSchema>
|
|
34
|
+
|
|
35
|
+
export const DockerStateSchema = FeatureStateSchema.extend({
|
|
36
|
+
/** List of known Docker containers */
|
|
37
|
+
containers: z.array(DockerContainerSchema).describe('List of known Docker containers'),
|
|
38
|
+
/** List of known Docker images */
|
|
39
|
+
images: z.array(DockerImageSchema).describe('List of known Docker images'),
|
|
40
|
+
/** Whether Docker CLI is available on this system */
|
|
41
|
+
isDockerAvailable: z.boolean().describe('Whether Docker CLI is available on this system'),
|
|
42
|
+
/** Last error message from a Docker operation */
|
|
43
|
+
lastError: z.string().optional().describe('Last error message from a Docker operation'),
|
|
44
|
+
})
|
|
45
|
+
export type DockerState = z.infer<typeof DockerStateSchema>
|
|
46
|
+
|
|
47
|
+
export const DockerOptionsSchema = FeatureOptionsSchema.extend({
|
|
48
|
+
/** Path to docker executable */
|
|
49
|
+
dockerPath: z.string().optional().describe('Path to docker executable'),
|
|
50
|
+
/** Command timeout in ms */
|
|
51
|
+
timeout: z.number().optional().describe('Command timeout in milliseconds'),
|
|
52
|
+
/** Auto refresh containers/images on operations */
|
|
53
|
+
autoRefresh: z.boolean().optional().describe('Auto refresh containers/images after operations'),
|
|
54
|
+
})
|
|
55
|
+
export type DockerOptions = z.infer<typeof DockerOptionsSchema>
|
|
56
|
+
|
|
57
|
+
/** Shell-like interface for executing commands against a Docker container */
|
|
58
|
+
export interface DockerShell {
|
|
59
|
+
/** The ID of the container being targeted */
|
|
60
|
+
readonly containerId: string
|
|
61
|
+
/** The result of the most recently executed command, or null if no command has been run */
|
|
62
|
+
readonly last: { stdout: string; stderr: string; exitCode: number } | null
|
|
63
|
+
/** Execute a command string in the container via sh -c */
|
|
64
|
+
run(command: string): Promise<{ stdout: string; stderr: string; exitCode: number }>
|
|
65
|
+
/** Destroy the shell container (only needed when volumes created a new container) */
|
|
66
|
+
destroy(): Promise<void>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Docker CLI interface feature for managing containers, images, and executing Docker commands.
|
|
71
|
+
*
|
|
72
|
+
* Provides comprehensive Docker operations including:
|
|
73
|
+
* - Container management (list, start, stop, create, remove)
|
|
74
|
+
* - Image management (list, pull, build, remove)
|
|
75
|
+
* - Command execution inside containers
|
|
76
|
+
* - Docker system information
|
|
77
|
+
*
|
|
78
|
+
* @extends Feature
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const docker = container.feature('docker', { enable: true })
|
|
82
|
+
* await docker.checkDockerAvailability()
|
|
83
|
+
* const containers = await docker.listContainers({ all: true })
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export class Docker extends Feature<DockerState, DockerOptions> {
|
|
87
|
+
static override shortcut = 'features.docker' as const
|
|
88
|
+
static override stateSchema = DockerStateSchema
|
|
89
|
+
static override optionsSchema = DockerOptionsSchema
|
|
90
|
+
|
|
91
|
+
override get initialState(): DockerState {
|
|
92
|
+
return {
|
|
93
|
+
...super.initialState,
|
|
94
|
+
containers: [],
|
|
95
|
+
images: [],
|
|
96
|
+
isDockerAvailable: false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the proc feature for executing shell commands
|
|
102
|
+
*/
|
|
103
|
+
get proc() {
|
|
104
|
+
return this.container.feature('proc')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if Docker is available and working.
|
|
109
|
+
*
|
|
110
|
+
* @returns Promise resolving to true if Docker CLI is accessible, false otherwise
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* const available = await docker.checkDockerAvailability()
|
|
114
|
+
* if (!available) console.log('Docker is not installed or not running')
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
async checkDockerAvailability(): Promise<boolean> {
|
|
118
|
+
try {
|
|
119
|
+
const dockerPath = this.options.dockerPath || 'docker'
|
|
120
|
+
const result = await this.proc.spawnAndCapture(dockerPath, ['--version'])
|
|
121
|
+
|
|
122
|
+
if (result.exitCode === 0) {
|
|
123
|
+
this.setState({ isDockerAvailable: true, lastError: undefined })
|
|
124
|
+
return true
|
|
125
|
+
} else {
|
|
126
|
+
this.setState({ isDockerAvailable: false, lastError: 'Docker command failed' })
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.setState({
|
|
131
|
+
isDockerAvailable: false,
|
|
132
|
+
lastError: error instanceof Error ? error.message : 'Unknown error'
|
|
133
|
+
})
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Execute a Docker command and return the result.
|
|
140
|
+
*
|
|
141
|
+
* @param args - Array of CLI arguments to pass to the docker binary
|
|
142
|
+
* @returns Promise resolving to an object with stdout, stderr, and exitCode
|
|
143
|
+
* @throws Error if Docker is not available
|
|
144
|
+
*/
|
|
145
|
+
private async executeDockerCommand(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
146
|
+
if (!this.state.current.isDockerAvailable) {
|
|
147
|
+
const available = await this.checkDockerAvailability()
|
|
148
|
+
if (!available) {
|
|
149
|
+
throw new Error('Docker is not available')
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const dockerPath = this.options.dockerPath || 'docker'
|
|
155
|
+
const result = await this.proc.spawnAndCapture(dockerPath, args)
|
|
156
|
+
|
|
157
|
+
if (result.exitCode !== 0) {
|
|
158
|
+
this.setState({ lastError: result.stderr })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
164
|
+
this.setState({ lastError: message })
|
|
165
|
+
throw error
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* List all containers (running and stopped).
|
|
171
|
+
*
|
|
172
|
+
* @param options - Listing options
|
|
173
|
+
* @param options.all - Include stopped containers (default: false)
|
|
174
|
+
* @returns Promise resolving to an array of DockerContainer objects
|
|
175
|
+
* @throws Error if the docker ps command fails
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* const running = await docker.listContainers()
|
|
179
|
+
* const all = await docker.listContainers({ all: true })
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
async listContainers(options: { all?: boolean } = {}): Promise<DockerContainer[]> {
|
|
183
|
+
const args = ['ps', '--format', 'json']
|
|
184
|
+
if (options.all) {
|
|
185
|
+
args.push('--all')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const result = await this.executeDockerCommand(args)
|
|
189
|
+
|
|
190
|
+
if (result.exitCode === 0) {
|
|
191
|
+
const containers: DockerContainer[] = []
|
|
192
|
+
const lines = result.stdout.trim().split('\n').filter(line => line.trim())
|
|
193
|
+
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
try {
|
|
196
|
+
const containerData = JSON.parse(line)
|
|
197
|
+
containers.push({
|
|
198
|
+
id: containerData.ID,
|
|
199
|
+
name: containerData.Names,
|
|
200
|
+
image: containerData.Image,
|
|
201
|
+
status: containerData.Status,
|
|
202
|
+
ports: containerData.Ports ? containerData.Ports.split(',').map((p: string) => p.trim()) : [],
|
|
203
|
+
created: containerData.CreatedAt
|
|
204
|
+
})
|
|
205
|
+
} catch (e) {
|
|
206
|
+
// Skip invalid JSON lines
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (this.options.autoRefresh) {
|
|
211
|
+
this.setState({ containers })
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return containers
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
throw new Error(`Failed to list containers: ${result.stderr}`)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* List all images available locally.
|
|
222
|
+
*
|
|
223
|
+
* @returns Promise resolving to an array of DockerImage objects
|
|
224
|
+
* @throws Error if the docker images command fails
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* const images = await docker.listImages()
|
|
228
|
+
* console.log(images.map(i => `${i.repository}:${i.tag}`))
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
async listImages(): Promise<DockerImage[]> {
|
|
232
|
+
const result = await this.executeDockerCommand(['images', '--format', 'json'])
|
|
233
|
+
|
|
234
|
+
if (result.exitCode === 0) {
|
|
235
|
+
const images: DockerImage[] = []
|
|
236
|
+
const lines = result.stdout.trim().split('\n').filter(line => line.trim())
|
|
237
|
+
|
|
238
|
+
for (const line of lines) {
|
|
239
|
+
try {
|
|
240
|
+
const imageData = JSON.parse(line)
|
|
241
|
+
images.push({
|
|
242
|
+
id: imageData.ID,
|
|
243
|
+
repository: imageData.Repository,
|
|
244
|
+
tag: imageData.Tag,
|
|
245
|
+
size: imageData.Size,
|
|
246
|
+
created: imageData.CreatedAt
|
|
247
|
+
})
|
|
248
|
+
} catch (e) {
|
|
249
|
+
// Skip invalid JSON lines
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (this.options.autoRefresh) {
|
|
254
|
+
this.setState({ images })
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return images
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
throw new Error(`Failed to list images: ${result.stderr}`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Start a stopped container.
|
|
265
|
+
*
|
|
266
|
+
* @param containerIdOrName - Container ID or name to start
|
|
267
|
+
* @returns Promise that resolves when the container is started
|
|
268
|
+
* @throws Error if the container cannot be started
|
|
269
|
+
* @example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* await docker.startContainer('my-app')
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
async startContainer(containerIdOrName: string): Promise<void> {
|
|
275
|
+
const result = await this.executeDockerCommand(['start', containerIdOrName])
|
|
276
|
+
|
|
277
|
+
if (result.exitCode !== 0) {
|
|
278
|
+
throw new Error(`Failed to start container: ${result.stderr}`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (this.options.autoRefresh) {
|
|
282
|
+
await this.listContainers({ all: true })
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Stop a running container.
|
|
288
|
+
*
|
|
289
|
+
* @param containerIdOrName - Container ID or name to stop
|
|
290
|
+
* @param timeout - Seconds to wait before killing the container
|
|
291
|
+
* @returns Promise that resolves when the container is stopped
|
|
292
|
+
* @throws Error if the container cannot be stopped
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* await docker.stopContainer('my-app')
|
|
296
|
+
* await docker.stopContainer('my-app', 30) // wait up to 30s
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
async stopContainer(containerIdOrName: string, timeout?: number): Promise<void> {
|
|
300
|
+
const args = ['stop']
|
|
301
|
+
if (timeout) {
|
|
302
|
+
args.push('--time', timeout.toString())
|
|
303
|
+
}
|
|
304
|
+
args.push(containerIdOrName)
|
|
305
|
+
|
|
306
|
+
const result = await this.executeDockerCommand(args)
|
|
307
|
+
|
|
308
|
+
if (result.exitCode !== 0) {
|
|
309
|
+
throw new Error(`Failed to stop container: ${result.stderr}`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (this.options.autoRefresh) {
|
|
313
|
+
await this.listContainers({ all: true })
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Remove a container.
|
|
319
|
+
*
|
|
320
|
+
* @param containerIdOrName - Container ID or name to remove
|
|
321
|
+
* @param options - Removal options
|
|
322
|
+
* @param options.force - Force removal of a running container
|
|
323
|
+
* @returns Promise that resolves when the container is removed
|
|
324
|
+
* @throws Error if the container cannot be removed
|
|
325
|
+
* @example
|
|
326
|
+
* ```typescript
|
|
327
|
+
* await docker.removeContainer('old-container')
|
|
328
|
+
* await docker.removeContainer('stubborn-container', { force: true })
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
async removeContainer(containerIdOrName: string, options: { force?: boolean } = {}): Promise<void> {
|
|
332
|
+
const args = ['rm']
|
|
333
|
+
if (options.force) {
|
|
334
|
+
args.push('--force')
|
|
335
|
+
}
|
|
336
|
+
args.push(containerIdOrName)
|
|
337
|
+
|
|
338
|
+
const result = await this.executeDockerCommand(args)
|
|
339
|
+
|
|
340
|
+
if (result.exitCode !== 0) {
|
|
341
|
+
throw new Error(`Failed to remove container: ${result.stderr}`)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (this.options.autoRefresh) {
|
|
345
|
+
await this.listContainers({ all: true })
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Create and run a new container from the given image.
|
|
351
|
+
*
|
|
352
|
+
* @param image - Docker image to run (e.g. 'nginx:latest')
|
|
353
|
+
* @param options - Container run options
|
|
354
|
+
* @param options.name - Assign a name to the container
|
|
355
|
+
* @param options.ports - Port mappings in 'host:container' format (e.g. ['8080:80'])
|
|
356
|
+
* @param options.volumes - Volume mounts in 'host:container' format (e.g. ['./data:/app/data'])
|
|
357
|
+
* @param options.environment - Environment variables as key-value pairs
|
|
358
|
+
* @param options.detach - Run the container in the background
|
|
359
|
+
* @param options.interactive - Keep STDIN open
|
|
360
|
+
* @param options.tty - Allocate a pseudo-TTY
|
|
361
|
+
* @param options.command - Command and arguments to run inside the container
|
|
362
|
+
* @param options.workdir - Working directory inside the container
|
|
363
|
+
* @param options.user - Username or UID to run as
|
|
364
|
+
* @param options.entrypoint - Override the default entrypoint
|
|
365
|
+
* @param options.network - Connect the container to a network
|
|
366
|
+
* @param options.restart - Restart policy (e.g. 'always', 'on-failure')
|
|
367
|
+
* @returns Promise resolving to the container ID
|
|
368
|
+
* @throws Error if the container cannot be started
|
|
369
|
+
* @example
|
|
370
|
+
* ```typescript
|
|
371
|
+
* const containerId = await docker.runContainer('nginx:latest', {
|
|
372
|
+
* name: 'web',
|
|
373
|
+
* ports: ['8080:80'],
|
|
374
|
+
* detach: true,
|
|
375
|
+
* environment: { NODE_ENV: 'production' }
|
|
376
|
+
* })
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
async runContainer(
|
|
380
|
+
image: string,
|
|
381
|
+
options: {
|
|
382
|
+
/** Assign a name to the container */
|
|
383
|
+
name?: string
|
|
384
|
+
/** Port mappings in 'host:container' format */
|
|
385
|
+
ports?: string[]
|
|
386
|
+
/** Volume mounts in 'host:container' format */
|
|
387
|
+
volumes?: string[]
|
|
388
|
+
/** Environment variables as key-value pairs */
|
|
389
|
+
environment?: Record<string, string>
|
|
390
|
+
/** Run the container in the background */
|
|
391
|
+
detach?: boolean
|
|
392
|
+
/** Keep STDIN open */
|
|
393
|
+
interactive?: boolean
|
|
394
|
+
/** Allocate a pseudo-TTY */
|
|
395
|
+
tty?: boolean
|
|
396
|
+
/** Command and arguments to run inside the container */
|
|
397
|
+
command?: string[]
|
|
398
|
+
/** Working directory inside the container */
|
|
399
|
+
workdir?: string
|
|
400
|
+
/** Username or UID to run as */
|
|
401
|
+
user?: string
|
|
402
|
+
/** Override the default entrypoint */
|
|
403
|
+
entrypoint?: string
|
|
404
|
+
/** Connect the container to a network */
|
|
405
|
+
network?: string
|
|
406
|
+
/** Restart policy (e.g. 'always', 'on-failure') */
|
|
407
|
+
restart?: string
|
|
408
|
+
} = {}
|
|
409
|
+
): Promise<string> {
|
|
410
|
+
const args = ['run']
|
|
411
|
+
|
|
412
|
+
if (options.detach) args.push('--detach')
|
|
413
|
+
if (options.interactive) args.push('--interactive')
|
|
414
|
+
if (options.tty) args.push('--tty')
|
|
415
|
+
if (options.name) args.push('--name', options.name)
|
|
416
|
+
if (options.workdir) args.push('--workdir', options.workdir)
|
|
417
|
+
if (options.user) args.push('--user', options.user)
|
|
418
|
+
if (options.entrypoint) args.push('--entrypoint', options.entrypoint)
|
|
419
|
+
if (options.network) args.push('--network', options.network)
|
|
420
|
+
if (options.restart) args.push('--restart', options.restart)
|
|
421
|
+
|
|
422
|
+
if (options.ports) {
|
|
423
|
+
for (const port of options.ports) {
|
|
424
|
+
args.push('--publish', port)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (options.volumes) {
|
|
429
|
+
for (const volume of options.volumes) {
|
|
430
|
+
args.push('--volume', volume)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (options.environment) {
|
|
435
|
+
for (const [key, value] of Object.entries(options.environment)) {
|
|
436
|
+
args.push('--env', `${key}=${value}`)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
args.push(image)
|
|
441
|
+
|
|
442
|
+
if (options.command) {
|
|
443
|
+
args.push(...options.command)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const result = await this.executeDockerCommand(args)
|
|
447
|
+
|
|
448
|
+
if (result.exitCode !== 0) {
|
|
449
|
+
throw new Error(`Failed to run container: ${result.stderr}`)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (this.options.autoRefresh) {
|
|
453
|
+
await this.listContainers({ all: true })
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return result.stdout.trim()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Execute a command inside a running container.
|
|
461
|
+
*
|
|
462
|
+
* When volumes are specified, uses `docker run --rm` with the container's image
|
|
463
|
+
* instead of `docker exec`, since exec does not support volume mounts.
|
|
464
|
+
*
|
|
465
|
+
* @param containerIdOrName - Container ID or name to execute in
|
|
466
|
+
* @param command - Command and arguments array (e.g. ['ls', '-la'])
|
|
467
|
+
* @param options - Execution options
|
|
468
|
+
* @param options.interactive - Keep STDIN open
|
|
469
|
+
* @param options.tty - Allocate a pseudo-TTY
|
|
470
|
+
* @param options.user - Username or UID to run as
|
|
471
|
+
* @param options.workdir - Working directory inside the container
|
|
472
|
+
* @param options.detach - Run the command in the background
|
|
473
|
+
* @param options.environment - Environment variables as key-value pairs
|
|
474
|
+
* @param options.volumes - Volume mounts; triggers a docker run --rm fallback
|
|
475
|
+
* @returns Promise resolving to an object with stdout, stderr, and exitCode
|
|
476
|
+
* @example
|
|
477
|
+
* ```typescript
|
|
478
|
+
* const result = await docker.execCommand('my-app', ['ls', '-la', '/app'])
|
|
479
|
+
* console.log(result.stdout)
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
async execCommand(
|
|
483
|
+
containerIdOrName: string,
|
|
484
|
+
command: string[],
|
|
485
|
+
options: {
|
|
486
|
+
/** Keep STDIN open */
|
|
487
|
+
interactive?: boolean
|
|
488
|
+
/** Allocate a pseudo-TTY */
|
|
489
|
+
tty?: boolean
|
|
490
|
+
/** Username or UID to run as */
|
|
491
|
+
user?: string
|
|
492
|
+
/** Working directory inside the container */
|
|
493
|
+
workdir?: string
|
|
494
|
+
/** Run the command in the background */
|
|
495
|
+
detach?: boolean
|
|
496
|
+
/** Environment variables as key-value pairs */
|
|
497
|
+
environment?: Record<string, string>
|
|
498
|
+
/** Volume mounts; triggers a docker run --rm fallback */
|
|
499
|
+
volumes?: string[]
|
|
500
|
+
} = {}
|
|
501
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
502
|
+
// docker exec does not support volume mounts; fall back to docker run --rm
|
|
503
|
+
if (options.volumes?.length) {
|
|
504
|
+
const image = await this.getContainerImage(containerIdOrName)
|
|
505
|
+
|
|
506
|
+
const args = ['run', '--rm']
|
|
507
|
+
for (const vol of options.volumes) { args.push('--volume', vol) }
|
|
508
|
+
if (options.interactive) args.push('--interactive')
|
|
509
|
+
if (options.tty) args.push('--tty')
|
|
510
|
+
if (options.user) args.push('--user', options.user)
|
|
511
|
+
if (options.workdir) args.push('--workdir', options.workdir)
|
|
512
|
+
if (options.environment) {
|
|
513
|
+
for (const [key, value] of Object.entries(options.environment)) {
|
|
514
|
+
args.push('--env', `${key}=${value}`)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
args.push(image, ...command)
|
|
518
|
+
return this.executeDockerCommand(args)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const args = ['exec']
|
|
522
|
+
|
|
523
|
+
if (options.interactive) args.push('--interactive')
|
|
524
|
+
if (options.tty) args.push('--tty')
|
|
525
|
+
if (options.user) args.push('--user', options.user)
|
|
526
|
+
if (options.workdir) args.push('--workdir', options.workdir)
|
|
527
|
+
if (options.detach) args.push('--detach')
|
|
528
|
+
if (options.environment) {
|
|
529
|
+
for (const [key, value] of Object.entries(options.environment)) {
|
|
530
|
+
args.push('--env', `${key}=${value}`)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
args.push(containerIdOrName, ...command)
|
|
535
|
+
|
|
536
|
+
const result = await this.executeDockerCommand(args)
|
|
537
|
+
return result
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Look up the image name for a running container via docker inspect.
|
|
542
|
+
*
|
|
543
|
+
* @param containerIdOrName - Container ID or name to inspect
|
|
544
|
+
* @returns Promise resolving to the image name string
|
|
545
|
+
* @throws Error if the container cannot be inspected
|
|
546
|
+
*/
|
|
547
|
+
private async getContainerImage(containerIdOrName: string): Promise<string> {
|
|
548
|
+
const result = await this.executeDockerCommand([
|
|
549
|
+
'inspect', '--format', '{{.Config.Image}}', containerIdOrName
|
|
550
|
+
])
|
|
551
|
+
if (result.exitCode !== 0) {
|
|
552
|
+
throw new Error(`Failed to inspect container ${containerIdOrName}: ${result.stderr}`)
|
|
553
|
+
}
|
|
554
|
+
return result.stdout.trim()
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Create a shell-like wrapper for executing multiple commands against a container.
|
|
559
|
+
*
|
|
560
|
+
* When volume mounts are specified, a new long-running container is created from
|
|
561
|
+
* the same image with the mounts applied (since docker exec does not support volumes).
|
|
562
|
+
* Call `destroy()` when finished to clean up the helper container.
|
|
563
|
+
*
|
|
564
|
+
* Returns an object with:
|
|
565
|
+
* - `run(command)` — execute a shell command string via `sh -c`
|
|
566
|
+
* - `last` — getter for the most recent command result
|
|
567
|
+
* - `destroy()` — stop the helper container (no-op when no volumes were needed)
|
|
568
|
+
*/
|
|
569
|
+
async createShell(
|
|
570
|
+
containerIdOrName: string,
|
|
571
|
+
options: {
|
|
572
|
+
volumes?: string[]
|
|
573
|
+
workdir?: string
|
|
574
|
+
user?: string
|
|
575
|
+
environment?: Record<string, string>
|
|
576
|
+
} = {}
|
|
577
|
+
): Promise<DockerShell> {
|
|
578
|
+
const docker = this
|
|
579
|
+
let targetContainer = containerIdOrName
|
|
580
|
+
let createdContainer: string | null = null
|
|
581
|
+
|
|
582
|
+
if (options.volumes?.length) {
|
|
583
|
+
const image = await this.getContainerImage(containerIdOrName)
|
|
584
|
+
|
|
585
|
+
const runArgs = ['run', '-d', '--rm']
|
|
586
|
+
for (const vol of options.volumes) { runArgs.push('--volume', vol) }
|
|
587
|
+
if (options.workdir) runArgs.push('--workdir', options.workdir)
|
|
588
|
+
if (options.user) runArgs.push('--user', options.user)
|
|
589
|
+
if (options.environment) {
|
|
590
|
+
for (const [key, value] of Object.entries(options.environment)) {
|
|
591
|
+
runArgs.push('--env', `${key}=${value}`)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
runArgs.push(image, 'sleep', 'infinity')
|
|
595
|
+
|
|
596
|
+
const runResult = await this.executeDockerCommand(runArgs)
|
|
597
|
+
if (runResult.exitCode !== 0) {
|
|
598
|
+
throw new Error(`Failed to create shell container: ${runResult.stderr}`)
|
|
599
|
+
}
|
|
600
|
+
targetContainer = runResult.stdout.trim()
|
|
601
|
+
createdContainer = targetContainer
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Only pass workdir/user to exec when we didn't bake them into the container
|
|
605
|
+
const execOpts: { workdir?: string; user?: string } = {}
|
|
606
|
+
if (!createdContainer) {
|
|
607
|
+
if (options.workdir) execOpts.workdir = options.workdir
|
|
608
|
+
if (options.user) execOpts.user = options.user
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
let _last: { stdout: string; stderr: string; exitCode: number } | null = null
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
get containerId() { return targetContainer },
|
|
615
|
+
get last() { return _last },
|
|
616
|
+
run: async (command: string) => {
|
|
617
|
+
_last = await docker.execCommand(targetContainer, ['sh', '-c', command], execOpts)
|
|
618
|
+
return _last
|
|
619
|
+
},
|
|
620
|
+
destroy: async () => {
|
|
621
|
+
if (createdContainer) {
|
|
622
|
+
await docker.executeDockerCommand(['stop', createdContainer])
|
|
623
|
+
createdContainer = null
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Pull an image from a registry.
|
|
631
|
+
*
|
|
632
|
+
* @param image - Full image reference (e.g. 'nginx:latest', 'ghcr.io/org/repo:tag')
|
|
633
|
+
* @returns Promise that resolves when the pull is complete
|
|
634
|
+
* @throws Error if the pull fails
|
|
635
|
+
* @example
|
|
636
|
+
* ```typescript
|
|
637
|
+
* await docker.pullImage('node:20-alpine')
|
|
638
|
+
* ```
|
|
639
|
+
*/
|
|
640
|
+
async pullImage(image: string): Promise<void> {
|
|
641
|
+
const result = await this.executeDockerCommand(['pull', image])
|
|
642
|
+
|
|
643
|
+
if (result.exitCode !== 0) {
|
|
644
|
+
throw new Error(`Failed to pull image: ${result.stderr}`)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (this.options.autoRefresh) {
|
|
648
|
+
await this.listImages()
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Remove an image from the local store.
|
|
654
|
+
*
|
|
655
|
+
* @param imageIdOrName - Image ID, repository, or repository:tag to remove
|
|
656
|
+
* @param options - Removal options
|
|
657
|
+
* @param options.force - Force removal even if the image is in use
|
|
658
|
+
* @returns Promise that resolves when the image is removed
|
|
659
|
+
* @throws Error if the image cannot be removed
|
|
660
|
+
* @example
|
|
661
|
+
* ```typescript
|
|
662
|
+
* await docker.removeImage('nginx:latest')
|
|
663
|
+
* await docker.removeImage('old-image', { force: true })
|
|
664
|
+
* ```
|
|
665
|
+
*/
|
|
666
|
+
async removeImage(imageIdOrName: string, options: { force?: boolean } = {}): Promise<void> {
|
|
667
|
+
const args = ['rmi']
|
|
668
|
+
if (options.force) {
|
|
669
|
+
args.push('--force')
|
|
670
|
+
}
|
|
671
|
+
args.push(imageIdOrName)
|
|
672
|
+
|
|
673
|
+
const result = await this.executeDockerCommand(args)
|
|
674
|
+
|
|
675
|
+
if (result.exitCode !== 0) {
|
|
676
|
+
throw new Error(`Failed to remove image: ${result.stderr}`)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (this.options.autoRefresh) {
|
|
680
|
+
await this.listImages()
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Build an image from a Dockerfile.
|
|
686
|
+
*
|
|
687
|
+
* @param contextPath - Path to the build context directory
|
|
688
|
+
* @param options - Build options
|
|
689
|
+
* @param options.tag - Tag the resulting image (e.g. 'my-app:latest')
|
|
690
|
+
* @param options.dockerfile - Path to an alternate Dockerfile
|
|
691
|
+
* @param options.buildArgs - Build-time variables as key-value pairs
|
|
692
|
+
* @param options.target - Target build stage in a multi-stage Dockerfile
|
|
693
|
+
* @param options.nocache - Do not use cache when building the image
|
|
694
|
+
* @returns Promise that resolves when the build is complete
|
|
695
|
+
* @throws Error if the build fails
|
|
696
|
+
* @example
|
|
697
|
+
* ```typescript
|
|
698
|
+
* await docker.buildImage('./project', {
|
|
699
|
+
* tag: 'my-app:latest',
|
|
700
|
+
* buildArgs: { NODE_ENV: 'production' }
|
|
701
|
+
* })
|
|
702
|
+
* ```
|
|
703
|
+
*/
|
|
704
|
+
async buildImage(
|
|
705
|
+
contextPath: string,
|
|
706
|
+
options: {
|
|
707
|
+
/** Tag the resulting image (e.g. 'my-app:latest') */
|
|
708
|
+
tag?: string
|
|
709
|
+
/** Path to an alternate Dockerfile */
|
|
710
|
+
dockerfile?: string
|
|
711
|
+
/** Build-time variables as key-value pairs */
|
|
712
|
+
buildArgs?: Record<string, string>
|
|
713
|
+
/** Target build stage in a multi-stage Dockerfile */
|
|
714
|
+
target?: string
|
|
715
|
+
/** Do not use cache when building the image */
|
|
716
|
+
nocache?: boolean
|
|
717
|
+
} = {}
|
|
718
|
+
): Promise<void> {
|
|
719
|
+
const args = ['build']
|
|
720
|
+
|
|
721
|
+
if (options.tag) args.push('--tag', options.tag)
|
|
722
|
+
if (options.dockerfile) args.push('--file', options.dockerfile)
|
|
723
|
+
if (options.target) args.push('--target', options.target)
|
|
724
|
+
if (options.nocache) args.push('--no-cache')
|
|
725
|
+
|
|
726
|
+
if (options.buildArgs) {
|
|
727
|
+
for (const [key, value] of Object.entries(options.buildArgs)) {
|
|
728
|
+
args.push('--build-arg', `${key}=${value}`)
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
args.push(contextPath)
|
|
733
|
+
|
|
734
|
+
const result = await this.executeDockerCommand(args)
|
|
735
|
+
|
|
736
|
+
if (result.exitCode !== 0) {
|
|
737
|
+
throw new Error(`Failed to build image: ${result.stderr}`)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (this.options.autoRefresh) {
|
|
741
|
+
await this.listImages()
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Get container logs.
|
|
747
|
+
*
|
|
748
|
+
* @param containerIdOrName - Container ID or name to fetch logs from
|
|
749
|
+
* @param options - Log retrieval options
|
|
750
|
+
* @param options.follow - Follow log output (stream)
|
|
751
|
+
* @param options.tail - Number of lines to show from the end of the logs
|
|
752
|
+
* @param options.since - Show logs since a timestamp or relative time (e.g. '10m', '2024-01-01T00:00:00')
|
|
753
|
+
* @param options.timestamps - Prepend a timestamp to each log line
|
|
754
|
+
* @returns Promise resolving to the log output string
|
|
755
|
+
* @throws Error if logs cannot be retrieved
|
|
756
|
+
* @example
|
|
757
|
+
* ```typescript
|
|
758
|
+
* const logs = await docker.getLogs('my-app', { tail: 100, timestamps: true })
|
|
759
|
+
* console.log(logs)
|
|
760
|
+
* ```
|
|
761
|
+
*/
|
|
762
|
+
async getLogs(
|
|
763
|
+
containerIdOrName: string,
|
|
764
|
+
options: {
|
|
765
|
+
/** Follow log output (stream) */
|
|
766
|
+
follow?: boolean
|
|
767
|
+
/** Number of lines to show from the end of the logs */
|
|
768
|
+
tail?: number
|
|
769
|
+
/** Show logs since a timestamp or relative time */
|
|
770
|
+
since?: string
|
|
771
|
+
/** Prepend a timestamp to each log line */
|
|
772
|
+
timestamps?: boolean
|
|
773
|
+
} = {}
|
|
774
|
+
): Promise<string> {
|
|
775
|
+
const args = ['logs']
|
|
776
|
+
|
|
777
|
+
if (options.follow) args.push('--follow')
|
|
778
|
+
if (options.tail) args.push('--tail', options.tail.toString())
|
|
779
|
+
if (options.since) args.push('--since', options.since)
|
|
780
|
+
if (options.timestamps) args.push('--timestamps')
|
|
781
|
+
|
|
782
|
+
args.push(containerIdOrName)
|
|
783
|
+
|
|
784
|
+
const result = await this.executeDockerCommand(args)
|
|
785
|
+
|
|
786
|
+
if (result.exitCode !== 0) {
|
|
787
|
+
throw new Error(`Failed to get logs: ${result.stderr}`)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return result.stdout
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Get Docker system information (engine version, storage driver, OS, etc.).
|
|
795
|
+
*
|
|
796
|
+
* @returns Promise resolving to the parsed JSON system info object
|
|
797
|
+
* @throws Error if the system info command fails
|
|
798
|
+
* @example
|
|
799
|
+
* ```typescript
|
|
800
|
+
* const info = await docker.getSystemInfo()
|
|
801
|
+
* console.log(info.ServerVersion)
|
|
802
|
+
* ```
|
|
803
|
+
*/
|
|
804
|
+
async getSystemInfo(): Promise<any> {
|
|
805
|
+
const result = await this.executeDockerCommand(['system', 'info', '--format', 'json'])
|
|
806
|
+
|
|
807
|
+
if (result.exitCode !== 0) {
|
|
808
|
+
throw new Error(`Failed to get system info: ${result.stderr}`)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return JSON.parse(result.stdout)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Prune unused Docker resources.
|
|
816
|
+
*
|
|
817
|
+
* When no specific resource type is selected, falls back to `docker system prune`.
|
|
818
|
+
*
|
|
819
|
+
* @param options - Pruning options
|
|
820
|
+
* @param options.containers - Prune stopped containers
|
|
821
|
+
* @param options.images - Prune dangling images
|
|
822
|
+
* @param options.volumes - Prune unused volumes
|
|
823
|
+
* @param options.networks - Prune unused networks
|
|
824
|
+
* @param options.all - Prune all resource types (containers, images, volumes, networks)
|
|
825
|
+
* @param options.force - Skip confirmation prompts for image pruning
|
|
826
|
+
* @returns Promise that resolves when pruning is complete
|
|
827
|
+
* @example
|
|
828
|
+
* ```typescript
|
|
829
|
+
* await docker.prune({ all: true })
|
|
830
|
+
* await docker.prune({ containers: true, images: true })
|
|
831
|
+
* ```
|
|
832
|
+
*/
|
|
833
|
+
async prune(options: {
|
|
834
|
+
/** Prune stopped containers */
|
|
835
|
+
containers?: boolean
|
|
836
|
+
/** Prune dangling images */
|
|
837
|
+
images?: boolean
|
|
838
|
+
/** Prune unused volumes */
|
|
839
|
+
volumes?: boolean
|
|
840
|
+
/** Prune unused networks */
|
|
841
|
+
networks?: boolean
|
|
842
|
+
/** Prune all resource types */
|
|
843
|
+
all?: boolean
|
|
844
|
+
/** Skip confirmation prompts for image pruning */
|
|
845
|
+
force?: boolean
|
|
846
|
+
} = {}): Promise<void> {
|
|
847
|
+
const commands = []
|
|
848
|
+
|
|
849
|
+
if (options.containers || options.all) {
|
|
850
|
+
commands.push(['container', 'prune', '--force'])
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (options.images || options.all) {
|
|
854
|
+
const args = ['image', 'prune']
|
|
855
|
+
if (options.force) args.push('--force')
|
|
856
|
+
commands.push(args)
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (options.volumes || options.all) {
|
|
860
|
+
commands.push(['volume', 'prune', '--force'])
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (options.networks || options.all) {
|
|
864
|
+
commands.push(['network', 'prune', '--force'])
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (commands.length === 0) {
|
|
868
|
+
commands.push(['system', 'prune', '--force'])
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
for (const command of commands) {
|
|
872
|
+
await this.executeDockerCommand(command)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (this.options.autoRefresh) {
|
|
876
|
+
await Promise.all([
|
|
877
|
+
this.listContainers({ all: true }),
|
|
878
|
+
this.listImages()
|
|
879
|
+
])
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Initialize the Docker feature by checking availability and optionally refreshing state.
|
|
885
|
+
*
|
|
886
|
+
* @param options - Enable options passed to the base Feature
|
|
887
|
+
* @returns Promise resolving to this Docker instance
|
|
888
|
+
*/
|
|
889
|
+
override async enable(options: any = {}): Promise<this> {
|
|
890
|
+
await super.enable(options)
|
|
891
|
+
|
|
892
|
+
// Check Docker availability on enable
|
|
893
|
+
await this.checkDockerAvailability()
|
|
894
|
+
|
|
895
|
+
// Initial refresh of containers and images if Docker is available
|
|
896
|
+
if (this.state.current.isDockerAvailable && this.options.autoRefresh) {
|
|
897
|
+
try {
|
|
898
|
+
await Promise.all([
|
|
899
|
+
this.listContainers({ all: true }),
|
|
900
|
+
this.listImages()
|
|
901
|
+
])
|
|
902
|
+
} catch (error) {
|
|
903
|
+
// Don't fail enable if we can't list initially
|
|
904
|
+
this.setState({ lastError: error instanceof Error ? error.message : 'Unknown error' })
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return this
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export default features.register('docker', Docker)
|