import type { ExtensionAPI } from "@jaeswift/jae-coding-agent"; import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join, relative } from "node:path"; import { Type } from "@sinclair/typebox"; interface AuditResult { outdated: string[]; vulnerabilities: string[]; unused: string[]; missing: string[]; summary: string; } export default function (pi: ExtensionAPI) { async function parsePackageJson(cwd: string): Promise<{ deps: Record; devDeps: Record; scripts: Record } | null> { const pkgPath = join(cwd, "package.json"); if (!existsSync(pkgPath)) return null; try { const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); return { deps: pkg.dependencies || {}, devDeps: pkg.devDependencies || {}, scripts: pkg.scripts || {}, }; } catch { return null; } } async function runNpmAudit(cwd: string): Promise { try { const result = await pi.exec("npm", ["audit", "--json"], { cwd }); if (result.stdout.trim()) { try { const audit = JSON.parse(result.stdout); const meta = audit.metadata || {}; const vulns = meta.vulnerabilities || {}; const lines: string[] = []; let totalVulns = 0; for (const [severity, count] of Object.entries(vulns)) { if (typeof count === "number" && count > 0) { lines.push(` ${severity}: ${count}`); totalVulns += count; } } if (totalVulns === 0) return "\u2705 No known vulnerabilities found."; return `\u26A0\uFE0F Found ${totalVulns} vulnerabilities:\n${lines.join("\n")}`; } catch { return result.stdout.substring(0, 1000); } } return result.stderr.trim() || "No audit data available."; } catch (err: any) { return `Audit error: ${err.message}`; } } async function runNpmOutdated(cwd: string): Promise { try { const result = await pi.exec("npm", ["outdated", "--json"], { cwd }); if (result.stdout.trim()) { try { const outdated = JSON.parse(result.stdout); const lines: string[] = []; for (const [pkg, info] of Object.entries(outdated)) { const i = info as any; lines.push(` ${pkg}: ${i.current || "?"} \u2192 ${i.latest || "?"}`); } return lines; } catch { return [result.stdout.substring(0, 500)]; } } return []; } catch { return []; } } async function findUnusedDeps(cwd: string, deps: Record): Promise { const unused: string[] = []; const depNames = Object.keys(deps); if (depNames.length === 0) return unused; for (const dep of depNames) { try { // Search for import/require of this dep in source files const result = await pi.exec( "grep", ["-r", "-l", "--include=*.ts", "--include=*.tsx", "--include=*.js", "--include=*.jsx", "--include=*.mjs", "--include=*.cjs", dep, "."], { cwd }, ); if (result.code !== 0 || !result.stdout.trim()) { // Also check if referenced in config files const configResult = await pi.exec( "grep", ["-r", "-l", "--include=*.json", "--include=*.yaml", "--include=*.yml", "--include=*.toml", "--include=*.config.*", dep, "."], { cwd }, ); // Exclude package.json and lock files const configFiles = (configResult.stdout || "").split("\n").filter( (f) => f.trim() && !f.includes("package.json") && !f.includes("package-lock") && !f.includes("node_modules"), ); if (configFiles.length === 0) { unused.push(dep); } } } catch { // grep failed, skip } } return unused; } async function fullAudit(cwd: string): Promise { const pkg = await parsePackageJson(cwd); if (!pkg) { return { outdated: [], vulnerabilities: ["No package.json found in current directory."], unused: [], missing: [], summary: "\u274C No package.json found. Navigate to a Node.js project directory.", }; } const allDeps = { ...pkg.deps, ...pkg.devDeps }; const totalDeps = Object.keys(allDeps).length; // Run checks in parallel const [outdated, securityReport, unused] = await Promise.all([ runNpmOutdated(cwd), runNpmAudit(cwd), findUnusedDeps(cwd, pkg.deps), ]); // Check for missing deps (referenced but not in package.json) const missing: string[] = []; try { const result = await pi.exec( "grep", ["-r", "-h", "-o", "--include=*.ts", "--include=*.tsx", "--include=*.js", "--include=*.jsx", `from ['"][^./][^'"]*['"]`, "."], { cwd }, ); if (result.stdout) { const imports = new Set( result.stdout.split("\n") .map((line) => { const match = line.match(/from\s+['"]([^./][^'"]*)['"]/); return match ? match[1].split("/")[0] : null; }) .filter((name): name is string => name !== null && !name.startsWith("node:")), ); for (const imp of imports) { if (!allDeps[imp] && !imp.startsWith("@")) { missing.push(imp); } } } } catch { /* ignore grep errors */ } const summaryLines = [ `\u{1F4E6} Dependencies: ${totalDeps} total (${Object.keys(pkg.deps).length} prod, ${Object.keys(pkg.devDeps).length} dev)`, `\u{1F504} Outdated: ${outdated.length > 0 ? outdated.length + " packages" : "all up to date \u2705"}`, `\u{1F6E1}\uFE0F Security: ${securityReport}`, `\u{1F5D1}\uFE0F Potentially unused: ${unused.length > 0 ? unused.join(", ") : "none detected \u2705"}`, ]; if (missing.length > 0) { summaryLines.push(`\u26A0\uFE0F Possibly missing: ${missing.join(", ")}`); } return { outdated, vulnerabilities: [securityReport], unused, missing, summary: summaryLines.join("\n"), }; } pi.registerTool({ name: "audit_dependencies", label: "Audit Dependencies", description: "Scan project dependencies for outdated packages, security vulnerabilities, and unused dependencies. Optionally auto-fix issues.", parameters: Type.Object({ action: Type.Optional(Type.String({ description: "Action: 'scan' (default), 'fix', 'security'" })), }), execute: async (args, ctx) => { const action = (args.action || "scan").toLowerCase(); if (action === "fix") { const updateResult = await pi.exec("npm", ["update"], { cwd: ctx.cwd }); const auditFixResult = await pi.exec("npm", ["audit", "fix"], { cwd: ctx.cwd }); return { output: `\u{1F527} Auto-fix results:\n\nnpm update:\n${updateResult.stdout || updateResult.stderr}\n\nnpm audit fix:\n${auditFixResult.stdout || auditFixResult.stderr}`, }; } if (action === "security") { const report = await runNpmAudit(ctx.cwd); return { output: `\u{1F6E1}\uFE0F Security Report:\n\n${report}` }; } const result = await fullAudit(ctx.cwd); return { output: result.summary }; }, }); pi.registerCommand("audit", { description: "Audit dependencies: /audit [fix|security]", handler: async (args, ctx) => { const subcommand = args.trim().toLowerCase(); if (subcommand === "fix") { ctx.ui.notify("\u{1F527} Running npm update and audit fix...", "info"); const updateResult = await pi.exec("npm", ["update"], { cwd: ctx.cwd }); const auditFixResult = await pi.exec("npm", ["audit", "fix"], { cwd: ctx.cwd }); ctx.ui.notify( `\u{1F527} Fix Results:\n\nnpm update:\n${(updateResult.stdout || updateResult.stderr || "done").substring(0, 500)}\n\nnpm audit fix:\n${(auditFixResult.stdout || auditFixResult.stderr || "done").substring(0, 500)}`, "info", ); return; } if (subcommand === "security") { ctx.ui.notify("\u{1F6E1}\uFE0F Running security audit...", "info"); const report = await runNpmAudit(ctx.cwd); ctx.ui.notify(`\u{1F6E1}\uFE0F Security Report:\n\n${report}`, "info"); return; } ctx.ui.notify("\u{1F50D} Scanning dependencies...", "info"); const result = await fullAudit(ctx.cwd); let output = `\u{1F4CB} Dependency Audit Report\n${'\u2500'.repeat(40)}\n\n${result.summary}`; if (result.outdated.length > 0) { output += `\n\n\u{1F504} Outdated Packages:\n${result.outdated.join("\n")}`; } if (result.unused.length > 0) { output += `\n\n\u{1F5D1}\uFE0F Potentially Unused:\n ${result.unused.join(", ")}`; output += `\n Run: npm uninstall ${result.unused.join(" ")}`; } if (result.missing.length > 0) { output += `\n\n\u26A0\uFE0F Possibly Missing:\n ${result.missing.join(", ")}`; output += `\n Run: npm install ${result.missing.join(" ")}`; } output += `\n\n\u{1F4A1} Tip: /audit fix to auto-update, /audit security for vuln details`; ctx.ui.notify(output, "info"); }, }); }