Some checks are pending
CI / build-check-test (push) Waiting to run
New extensions shipping with Agent JAE: - jae-rules: Per-project .jae/rules.md injection - cost-tracker: Token/cost tracking with budget limits - project-dna: Auto-analyze project fingerprint - teach-mode: Pedagogical explanation mode - bookmarks: File bookmark management - replay: Session recording and asciicast export - checkpoints: Git-based undo/time-travel - pr-review: Autonomous PR diff review - auto-docs: Auto-generate README and docs - screenshot-context: Vision analysis for screenshots - deploy: One-command deploy (Docker/Vercel/rsync/static) - pair-programming: Real-time file watching with feedback - dashboard: Terminal HUD with live stats - swarm: Multi-agent parallel task execution - skill-marketplace: Search/install/publish skills - widget-api-demo: TUI widget API demonstration
134 lines
5.9 KiB
TypeScript
134 lines
5.9 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";
|
|
|
|
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");
|
|
},
|
|
});
|
|
}
|