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
149 lines
5.7 KiB
TypeScript
149 lines
5.7 KiB
TypeScript
import type { ExtensionAPI } from "@jaeswift/jae-coding-agent";
|
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { join, basename } from "node:path";
|
|
import { Type } from "@sinclair/typebox";
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
async function analyzeAndGenerate(cwd: string, ctx: any): Promise<string> {
|
|
const sections: string[] = [];
|
|
|
|
// Project name
|
|
let projectName = basename(cwd);
|
|
let description = "";
|
|
const pkgPath = join(cwd, "package.json");
|
|
if (existsSync(pkgPath)) {
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
projectName = pkg.name || projectName;
|
|
description = pkg.description || "";
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
sections.push(`# ${projectName}\n`);
|
|
if (description) sections.push(`${description}\n`);
|
|
|
|
// Tech stack
|
|
const stackChecks = [
|
|
{ file: "package.json", label: "Node.js/JavaScript" },
|
|
{ file: "tsconfig.json", label: "TypeScript" },
|
|
{ file: "Cargo.toml", label: "Rust" },
|
|
{ file: "go.mod", label: "Go" },
|
|
{ file: "pyproject.toml", label: "Python" },
|
|
{ file: "requirements.txt", label: "Python" },
|
|
{ file: "Dockerfile", label: "Docker" },
|
|
{ file: "docker-compose.yml", label: "Docker Compose" },
|
|
{ file: "next.config.js", label: "Next.js" },
|
|
{ file: "next.config.mjs", label: "Next.js" },
|
|
{ file: "vite.config.ts", label: "Vite" },
|
|
{ file: "tailwind.config.js", label: "Tailwind CSS" },
|
|
{ file: "tailwind.config.ts", label: "Tailwind CSS" },
|
|
];
|
|
const stack = [...new Set(stackChecks.filter((c) => existsSync(join(cwd, c.file))).map((c) => c.label))];
|
|
if (stack.length > 0) {
|
|
sections.push(`## Tech Stack\n${stack.map((s) => `- ${s}`).join("\n")}\n`);
|
|
}
|
|
|
|
// Prerequisites
|
|
sections.push(`## Prerequisites\n`);
|
|
if (existsSync(join(cwd, "package.json"))) sections.push(`- Node.js (v18+ recommended)\n- npm or yarn\n`);
|
|
if (existsSync(join(cwd, "requirements.txt"))) sections.push(`- Python 3.8+\n- pip\n`);
|
|
if (existsSync(join(cwd, "Cargo.toml"))) sections.push(`- Rust / cargo\n`);
|
|
if (existsSync(join(cwd, "Dockerfile"))) sections.push(`- Docker\n`);
|
|
|
|
// Installation
|
|
sections.push(`## Installation\n\n\`\`\`bash\ngit clone <repo-url>\ncd ${basename(cwd)}\n`);
|
|
if (existsSync(join(cwd, "package.json"))) sections.push(`npm install\n`);
|
|
if (existsSync(join(cwd, "requirements.txt"))) sections.push(`pip install -r requirements.txt\n`);
|
|
sections.push(`\`\`\`\n`);
|
|
|
|
// Scripts
|
|
if (existsSync(pkgPath)) {
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
if (pkg.scripts && Object.keys(pkg.scripts).length > 0) {
|
|
sections.push(`## Available Scripts\n`);
|
|
for (const [name, cmd] of Object.entries(pkg.scripts).slice(0, 20) as [string, string][]) {
|
|
sections.push(`- \`npm run ${name}\` - ${cmd}`);
|
|
}
|
|
sections.push("");
|
|
}
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
// Project structure
|
|
try {
|
|
const treeResult = await pi.exec(
|
|
"find",
|
|
[".", "-maxdepth", "2", "-type", "f",
|
|
"-not", "-path", "*/node_modules/*",
|
|
"-not", "-path", "*/.git/*",
|
|
"-not", "-path", "*/dist/*"],
|
|
{ cwd },
|
|
);
|
|
if (treeResult.code === 0 && treeResult.stdout.trim()) {
|
|
const files = treeResult.stdout.trim().split("\n").sort().slice(0, 40);
|
|
sections.push(`## Project Structure\n\n\`\`\`\n${files.join("\n")}\n\`\`\`\n`);
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
// Env vars
|
|
const envExample = join(cwd, ".env.example");
|
|
if (existsSync(envExample)) {
|
|
const envContent = readFileSync(envExample, "utf-8").trim();
|
|
sections.push(`## Environment Variables\n\nCopy \`.env.example\` to \`.env\` and configure:\n\n\`\`\`\n${envContent}\n\`\`\`\n`);
|
|
}
|
|
|
|
// License
|
|
const licensePath = join(cwd, "LICENSE");
|
|
if (existsSync(licensePath)) {
|
|
const license = readFileSync(licensePath, "utf-8");
|
|
const firstLine = license.split("\n")[0].trim();
|
|
sections.push(`## License\n\n${firstLine || "See LICENSE file."}\n`);
|
|
}
|
|
|
|
sections.push(`\n---\n*Auto-generated by Agent JAE on ${new Date().toISOString().split("T")[0]}*\n`);
|
|
|
|
return sections.join("\n");
|
|
}
|
|
|
|
pi.registerTool({
|
|
name: "generate_docs",
|
|
label: "Generate Documentation",
|
|
description: "Auto-generate README and documentation for the current project",
|
|
parameters: Type.Object({
|
|
output: Type.Optional(Type.String({ description: "Output filename (default: README.md)" })),
|
|
}),
|
|
execute: async (args, ctx) => {
|
|
const filename = args.output || "README.md";
|
|
const content = await analyzeAndGenerate(ctx.cwd, ctx);
|
|
const outPath = join(ctx.cwd, filename);
|
|
writeFileSync(outPath, content, "utf-8");
|
|
return { output: `Documentation generated: ${outPath} (${content.length} chars)` };
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("docs", {
|
|
description: "Auto-generate README & docs: /docs [filename]",
|
|
handler: async (args, ctx) => {
|
|
const filename = args.trim() || "README.md";
|
|
ctx.ui.notify("\u{1F4DD} Analyzing project and generating documentation...", "info");
|
|
|
|
const content = await analyzeAndGenerate(ctx.cwd, ctx);
|
|
const outPath = join(ctx.cwd, filename);
|
|
|
|
if (existsSync(outPath)) {
|
|
const confirmed = await ctx.ui.confirm(
|
|
"Overwrite?",
|
|
`${filename} already exists. Overwrite?`,
|
|
);
|
|
if (!confirmed) {
|
|
ctx.ui.notify("Cancelled.", "info");
|
|
return;
|
|
}
|
|
}
|
|
|
|
writeFileSync(outPath, content, "utf-8");
|
|
ctx.ui.notify(`\u{1F4DD} Documentation generated: ${outPath} (${content.length} chars)`, "info");
|
|
},
|
|
});
|
|
}
|