import type { ExtensionAPI } from "@jaeswift/jae-coding-agent"; import { existsSync, readFileSync } from "node:fs"; import { join, basename } from "node:path"; import { Type } from "@sinclair/typebox"; export default function (pi: ExtensionAPI) { async function dockerExec(args: string[], cwd: string): Promise<{ ok: boolean; output: string }> { try { const result = await pi.exec("docker", args, { cwd }); return { ok: result.code === 0, output: (result.stdout || "").trim() || (result.stderr || "").trim() || "(no output)", }; } catch (err: any) { return { ok: false, output: `Docker error: ${err.message}` }; } } async function getContainerStatus(cwd: string): Promise { const result = await dockerExec( ["ps", "--format", "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"], cwd, ); if (!result.ok) return `\u274C Docker not available: ${result.output}`; if (!result.output.trim() || result.output.split("\n").length <= 1) { return "\u{1F4AD} No running containers."; } return `\u{1F433} Running Containers:\n\n${result.output}`; } async function getContainerLogs(container: string, cwd: string, tail: number = 50): Promise { const result = await dockerExec(["logs", "--tail", String(tail), container], cwd); if (!result.ok) return `\u274C Failed to get logs for '${container}': ${result.output}`; return `\u{1F4DC} Logs for ${container} (last ${tail} lines):\n\n${result.output}`; } async function buildImage(cwd: string, tag?: string, dockerfile?: string): Promise { const imageTag = tag || basename(cwd).toLowerCase().replace(/[^a-z0-9_.-]/g, "-"); const args = ["build", "-t", imageTag]; if (dockerfile) args.push("-f", dockerfile); args.push("."); const result = await dockerExec(args, cwd); if (!result.ok) return `\u274C Build failed:\n${result.output.substring(0, 1000)}`; // Get image info const info = await dockerExec(["images", imageTag, "--format", "{{.Repository}}:{{.Tag}} {{.Size}} {{.CreatedAt}}"], cwd); return `\u2705 Image built successfully: ${imageTag}\n${info.output}\n\nRun: docker run -d -p 3000:3000 ${imageTag}`; } async function analyzeProjectForDockerfile(cwd: string): Promise { const analysis: string[] = []; // Detect project type if (existsSync(join(cwd, "package.json"))) { try { const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8")); const deps = { ...pkg.dependencies, ...pkg.devDependencies }; if (deps["next"]) { analysis.push("Detected: Next.js application"); analysis.push("Recommended: Multi-stage build with Node.js Alpine"); analysis.push(generateNodeDockerfile("next", pkg)); } else if (deps["vite"] || deps["react-scripts"]) { analysis.push("Detected: SPA (React/Vite)"); analysis.push("Recommended: Multi-stage build with nginx"); analysis.push(generateSPADockerfile(pkg)); } else { analysis.push("Detected: Node.js application"); analysis.push(generateNodeDockerfile("node", pkg)); } } catch { analysis.push("Could not parse package.json"); } } else if (existsSync(join(cwd, "requirements.txt")) || existsSync(join(cwd, "pyproject.toml"))) { analysis.push("Detected: Python application"); analysis.push(generatePythonDockerfile(cwd)); } else if (existsSync(join(cwd, "go.mod"))) { analysis.push("Detected: Go application"); analysis.push(generateGoDockerfile()); } else if (existsSync(join(cwd, "Cargo.toml"))) { analysis.push("Detected: Rust application"); analysis.push("Recommended: Multi-stage build with rust:alpine"); } else { analysis.push("Could not detect project type. Provide a Dockerfile manually."); } return analysis.join("\n\n"); } function generateNodeDockerfile(type: string, pkg: any): string { const startCmd = pkg.scripts?.start ? "npm start" : "node index.js"; if (type === "next") { return `# Suggested Dockerfile for Next.js FROM node:20-alpine AS deps WORKDIR /app COPY package*.json ./ RUN npm ci FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public EXPOSE 3000 CMD ["node", "server.js"]`; } return `# Suggested Dockerfile for Node.js FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY . . EXPOSE 3000 CMD ["${startCmd.split(" ")[0]}", ${startCmd.split(" ").slice(1).map((s: string) => `"${s}"`).join(", ")}]`; } function generateSPADockerfile(pkg: any): string { const buildDir = pkg.scripts?.build?.includes("vite") ? "dist" : "build"; return `# Suggested Dockerfile for SPA FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --from=builder /app/${buildDir} /usr/share/nginx/html COPY < { const action = args.action.toLowerCase(); switch (action) { case "status": case "ps": case "list": { const status = await getContainerStatus(ctx.cwd); return { output: status }; } case "build": { if (!existsSync(join(ctx.cwd, "Dockerfile"))) { return { error: "No Dockerfile found. Use action 'generate' to create one." }; } const result = await buildImage(ctx.cwd, args.tag); return { output: result }; } case "logs": { if (!args.container) return { error: "Specify a container name or ID." }; const logs = await getContainerLogs(args.container, ctx.cwd); return { output: logs }; } case "generate": { const analysis = await analyzeProjectForDockerfile(ctx.cwd); return { output: `\u{1F433} Dockerfile Generation:\n\n${analysis}` }; } case "run": { const tag = args.tag || basename(ctx.cwd).toLowerCase().replace(/[^a-z0-9_.-]/g, "-"); const runArgs = ["run", "-d"]; if (args.port) runArgs.push("-p", args.port); runArgs.push(tag); const result = await dockerExec(runArgs, ctx.cwd); return { output: result.ok ? `\u2705 Container started: ${result.output.substring(0, 12)}` : `\u274C ${result.output}` }; } case "stop": { if (!args.container) return { error: "Specify a container name or ID." }; const result = await dockerExec(["stop", args.container], ctx.cwd); return { output: result.ok ? `\u2705 Stopped: ${args.container}` : `\u274C ${result.output}` }; } default: return { error: `Unknown action '${action}'. Use: status, build, logs, generate, run, stop` }; } }, }); pi.registerCommand("docker", { description: "Docker manager: /docker [build|logs |generate|status]", handler: async (args, ctx) => { const parts = args.trim().split(/\s+/); const subcommand = (parts[0] || "status").toLowerCase(); switch (subcommand) { case "status": case "ps": case "": { const status = await getContainerStatus(ctx.cwd); ctx.ui.notify(status, "info"); break; } case "build": { if (!existsSync(join(ctx.cwd, "Dockerfile"))) { ctx.ui.notify("\u274C No Dockerfile found. Use /docker generate first.", "warning"); return; } ctx.ui.notify("\u{1F528} Building Docker image...", "info"); const tag = parts[1] || undefined; const result = await buildImage(ctx.cwd, tag); ctx.ui.notify(result, "info"); break; } case "logs": { const container = parts[1]; if (!container) { ctx.ui.notify("Usage: /docker logs ", "warning"); return; } const tail = parseInt(parts[2] || "50", 10); const logs = await getContainerLogs(container, ctx.cwd, tail); ctx.ui.notify(logs, "info"); break; } case "generate": { ctx.ui.notify("\u{1F50D} Analyzing project for Dockerfile generation...", "info"); const analysis = await analyzeProjectForDockerfile(ctx.cwd); ctx.ui.notify(`\u{1F433} Dockerfile Suggestion:\n\n${analysis}\n\n\u{1F4A1} Tip: Ask JAE to create the Dockerfile based on this analysis.`, "info"); break; } case "stop": { const container = parts[1]; if (!container) { ctx.ui.notify("Usage: /docker stop ", "warning"); return; } const result = await dockerExec(["stop", container], ctx.cwd); ctx.ui.notify(result.ok ? `\u2705 Stopped: ${container}` : `\u274C ${result.output}`, "info"); break; } case "run": { const tag = parts[1]; if (!tag) { ctx.ui.notify("Usage: /docker run [port-mapping]", "warning"); return; } const port = parts[2] || undefined; const runArgs = ["run", "-d"]; if (port) runArgs.push("-p", port); runArgs.push(tag); const result = await dockerExec(runArgs, ctx.cwd); ctx.ui.notify(result.ok ? `\u2705 Container started: ${result.output.substring(0, 12)}` : `\u274C ${result.output}`, "info"); break; } default: ctx.ui.notify( `\u{1F433} Docker Manager\n${'\u2500'.repeat(30)}\n\n` + "Commands:\n" + " /docker - Show running containers\n" + " /docker build - Build image from Dockerfile\n" + " /docker logs - Show container logs\n" + " /docker generate - Generate Dockerfile for project\n" + " /docker run - Run a container\n" + " /docker stop - Stop a container", "info", ); } }, }); }