@soederpop/luca 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/CLAUDE.md +71 -0
  2. package/README.md +78 -0
  3. package/bun.lock +2928 -0
  4. package/bunfig.toml +3 -0
  5. package/commands/audit-docs.ts +740 -0
  6. package/commands/build-scaffolds.ts +154 -0
  7. package/commands/generate-api-docs.ts +114 -0
  8. package/commands/update-introspection.ts +67 -0
  9. package/docs/CLI.md +335 -0
  10. package/docs/README.md +88 -0
  11. package/docs/TABLE-OF-CONTENTS.md +157 -0
  12. package/docs/apis/clients/elevenlabs.md +84 -0
  13. package/docs/apis/clients/graph.md +56 -0
  14. package/docs/apis/clients/openai.md +69 -0
  15. package/docs/apis/clients/rest.md +41 -0
  16. package/docs/apis/clients/websocket.md +107 -0
  17. package/docs/apis/features/agi/assistant.md +471 -0
  18. package/docs/apis/features/agi/assistants-manager.md +154 -0
  19. package/docs/apis/features/agi/claude-code.md +602 -0
  20. package/docs/apis/features/agi/conversation-history.md +352 -0
  21. package/docs/apis/features/agi/conversation.md +333 -0
  22. package/docs/apis/features/agi/docs-reader.md +121 -0
  23. package/docs/apis/features/agi/openai-codex.md +318 -0
  24. package/docs/apis/features/agi/openapi.md +138 -0
  25. package/docs/apis/features/agi/semantic-search.md +387 -0
  26. package/docs/apis/features/agi/skills-library.md +216 -0
  27. package/docs/apis/features/node/container-link.md +133 -0
  28. package/docs/apis/features/node/content-db.md +313 -0
  29. package/docs/apis/features/node/disk-cache.md +379 -0
  30. package/docs/apis/features/node/dns.md +651 -0
  31. package/docs/apis/features/node/docker.md +705 -0
  32. package/docs/apis/features/node/downloader.md +81 -0
  33. package/docs/apis/features/node/esbuild.md +59 -0
  34. package/docs/apis/features/node/file-manager.md +182 -0
  35. package/docs/apis/features/node/fs.md +581 -0
  36. package/docs/apis/features/node/git.md +330 -0
  37. package/docs/apis/features/node/google-auth.md +174 -0
  38. package/docs/apis/features/node/google-calendar.md +187 -0
  39. package/docs/apis/features/node/google-docs.md +151 -0
  40. package/docs/apis/features/node/google-drive.md +225 -0
  41. package/docs/apis/features/node/google-sheets.md +179 -0
  42. package/docs/apis/features/node/grep.md +290 -0
  43. package/docs/apis/features/node/helpers.md +135 -0
  44. package/docs/apis/features/node/ink.md +334 -0
  45. package/docs/apis/features/node/ipc-socket.md +260 -0
  46. package/docs/apis/features/node/json-tree.md +86 -0
  47. package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
  48. package/docs/apis/features/node/networking.md +281 -0
  49. package/docs/apis/features/node/nlp.md +133 -0
  50. package/docs/apis/features/node/opener.md +97 -0
  51. package/docs/apis/features/node/os.md +118 -0
  52. package/docs/apis/features/node/package-finder.md +402 -0
  53. package/docs/apis/features/node/postgres.md +212 -0
  54. package/docs/apis/features/node/proc.md +430 -0
  55. package/docs/apis/features/node/process-manager.md +210 -0
  56. package/docs/apis/features/node/python.md +278 -0
  57. package/docs/apis/features/node/repl.md +88 -0
  58. package/docs/apis/features/node/runpod.md +673 -0
  59. package/docs/apis/features/node/secure-shell.md +169 -0
  60. package/docs/apis/features/node/semantic-search.md +401 -0
  61. package/docs/apis/features/node/sqlite.md +211 -0
  62. package/docs/apis/features/node/telegram.md +254 -0
  63. package/docs/apis/features/node/tts.md +118 -0
  64. package/docs/apis/features/node/ui.md +703 -0
  65. package/docs/apis/features/node/vault.md +64 -0
  66. package/docs/apis/features/node/vm.md +84 -0
  67. package/docs/apis/features/node/window-manager.md +337 -0
  68. package/docs/apis/features/node/yaml-tree.md +85 -0
  69. package/docs/apis/features/node/yaml.md +176 -0
  70. package/docs/apis/features/web/asset-loader.md +47 -0
  71. package/docs/apis/features/web/container-link.md +133 -0
  72. package/docs/apis/features/web/esbuild.md +59 -0
  73. package/docs/apis/features/web/helpers.md +135 -0
  74. package/docs/apis/features/web/network.md +30 -0
  75. package/docs/apis/features/web/speech.md +55 -0
  76. package/docs/apis/features/web/vault.md +64 -0
  77. package/docs/apis/features/web/vm.md +84 -0
  78. package/docs/apis/features/web/voice.md +67 -0
  79. package/docs/apis/servers/express.md +127 -0
  80. package/docs/apis/servers/mcp.md +213 -0
  81. package/docs/apis/servers/websocket.md +99 -0
  82. package/docs/documentation-audit.md +134 -0
  83. package/docs/examples/content-db.md +77 -0
  84. package/docs/examples/disk-cache.md +83 -0
  85. package/docs/examples/docker.md +101 -0
  86. package/docs/examples/downloader.md +70 -0
  87. package/docs/examples/esbuild.md +80 -0
  88. package/docs/examples/file-manager.md +82 -0
  89. package/docs/examples/fs.md +83 -0
  90. package/docs/examples/git.md +85 -0
  91. package/docs/examples/google-auth.md +88 -0
  92. package/docs/examples/google-calendar.md +94 -0
  93. package/docs/examples/google-docs.md +82 -0
  94. package/docs/examples/google-drive.md +96 -0
  95. package/docs/examples/google-sheets.md +95 -0
  96. package/docs/examples/grep.md +85 -0
  97. package/docs/examples/ink-blocks.md +75 -0
  98. package/docs/examples/ink-renderer.md +41 -0
  99. package/docs/examples/ink.md +103 -0
  100. package/docs/examples/ipc-socket.md +103 -0
  101. package/docs/examples/json-tree.md +91 -0
  102. package/docs/examples/launcher-app-command-listener.md +120 -0
  103. package/docs/examples/networking.md +58 -0
  104. package/docs/examples/nlp.md +91 -0
  105. package/docs/examples/opener.md +78 -0
  106. package/docs/examples/os.md +72 -0
  107. package/docs/examples/package-finder.md +89 -0
  108. package/docs/examples/port-exposer.md +89 -0
  109. package/docs/examples/postgres.md +91 -0
  110. package/docs/examples/proc.md +81 -0
  111. package/docs/examples/process-manager.md +79 -0
  112. package/docs/examples/python.md +91 -0
  113. package/docs/examples/repl.md +93 -0
  114. package/docs/examples/runpod.md +119 -0
  115. package/docs/examples/secure-shell.md +92 -0
  116. package/docs/examples/sqlite.md +86 -0
  117. package/docs/examples/telegram.md +77 -0
  118. package/docs/examples/tts.md +86 -0
  119. package/docs/examples/ui.md +80 -0
  120. package/docs/examples/vault.md +70 -0
  121. package/docs/examples/vm.md +86 -0
  122. package/docs/examples/window-manager.md +125 -0
  123. package/docs/examples/yaml-tree.md +93 -0
  124. package/docs/examples/yaml.md +104 -0
  125. package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
  126. package/docs/ideas/container-use-api.md +9 -0
  127. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  128. package/docs/ideas/feature-stacks.md +22 -0
  129. package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
  130. package/docs/ideas/mcp-design.md +9 -0
  131. package/docs/ideas/web-container-debugging-feature.md +13 -0
  132. package/docs/introspection-audit.md +49 -0
  133. package/docs/introspection.md +154 -0
  134. package/docs/mcp/readme.md +162 -0
  135. package/docs/models.ts +38 -0
  136. package/docs/philosophy.md +85 -0
  137. package/docs/principles.md +7 -0
  138. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  139. package/docs/prompts/mcp-test-easy-command.md +27 -0
  140. package/docs/reports/assistant-bugs.md +38 -0
  141. package/docs/reports/attach-pattern-usage.md +18 -0
  142. package/docs/reports/code-audit-results.md +391 -0
  143. package/docs/reports/introspection-audit-tasks.md +378 -0
  144. package/docs/reports/luca-mcp-improvements.md +128 -0
  145. package/docs/scaffolds/client.md +140 -0
  146. package/docs/scaffolds/command.md +106 -0
  147. package/docs/scaffolds/endpoint.md +176 -0
  148. package/docs/scaffolds/feature.md +148 -0
  149. package/docs/scaffolds/server.md +187 -0
  150. package/docs/tasks/web-container-helper-discovery.md +71 -0
  151. package/docs/todos.md +1 -0
  152. package/docs/tutorials/01-getting-started.md +106 -0
  153. package/docs/tutorials/02-container.md +210 -0
  154. package/docs/tutorials/03-scripts.md +194 -0
  155. package/docs/tutorials/04-features-overview.md +196 -0
  156. package/docs/tutorials/05-state-and-events.md +171 -0
  157. package/docs/tutorials/06-servers.md +157 -0
  158. package/docs/tutorials/07-endpoints.md +198 -0
  159. package/docs/tutorials/08-commands.md +171 -0
  160. package/docs/tutorials/09-clients.md +162 -0
  161. package/docs/tutorials/10-creating-features.md +198 -0
  162. package/docs/tutorials/11-contentbase.md +191 -0
  163. package/docs/tutorials/12-assistants.md +215 -0
  164. package/docs/tutorials/13-introspection.md +147 -0
  165. package/docs/tutorials/14-type-system.md +174 -0
  166. package/docs/tutorials/15-project-patterns.md +222 -0
  167. package/docs/tutorials/16-google-features.md +534 -0
  168. package/docs/tutorials/17-tui-blocks.md +530 -0
  169. package/docs/tutorials/18-semantic-search.md +334 -0
  170. package/index.ts +1 -0
  171. package/luca.console.ts +9 -0
  172. package/main.py +6 -0
  173. package/package.json +154 -0
  174. package/pyproject.toml +7 -0
  175. package/scripts/animations/chrome-glitch.ts +55 -0
  176. package/scripts/animations/index.ts +16 -0
  177. package/scripts/animations/neon-pulse.ts +64 -0
  178. package/scripts/animations/types.ts +6 -0
  179. package/scripts/build-web.ts +28 -0
  180. package/scripts/examples/ask-luca-expert.ts +42 -0
  181. package/scripts/examples/assistant-questions.ts +12 -0
  182. package/scripts/examples/excalidraw-expert.ts +75 -0
  183. package/scripts/examples/expert-chat.ts +0 -0
  184. package/scripts/examples/file-manager.ts +14 -0
  185. package/scripts/examples/ideas.ts +12 -0
  186. package/scripts/examples/interactive-chat.ts +20 -0
  187. package/scripts/examples/openai-tool-calls.ts +113 -0
  188. package/scripts/examples/opening-a-web-browser.ts +5 -0
  189. package/scripts/examples/telegram-bot.ts +79 -0
  190. package/scripts/examples/telegram-ink-ui.ts +302 -0
  191. package/scripts/examples/using-assistant-with-mcp.ts +560 -0
  192. package/scripts/examples/using-claude-code.ts +10 -0
  193. package/scripts/examples/using-contentdb.ts +35 -0
  194. package/scripts/examples/using-conversations.ts +35 -0
  195. package/scripts/examples/using-disk-cache.ts +10 -0
  196. package/scripts/examples/using-docker-shell.ts +75 -0
  197. package/scripts/examples/using-elevenlabs.ts +25 -0
  198. package/scripts/examples/using-google-calendar.ts +57 -0
  199. package/scripts/examples/using-google-docs.ts +74 -0
  200. package/scripts/examples/using-google-drive.ts +74 -0
  201. package/scripts/examples/using-google-sheets.ts +89 -0
  202. package/scripts/examples/using-nlp.ts +55 -0
  203. package/scripts/examples/using-ollama.ts +10 -0
  204. package/scripts/examples/using-openai-codex.ts +23 -0
  205. package/scripts/examples/using-postgres.ts +55 -0
  206. package/scripts/examples/using-runpod.ts +32 -0
  207. package/scripts/examples/using-tts.ts +40 -0
  208. package/scripts/examples/vm-loading-esm-modules.ts +16 -0
  209. package/scripts/scaffold.ts +391 -0
  210. package/scripts/scratch.ts +15 -0
  211. package/scripts/test-command-listener.ts +123 -0
  212. package/scripts/test-window-manager-lifecycle.ts +86 -0
  213. package/scripts/test-window-manager.ts +43 -0
  214. package/scripts/update-introspection-data.ts +58 -0
  215. package/src/agi/README.md +14 -0
  216. package/src/agi/container.server.ts +114 -0
  217. package/src/agi/endpoints/ask.ts +60 -0
  218. package/src/agi/endpoints/conversations/[id].ts +45 -0
  219. package/src/agi/endpoints/conversations.ts +31 -0
  220. package/src/agi/endpoints/experts.ts +37 -0
  221. package/src/agi/features/assistant.ts +767 -0
  222. package/src/agi/features/assistants-manager.ts +260 -0
  223. package/src/agi/features/claude-code.ts +1111 -0
  224. package/src/agi/features/conversation-history.ts +497 -0
  225. package/src/agi/features/conversation.ts +799 -0
  226. package/src/agi/features/openai-codex.ts +631 -0
  227. package/src/agi/features/openapi.ts +438 -0
  228. package/src/agi/features/skills-library.ts +425 -0
  229. package/src/agi/index.ts +6 -0
  230. package/src/agi/lib/token-counter.ts +122 -0
  231. package/src/browser.ts +25 -0
  232. package/src/bus.ts +100 -0
  233. package/src/cli/cli.ts +70 -0
  234. package/src/client.ts +461 -0
  235. package/src/clients/civitai/index.ts +541 -0
  236. package/src/clients/client-template.ts +41 -0
  237. package/src/clients/comfyui/index.ts +597 -0
  238. package/src/clients/elevenlabs/index.ts +291 -0
  239. package/src/clients/openai/index.ts +451 -0
  240. package/src/clients/supabase/index.ts +366 -0
  241. package/src/command.ts +164 -0
  242. package/src/commands/chat.ts +182 -0
  243. package/src/commands/console.ts +192 -0
  244. package/src/commands/describe.ts +433 -0
  245. package/src/commands/eval.ts +116 -0
  246. package/src/commands/help.ts +214 -0
  247. package/src/commands/index.ts +14 -0
  248. package/src/commands/mcp.ts +64 -0
  249. package/src/commands/prompt.ts +807 -0
  250. package/src/commands/run.ts +257 -0
  251. package/src/commands/sandbox-mcp.ts +439 -0
  252. package/src/commands/scaffold.ts +79 -0
  253. package/src/commands/serve.ts +172 -0
  254. package/src/container.ts +781 -0
  255. package/src/endpoint.ts +340 -0
  256. package/src/feature.ts +75 -0
  257. package/src/hash-object.ts +97 -0
  258. package/src/helper.ts +543 -0
  259. package/src/introspection/generated.agi.ts +23388 -0
  260. package/src/introspection/generated.node.ts +18899 -0
  261. package/src/introspection/generated.web.ts +2021 -0
  262. package/src/introspection/index.ts +256 -0
  263. package/src/introspection/scan.ts +912 -0
  264. package/src/node/container.ts +354 -0
  265. package/src/node/feature.ts +13 -0
  266. package/src/node/features/container-link.ts +558 -0
  267. package/src/node/features/content-db.ts +475 -0
  268. package/src/node/features/disk-cache.ts +382 -0
  269. package/src/node/features/dns.ts +655 -0
  270. package/src/node/features/docker.ts +912 -0
  271. package/src/node/features/downloader.ts +92 -0
  272. package/src/node/features/esbuild.ts +68 -0
  273. package/src/node/features/file-manager.ts +357 -0
  274. package/src/node/features/fs.ts +534 -0
  275. package/src/node/features/git.ts +492 -0
  276. package/src/node/features/google-auth.ts +502 -0
  277. package/src/node/features/google-calendar.ts +300 -0
  278. package/src/node/features/google-docs.ts +404 -0
  279. package/src/node/features/google-drive.ts +339 -0
  280. package/src/node/features/google-sheets.ts +279 -0
  281. package/src/node/features/grep.ts +406 -0
  282. package/src/node/features/helpers.ts +374 -0
  283. package/src/node/features/ink.ts +490 -0
  284. package/src/node/features/ipc-socket.ts +459 -0
  285. package/src/node/features/json-tree.ts +188 -0
  286. package/src/node/features/launcher-app-command-listener.ts +388 -0
  287. package/src/node/features/networking.ts +925 -0
  288. package/src/node/features/nlp.ts +211 -0
  289. package/src/node/features/opener.ts +166 -0
  290. package/src/node/features/os.ts +157 -0
  291. package/src/node/features/package-finder.ts +539 -0
  292. package/src/node/features/port-exposer.ts +342 -0
  293. package/src/node/features/postgres.ts +273 -0
  294. package/src/node/features/proc.ts +502 -0
  295. package/src/node/features/process-manager.ts +542 -0
  296. package/src/node/features/python.ts +444 -0
  297. package/src/node/features/repl.ts +194 -0
  298. package/src/node/features/runpod.ts +802 -0
  299. package/src/node/features/secure-shell.ts +248 -0
  300. package/src/node/features/semantic-search.ts +924 -0
  301. package/src/node/features/sqlite.ts +289 -0
  302. package/src/node/features/telegram.ts +342 -0
  303. package/src/node/features/tts.ts +184 -0
  304. package/src/node/features/ui.ts +857 -0
  305. package/src/node/features/vault.ts +164 -0
  306. package/src/node/features/vm.ts +312 -0
  307. package/src/node/features/window-manager.ts +804 -0
  308. package/src/node/features/yaml-tree.ts +149 -0
  309. package/src/node/features/yaml.ts +132 -0
  310. package/src/node.ts +70 -0
  311. package/src/react/index.ts +175 -0
  312. package/src/registry.ts +199 -0
  313. package/src/scaffolds/generated.ts +1613 -0
  314. package/src/scaffolds/template.ts +37 -0
  315. package/src/schemas/base.ts +255 -0
  316. package/src/server.ts +135 -0
  317. package/src/servers/express.ts +209 -0
  318. package/src/servers/mcp.ts +805 -0
  319. package/src/servers/socket.ts +120 -0
  320. package/src/state.ts +101 -0
  321. package/src/web/clients/socket.ts +82 -0
  322. package/src/web/container.ts +74 -0
  323. package/src/web/extension.ts +30 -0
  324. package/src/web/feature.ts +12 -0
  325. package/src/web/features/asset-loader.ts +64 -0
  326. package/src/web/features/container-link.ts +385 -0
  327. package/src/web/features/esbuild.ts +79 -0
  328. package/src/web/features/helpers.ts +267 -0
  329. package/src/web/features/network.ts +61 -0
  330. package/src/web/features/speech.ts +87 -0
  331. package/src/web/features/vault.ts +189 -0
  332. package/src/web/features/vm.ts +78 -0
  333. package/src/web/features/voice-recognition.ts +129 -0
  334. package/src/web/shims/isomorphic-vm.ts +149 -0
  335. package/test/bus.test.ts +134 -0
  336. package/test/clients-servers.test.ts +216 -0
  337. package/test/container-link.test.ts +274 -0
  338. package/test/features.test.ts +160 -0
  339. package/test/integration.test.ts +787 -0
  340. package/test/node-container.test.ts +121 -0
  341. package/test/rate-limit.test.ts +272 -0
  342. package/test/semantic-search.test.ts +550 -0
  343. package/test/state.test.ts +121 -0
  344. package/test-integration/assistant.test.ts +138 -0
  345. package/test-integration/assistants-manager.test.ts +123 -0
  346. package/test-integration/claude-code.test.ts +98 -0
  347. package/test-integration/conversation-history.test.ts +205 -0
  348. package/test-integration/conversation.test.ts +137 -0
  349. package/test-integration/elevenlabs.test.ts +55 -0
  350. package/test-integration/google-services.test.ts +80 -0
  351. package/test-integration/helpers.ts +89 -0
  352. package/test-integration/openai-codex.test.ts +93 -0
  353. package/test-integration/runpod.test.ts +58 -0
  354. package/test-integration/server-endpoints.test.ts +97 -0
  355. package/test-integration/skills-library.test.ts +157 -0
  356. package/test-integration/telegram.test.ts +46 -0
  357. package/tsconfig.json +58 -0
  358. package/uv.lock +8 -0
@@ -0,0 +1,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)