Agent-JAE/default-extensions/docker-manager.ts

318 lines
12 KiB
TypeScript

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<string> {
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<string> {
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<string> {
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<string> {
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 <<EOF /etc/nginx/conf.d/default.conf
server {
listen 80;
root /usr/share/nginx/html;
location / {
try_files \$uri \$uri/ /index.html;
}
}
EOF
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]`;
}
function generatePythonDockerfile(cwd: string): string {
const hasRequirements = existsSync(join(cwd, "requirements.txt"));
const hasPyproject = existsSync(join(cwd, "pyproject.toml"));
const installCmd = hasRequirements
? "COPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt"
: hasPyproject
? "COPY pyproject.toml .\nRUN pip install --no-cache-dir ."
: "# Add your dependency installation here";
return `# Suggested Dockerfile for Python
FROM python:3.12-slim
WORKDIR /app
${installCmd}
COPY . .
EXPOSE 8000
CMD ["python", "main.py"]`;
}
function generateGoDockerfile(): string {
return `# Suggested Dockerfile for Go
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server .
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]`;
}
pi.registerTool({
name: "docker_manage",
label: "Docker Manager",
description: "Manage Docker containers: list running containers, build images, view logs, or generate Dockerfiles from project analysis.",
parameters: Type.Object({
action: Type.String({ description: "Action: 'status', 'build', 'logs', 'generate', 'run', 'stop'" }),
container: Type.Optional(Type.String({ description: "Container name or ID (for logs/stop)" })),
tag: Type.Optional(Type.String({ description: "Image tag for build" })),
port: Type.Optional(Type.String({ description: "Port mapping for run (e.g., '3000:3000')" })),
}),
execute: async (args, ctx) => {
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 <container>|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 <container-name-or-id>", "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 <container-name-or-id>", "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 <image> [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 <id> - Show container logs\n" +
" /docker generate - Generate Dockerfile for project\n" +
" /docker run <img> - Run a container\n" +
" /docker stop <id> - Stop a container",
"info",
);
}
},
});
}