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"; type DeployTarget = "docker" | "vercel" | "rsync" | "static" | "unknown"; interface StackInfo { type: DeployTarget; runtime: string; entrypoint?: string; } export default function (pi: ExtensionAPI) { function detectStack(cwd: string): StackInfo { if (existsSync(join(cwd, "Dockerfile"))) { return { type: "docker", runtime: "Docker", entrypoint: "Dockerfile" }; } if (existsSync(join(cwd, "docker-compose.yml"))) { return { type: "docker", runtime: "Docker Compose", entrypoint: "docker-compose.yml" }; } if (existsSync(join(cwd, "vercel.json"))) { return { type: "vercel", runtime: "Vercel" }; } 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"]) return { type: "vercel", runtime: "Next.js" }; if (deps["vite"] || deps["react-scripts"]) return { type: "static", runtime: "SPA (Vite/CRA)" }; if (pkg.scripts?.start) return { type: "docker", runtime: "Node.js" }; } catch { /* ignore */ } return { type: "docker", runtime: "Node.js" }; } if (existsSync(join(cwd, "requirements.txt")) || existsSync(join(cwd, "pyproject.toml"))) { return { type: "docker", runtime: "Python" }; } if (existsSync(join(cwd, "index.html"))) { return { type: "static", runtime: "Static HTML" }; } return { type: "unknown", runtime: "Unknown" }; } pi.registerTool({ name: "deploy", label: "Deploy Project", description: "Detect project stack and deploy. Supports Docker, Vercel, rsync, and static sites.", parameters: Type.Object({ target: Type.Optional(Type.String({ description: "Deploy target: docker, vercel, rsync, static (auto-detected if omitted)" })), host: Type.Optional(Type.String({ description: "Remote host for rsync deploy (user@host:/path)" })), tag: Type.Optional(Type.String({ description: "Docker image tag" })), }), execute: async (args, ctx) => { const stack = detectStack(ctx.cwd); const deployType = (args.target as DeployTarget) || stack.type; if (deployType === "unknown") { return { error: "Could not detect project stack. Specify a target: docker, vercel, rsync, static" }; } const results: string[] = [`Detected: ${stack.runtime}`, `Deploy target: ${deployType}`]; if (deployType === "docker") { const tag = args.tag || basename(ctx.cwd).toLowerCase(); const buildResult = await pi.exec("docker", ["build", "-t", tag, "."], { cwd: ctx.cwd }); if (buildResult.code !== 0) { return { error: `Docker build failed: ${buildResult.stderr}` }; } results.push(`Docker image built: ${tag}`); results.push(`Run with: docker run -d -p 3000:3000 ${tag}`); } else if (deployType === "vercel") { const vercelResult = await pi.exec("npx", ["vercel", "--yes"], { cwd: ctx.cwd }); results.push(vercelResult.code === 0 ? `Vercel deployed: ${vercelResult.stdout.trim()}` : `Vercel error: ${vercelResult.stderr}`); } else if (deployType === "rsync") { if (!args.host) { return { error: "rsync deploy requires --host argument (user@host:/path)" }; } const rsyncResult = await pi.exec( "rsync", ["-avz", "--exclude", "node_modules", "--exclude", ".git", "./", args.host], { cwd: ctx.cwd }, ); results.push(rsyncResult.code === 0 ? `Synced to ${args.host}` : `rsync error: ${rsyncResult.stderr}`); } else if (deployType === "static") { // Build if needed const pkgPath = join(ctx.cwd, "package.json"); if (existsSync(pkgPath)) { const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); if (pkg.scripts?.build) { const buildResult = await pi.exec("npm", ["run", "build"], { cwd: ctx.cwd }); results.push(buildResult.code === 0 ? "Build succeeded" : `Build error: ${buildResult.stderr}`); } } const distDir = existsSync(join(ctx.cwd, "dist")) ? "dist" : existsSync(join(ctx.cwd, "build")) ? "build" : "."; results.push(`Static files in: ${distDir}/`); results.push(`Serve locally: npx serve ${distDir}`); if (args.host) { const rsyncResult = await pi.exec("rsync", ["-avz", `${distDir}/`, args.host], { cwd: ctx.cwd }); results.push(rsyncResult.code === 0 ? `Deployed to ${args.host}` : `rsync error: ${rsyncResult.stderr}`); } } return { output: results.join("\n") }; }, }); pi.registerCommand("deploy", { description: "Deploy project: /deploy [docker|vercel|rsync|static] [--host user@host:/path] [--tag image-tag]", handler: async (args, ctx) => { const stack = detectStack(ctx.cwd); const parts = args.trim().split(/\s+/); const target = parts[0] || undefined; let host: string | undefined; let tag: string | undefined; for (let i = 0; i < parts.length; i++) { if (parts[i] === "--host" && parts[i + 1]) host = parts[i + 1]; if (parts[i] === "--tag" && parts[i + 1]) tag = parts[i + 1]; } if (!target) { ctx.ui.notify( `\u{1F680} Stack detected: ${stack.runtime} (${stack.type})\n\nUsage: /deploy [target] [options]\n Targets: docker, vercel, rsync, static\n Options: --host user@host:/path --tag image-name`, "info", ); return; } ctx.ui.notify(`\u{1F680} Deploying as ${target}...`, "info"); // Delegate to the tool ctx.ui.notify(`Use the deploy tool for full deployment. Target: ${target}, Host: ${host || "none"}, Tag: ${tag || "auto"}`, "info"); }, }); }