@elyracode/docker 0.5.12

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/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## [0.5.11] - 2026-05-16
4
+
5
+ ## [0.5.10] - 2026-05-15
6
+
7
+ ### Added
8
+ - Initial release
9
+ - `docker_exec` tool: run commands inside containers
10
+ - `docker_logs` tool: tail container logs with grep filtering
11
+ - `docker_status` tool: running containers, resource usage, ports
12
+ - `docker_compose` tool: up/down/restart/build operations
13
+ - `docker_env_check` tool: compare host .env with container environment
14
+ - `/docker` command for container status dashboard
15
+ - Auto-injects compose context into system prompt on session start
16
+ - Skill file for automatic Docker tool selection
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @elyracode/docker
2
+
3
+ Docker-aware development for Elyra. Container exec routing, log tailing, compose operations, environment sync checks, and container health monitoring.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ elyra install npm:@elyracode/docker
9
+ ```
10
+
11
+ ## Tools
12
+
13
+ | Tool | Description |
14
+ |------|-------------|
15
+ | `docker_exec` | Run a command inside a Docker container |
16
+ | `docker_logs` | Read recent logs from a container |
17
+ | `docker_status` | List running containers with resource usage, ports, and health |
18
+ | `docker_compose` | Run docker compose operations (up, down, restart, build) |
19
+ | `docker_env_check` | Compare host .env with container environment variables |
20
+
21
+ ## Commands
22
+
23
+ - `/docker` -- Container status dashboard (running containers, ports, health)
24
+
25
+ ## Usage
26
+
27
+ ### Container execution
28
+
29
+ The agent automatically routes commands through `docker exec` when it detects a Docker environment:
30
+
31
+ ```
32
+ > Run the database migrations
33
+ > Run the test suite
34
+ > Check the PHP version in the app container
35
+ ```
36
+
37
+ Or be explicit:
38
+
39
+ ```
40
+ > Run "php artisan migrate" in the app container
41
+ > Execute "npm run build" in the node container
42
+ ```
43
+
44
+ ### Log reading
45
+
46
+ ```
47
+ > Show me the last 50 lines of the app container logs
48
+ > What errors are in the nginx logs?
49
+ > Tail the database logs and look for slow queries
50
+ ```
51
+
52
+ ### Compose operations
53
+
54
+ ```
55
+ > Rebuild the app container
56
+ > Restart the database service
57
+ > Bring up the entire stack
58
+ > Shut down all containers
59
+ ```
60
+
61
+ ### Environment sync
62
+
63
+ ```
64
+ > Check if my .env matches what the app container sees
65
+ > Compare environment variables between host and container
66
+ ```
67
+
68
+ ### Status
69
+
70
+ ```
71
+ /docker
72
+ ```
73
+
74
+ Shows running containers, CPU/memory usage, port mappings, volume mounts, and health check status.
75
+
76
+ ## How it works
77
+
78
+ On session start, the extension reads `docker-compose.yml` (or `compose.yml`) and injects container context into the system prompt. The agent knows which services exist, which ports are mapped, and which volumes are mounted.
79
+
80
+ When commands need to run inside a container, `docker_exec` handles the routing. The agent doesn't need to be told "use docker exec" -- it knows from context.
81
+
82
+ ## Requirements
83
+
84
+ - Docker CLI installed and running
85
+ - Docker Compose v2 (for compose operations)
@@ -0,0 +1,461 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { ExtensionAPI } from "@elyracode/coding-agent";
5
+ import { Type } from "typebox";
6
+
7
+ // ── Helpers ─────────────────────────────────────────────────────────────────
8
+
9
+ function exec(cmd: string, cwd?: string, timeoutMs = 15000): string {
10
+ try {
11
+ return execSync(cmd, {
12
+ cwd,
13
+ encoding: "utf-8",
14
+ timeout: timeoutMs,
15
+ maxBuffer: 1024 * 1024,
16
+ stdio: ["pipe", "pipe", "pipe"],
17
+ }).trim();
18
+ } catch (error: any) {
19
+ const stderr = error.stderr?.toString().trim() ?? "";
20
+ const stdout = error.stdout?.toString().trim() ?? "";
21
+ return stderr || stdout || error.message;
22
+ }
23
+ }
24
+
25
+ function dockerAvailable(): boolean {
26
+ try {
27
+ execSync("docker info", { stdio: "pipe", timeout: 5000 });
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function findComposeFile(cwd: string): string | undefined {
35
+ const candidates = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
36
+ for (const name of candidates) {
37
+ if (existsSync(join(cwd, name))) return name;
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ function getRunningContainers(cwd?: string): string {
43
+ return exec(
44
+ 'docker ps --format "{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"',
45
+ cwd,
46
+ );
47
+ }
48
+
49
+ // ── Extension ───────────────────────────────────────────────────────────────
50
+
51
+ export default function (elyra: ExtensionAPI): void {
52
+ // Inject Docker context into system prompt on session start
53
+ elyra.on("before_agent_start", async (_event, ctx) => {
54
+ const cwd = ctx.cwd;
55
+ if (!dockerAvailable()) return;
56
+
57
+ const composeFile = findComposeFile(cwd);
58
+ if (!composeFile) return;
59
+
60
+ const composeContent = readFileSync(join(cwd, composeFile), "utf-8");
61
+ const running = getRunningContainers(cwd);
62
+
63
+ const contextBlock =
64
+ `## Docker Environment\n\n` +
65
+ `Compose file: ${composeFile}\n` +
66
+ `\`\`\`yaml\n${composeContent}\n\`\`\`\n\n` +
67
+ (running
68
+ ? `Running containers:\n\`\`\`\n${running}\n\`\`\`\n\n`
69
+ : "No containers currently running.\n\n") +
70
+ `When running commands (migrations, tests, builds), use docker_exec to route them ` +
71
+ `to the appropriate container instead of running on the host.`;
72
+
73
+ const currentPrompt = ctx.getSystemPrompt();
74
+ return {
75
+ systemPrompt: currentPrompt + "\n\n" + contextBlock,
76
+ };
77
+ });
78
+
79
+ // ── Tool: docker_exec ───────────────────────────────────────────────────
80
+
81
+ elyra.registerTool({
82
+ name: "docker_exec",
83
+ label: "Docker Exec",
84
+ description:
85
+ "Run a command inside a Docker container. Use this instead of bash when the project " +
86
+ "uses Docker and the command should run in the container environment (e.g., migrations, " +
87
+ "tests, package installs, artisan commands). Detects the right container from the service name.",
88
+ parameters: Type.Object({
89
+ container: Type.String({
90
+ description:
91
+ "Container or service name (e.g., 'app', 'node', 'web', 'php'). " +
92
+ "Use docker_status to list running containers if unsure.",
93
+ }),
94
+ command: Type.String({
95
+ description: "Command to execute inside the container",
96
+ }),
97
+ workdir: Type.Optional(
98
+ Type.String({
99
+ description: "Working directory inside the container (e.g., /var/www/html)",
100
+ }),
101
+ ),
102
+ user: Type.Optional(
103
+ Type.String({
104
+ description: "User to run as inside the container (e.g., 'www-data', 'node')",
105
+ }),
106
+ ),
107
+ }),
108
+ execute: async (_toolCallId, params) => {
109
+ if (!dockerAvailable()) {
110
+ return {
111
+ content: [{ type: "text", text: "Docker is not running or not installed." }],
112
+ details: {},
113
+ };
114
+ }
115
+
116
+ const parts = ["docker exec"];
117
+ if (params.workdir) parts.push(`-w "${params.workdir}"`);
118
+ if (params.user) parts.push(`-u "${params.user}"`);
119
+ parts.push(params.container);
120
+ parts.push(params.command);
121
+
122
+ const cmd = parts.join(" ");
123
+ const output = exec(cmd, undefined, 60000);
124
+
125
+ return {
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: `$ ${cmd}\n\n${output || "(no output)"}`,
130
+ },
131
+ ],
132
+ details: { container: params.container, command: params.command },
133
+ };
134
+ },
135
+ });
136
+
137
+ // ── Tool: docker_logs ───────────────────────────────────────────────────
138
+
139
+ elyra.registerTool({
140
+ name: "docker_logs",
141
+ label: "Docker Logs",
142
+ description:
143
+ "Read recent logs from a Docker container. Use this to diagnose errors, " +
144
+ "check application output, or monitor service behavior. " +
145
+ "Always check logs before debugging -- the answer is often there.",
146
+ parameters: Type.Object({
147
+ container: Type.String({
148
+ description: "Container or service name",
149
+ }),
150
+ tail: Type.Optional(
151
+ Type.Number({
152
+ description: "Number of lines to show (default: 100)",
153
+ }),
154
+ ),
155
+ since: Type.Optional(
156
+ Type.String({
157
+ description: "Show logs since timestamp or duration (e.g., '10m', '1h', '2024-01-01')",
158
+ }),
159
+ ),
160
+ grep: Type.Optional(
161
+ Type.String({
162
+ description: "Filter logs by pattern (grep)",
163
+ }),
164
+ ),
165
+ }),
166
+ execute: async (_toolCallId, params) => {
167
+ if (!dockerAvailable()) {
168
+ return {
169
+ content: [{ type: "text", text: "Docker is not running or not installed." }],
170
+ details: {},
171
+ };
172
+ }
173
+
174
+ const tail = params.tail ?? 100;
175
+ let cmd = `docker logs --tail ${tail}`;
176
+ if (params.since) cmd += ` --since "${params.since}"`;
177
+ cmd += ` ${params.container}`;
178
+ if (params.grep) cmd += ` 2>&1 | grep -i "${params.grep}"`;
179
+
180
+ const output = exec(cmd, undefined, 15000);
181
+
182
+ return {
183
+ content: [
184
+ {
185
+ type: "text",
186
+ text: output || "(no logs)",
187
+ },
188
+ ],
189
+ details: { container: params.container, lines: tail },
190
+ };
191
+ },
192
+ });
193
+
194
+ // ── Tool: docker_status ─────────────────────────────────────────────────
195
+
196
+ elyra.registerTool({
197
+ name: "docker_status",
198
+ label: "Docker Status",
199
+ description:
200
+ "List running Docker containers with status, ports, and resource usage. " +
201
+ "Use this to check what's running, find container names, and verify health.",
202
+ parameters: Type.Object({}),
203
+ execute: async () => {
204
+ if (!dockerAvailable()) {
205
+ return {
206
+ content: [{ type: "text", text: "Docker is not running or not installed." }],
207
+ details: {},
208
+ };
209
+ }
210
+
211
+ const ps = exec(
212
+ 'docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}\t{{.Size}}"',
213
+ );
214
+
215
+ // Get resource usage
216
+ let stats = "";
217
+ try {
218
+ stats = exec(
219
+ 'docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"',
220
+ undefined,
221
+ 10000,
222
+ );
223
+ } catch {
224
+ // stats might timeout on slow systems
225
+ }
226
+
227
+ const parts = ["# Docker Status\n"];
228
+ parts.push("## Running Containers\n```\n" + (ps || "No containers running") + "\n```\n");
229
+ if (stats) {
230
+ parts.push("## Resource Usage\n```\n" + stats + "\n```\n");
231
+ }
232
+
233
+ return {
234
+ content: [{ type: "text", text: parts.join("\n") }],
235
+ details: {},
236
+ };
237
+ },
238
+ });
239
+
240
+ // ── Tool: docker_compose ────────────────────────────────────────────────
241
+
242
+ elyra.registerTool({
243
+ name: "docker_compose",
244
+ label: "Docker Compose",
245
+ description:
246
+ "Run docker compose operations: up, down, restart, build, pull. " +
247
+ "Use this to manage the container stack -- start services, rebuild after " +
248
+ "Dockerfile changes, restart after config changes.",
249
+ parameters: Type.Object({
250
+ action: Type.String({
251
+ description: "Compose action: up, down, restart, build, pull, ps",
252
+ }),
253
+ service: Type.Optional(
254
+ Type.String({
255
+ description: "Target specific service (e.g., 'app', 'db'). Omit for all services.",
256
+ }),
257
+ ),
258
+ flags: Type.Optional(
259
+ Type.String({
260
+ description: "Additional flags (e.g., '-d' for detached, '--build' for up with build)",
261
+ }),
262
+ ),
263
+ }),
264
+ execute: async (_toolCallId, params) => {
265
+ if (!dockerAvailable()) {
266
+ return {
267
+ content: [{ type: "text", text: "Docker is not running or not installed." }],
268
+ details: {},
269
+ };
270
+ }
271
+
272
+ const allowed = ["up", "down", "restart", "build", "pull", "ps", "stop", "start"];
273
+ if (!allowed.includes(params.action)) {
274
+ return {
275
+ content: [
276
+ {
277
+ type: "text",
278
+ text: `Action "${params.action}" not allowed. Use: ${allowed.join(", ")}`,
279
+ },
280
+ ],
281
+ details: {},
282
+ };
283
+ }
284
+
285
+ let cmd = `docker compose ${params.action}`;
286
+ if (params.flags) cmd += ` ${params.flags}`;
287
+ if (params.service) cmd += ` ${params.service}`;
288
+
289
+ // Add -d for up if not specified
290
+ if (params.action === "up" && !params.flags?.includes("-d")) {
291
+ cmd = `docker compose up -d${params.service ? ` ${params.service}` : ""}`;
292
+ }
293
+
294
+ const cwd = process.cwd();
295
+ const output = exec(cmd, cwd, 120000);
296
+
297
+ return {
298
+ content: [
299
+ {
300
+ type: "text",
301
+ text: `$ ${cmd}\n\n${output || "(done)"}`,
302
+ },
303
+ ],
304
+ details: { action: params.action, service: params.service },
305
+ };
306
+ },
307
+ });
308
+
309
+ // ── Tool: docker_env_check ──────────────────────────────────────────────
310
+
311
+ elyra.registerTool({
312
+ name: "docker_env_check",
313
+ label: "Docker Env Check",
314
+ description:
315
+ "Compare host .env file with the actual environment variables inside a container. " +
316
+ "Finds mismatches that cause 'works on my machine' bugs: missing vars, different values, " +
317
+ "vars in .env but not in container, vars in container but not in .env.",
318
+ parameters: Type.Object({
319
+ container: Type.String({
320
+ description: "Container or service name to check",
321
+ }),
322
+ env_file: Type.Optional(
323
+ Type.String({
324
+ description: "Path to .env file on host (default: .env)",
325
+ }),
326
+ ),
327
+ }),
328
+ execute: async (_toolCallId, params) => {
329
+ if (!dockerAvailable()) {
330
+ return {
331
+ content: [{ type: "text", text: "Docker is not running or not installed." }],
332
+ details: {},
333
+ };
334
+ }
335
+
336
+ const envFile = params.env_file ?? ".env";
337
+ const cwd = process.cwd();
338
+ const envPath = join(cwd, envFile);
339
+
340
+ if (!existsSync(envPath)) {
341
+ return {
342
+ content: [{ type: "text", text: `File not found: ${envFile}` }],
343
+ details: {},
344
+ };
345
+ }
346
+
347
+ // Parse host .env
348
+ const hostEnv = new Map<string, string>();
349
+ const envContent = readFileSync(envPath, "utf-8");
350
+ for (const line of envContent.split("\n")) {
351
+ const trimmed = line.trim();
352
+ if (!trimmed || trimmed.startsWith("#")) continue;
353
+ const eqIdx = trimmed.indexOf("=");
354
+ if (eqIdx === -1) continue;
355
+ const key = trimmed.slice(0, eqIdx).trim();
356
+ const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
357
+ hostEnv.set(key, value);
358
+ }
359
+
360
+ // Get container env
361
+ const containerEnvRaw = exec(`docker exec ${params.container} env`, undefined, 10000);
362
+ const containerEnv = new Map<string, string>();
363
+ for (const line of containerEnvRaw.split("\n")) {
364
+ const eqIdx = line.indexOf("=");
365
+ if (eqIdx === -1) continue;
366
+ containerEnv.set(line.slice(0, eqIdx), line.slice(eqIdx + 1));
367
+ }
368
+
369
+ // Compare
370
+ const missing: string[] = [];
371
+ const different: string[] = [];
372
+ const extraInContainer: string[] = [];
373
+
374
+ for (const [key, hostValue] of hostEnv) {
375
+ if (!containerEnv.has(key)) {
376
+ missing.push(key);
377
+ } else if (containerEnv.get(key) !== hostValue) {
378
+ different.push(`${key}: host="${hostValue}" container="${containerEnv.get(key)}"`);
379
+ }
380
+ }
381
+
382
+ // Skip common system vars for "extra in container" check
383
+ const systemVars = new Set([
384
+ "PATH", "HOME", "HOSTNAME", "TERM", "SHLVL", "PWD", "LANG", "LC_ALL",
385
+ "GPG_KEY", "PHPIZE_DEPS", "PHP_INI_DIR", "PHP_VERSION", "PHP_SHA256",
386
+ "COMPOSER_ALLOW_SUPERUSER", "NODE_VERSION", "YARN_VERSION",
387
+ ]);
388
+ for (const [key] of containerEnv) {
389
+ if (!hostEnv.has(key) && !systemVars.has(key) && !key.startsWith("_")) {
390
+ extraInContainer.push(key);
391
+ }
392
+ }
393
+
394
+ const parts = [`# Environment Check: ${envFile} vs ${params.container}\n`];
395
+
396
+ if (missing.length === 0 && different.length === 0) {
397
+ parts.push("All host .env variables are present in the container with matching values.\n");
398
+ }
399
+
400
+ if (missing.length > 0) {
401
+ parts.push(`## Missing in container (${missing.length})\n`);
402
+ parts.push("These variables are in your .env but not in the container:\n");
403
+ for (const key of missing) parts.push(`- \`${key}\``);
404
+ parts.push("");
405
+ }
406
+
407
+ if (different.length > 0) {
408
+ parts.push(`## Different values (${different.length})\n`);
409
+ for (const diff of different) parts.push(`- ${diff}`);
410
+ parts.push("");
411
+ }
412
+
413
+ if (extraInContainer.length > 0) {
414
+ parts.push(`## Only in container (${extraInContainer.length})\n`);
415
+ parts.push("These are set in the container but not in your .env:\n");
416
+ for (const key of extraInContainer) parts.push(`- \`${key}\``);
417
+ parts.push("");
418
+ }
419
+
420
+ return {
421
+ content: [{ type: "text", text: parts.join("\n") }],
422
+ details: {
423
+ missing: missing.length,
424
+ different: different.length,
425
+ extra: extraInContainer.length,
426
+ },
427
+ };
428
+ },
429
+ });
430
+
431
+ // ── Command: /docker ────────────────────────────────────────────────────
432
+
433
+ elyra.registerCommand("docker", {
434
+ description: "Docker container status dashboard",
435
+ handler: async (_args, ctx) => {
436
+ if (!dockerAvailable()) {
437
+ ctx.ui.notify("Docker is not running or not installed.", "error");
438
+ return;
439
+ }
440
+
441
+ const ps = exec(
442
+ 'docker ps --format "{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"',
443
+ );
444
+
445
+ if (!ps) {
446
+ ctx.ui.notify("No Docker containers running.", "info");
447
+ return;
448
+ }
449
+
450
+ const lines = ps.split("\n").map((line) => {
451
+ const [name, image, status, ports] = line.split("\t");
452
+ return ` ${name}\t${image}\t${status}\t${ports || ""}`;
453
+ });
454
+
455
+ ctx.ui.notify(
456
+ `Docker Containers:\n\n${lines.join("\n")}`,
457
+ "info",
458
+ );
459
+ },
460
+ });
461
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@elyracode/docker",
3
+ "version": "0.5.12",
4
+ "description": "Docker-aware development for Elyra -- container exec, log tailing, compose operations, environment sync",
5
+ "type": "module",
6
+ "keywords": [
7
+ "elyra-package",
8
+ "docker",
9
+ "containers",
10
+ "docker-compose",
11
+ "devops"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Knut W. Horne",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/kwhorne/elyra.git",
18
+ "directory": "packages/docker"
19
+ },
20
+ "elyra": {
21
+ "extensions": [
22
+ "./extensions/index.ts"
23
+ ],
24
+ "skills": [
25
+ "./skills"
26
+ ]
27
+ },
28
+ "peerDependencies": {
29
+ "@elyracode/coding-agent": "*",
30
+ "typebox": "*"
31
+ },
32
+ "scripts": {
33
+ "clean": "echo 'nothing to clean'",
34
+ "build": "echo 'nothing to build'",
35
+ "check": "echo 'nothing to check'"
36
+ }
37
+ }
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: elyra-docker
3
+ description: Docker container awareness. Use when the project has docker-compose.yml or Dockerfile, or when the user asks about containers, logs, or Docker operations.
4
+ ---
5
+
6
+ # Docker Development
7
+
8
+ ## When to Use
9
+
10
+ Use Docker tools when:
11
+ - The project has a docker-compose.yml or compose.yml
12
+ - The user asks to run commands that should execute inside a container
13
+ - The user needs container logs or status
14
+ - Environment variable mismatches between host and container need debugging
15
+
16
+ ## Available Tools
17
+
18
+ | Tool | Use when |
19
+ |------|----------|
20
+ | `docker_exec` | Running commands inside containers (migrations, tests, builds) |
21
+ | `docker_logs` | Reading application or service logs from containers |
22
+ | `docker_status` | Checking what's running, ports, resource usage |
23
+ | `docker_compose` | Starting, stopping, restarting, or rebuilding services |
24
+ | `docker_env_check` | Debugging environment variable mismatches |
25
+
26
+ ## Container Routing
27
+
28
+ When a project uses Docker, commands like `php artisan migrate`, `npm test`, or `python manage.py` should typically run inside the appropriate container, not on the host. Use `docker_exec` to route these commands.
29
+
30
+ Common container names to look for:
31
+ - app, web, api -- the main application
32
+ - node, frontend -- JavaScript/Node.js services
33
+ - db, mysql, postgres, redis -- data services
34
+ - nginx, caddy -- web servers
35
+ - worker, queue -- background job processors
36
+
37
+ ## Rules
38
+
39
+ - Always check `docker_status` first if unsure which containers are running
40
+ - Use `docker_logs` before debugging -- the answer is often in the logs
41
+ - Run `docker_env_check` when environment-related bugs appear
42
+ - Prefer `docker_compose restart <service>` over `docker compose down && up` for faster iteration