import type { ExtensionAPI } from "@jaeswift/jae-coding-agent"; import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from "node:fs"; import { join, relative, basename } from "node:path"; import { Type } from "@sinclair/typebox"; interface EnvFile { name: string; path: string; vars: Map; } export default function (pi: ExtensionAPI) { function parseEnvFile(filePath: string): Map { const vars = new Map(); if (!existsSync(filePath)) return vars; try { const content = readFileSync(filePath, "utf-8"); for (const line of content.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const eqIndex = trimmed.indexOf("="); if (eqIndex === -1) continue; const key = trimmed.substring(0, eqIndex).trim(); let value = trimmed.substring(eqIndex + 1).trim(); // Strip quotes if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } vars.set(key, value); } } catch { /* ignore read errors */ } return vars; } function findEnvFiles(cwd: string): EnvFile[] { const envFiles: EnvFile[] = []; function scanDir(dir: string, depth: number = 0): void { if (depth > 3) return; // Don't go too deep try { const entries = readdirSync(dir); for (const entry of entries) { if (entry === "node_modules" || entry === ".git" || entry === "dist" || entry === "build") continue; const fullPath = join(dir, entry); try { const stat = statSync(fullPath); if (stat.isFile() && (entry.startsWith(".env") || entry.endsWith(".env"))) { envFiles.push({ name: relative(cwd, fullPath) || entry, path: fullPath, vars: parseEnvFile(fullPath), }); } else if (stat.isDirectory() && depth < 3) { scanDir(fullPath, depth + 1); } } catch { /* skip inaccessible */ } } } catch { /* skip unreadable dirs */ } } scanDir(cwd); return envFiles; } function maskValue(value: string): string { if (value.length <= 4) return "****"; return value.substring(0, 2) + "*".repeat(Math.min(value.length - 4, 20)) + value.substring(value.length - 2); } function diffEnvFiles(fileA: EnvFile, fileB: EnvFile): string { const allKeys = new Set([...fileA.vars.keys(), ...fileB.vars.keys()]); const lines: string[] = [ `\u{1F504} Diff: ${fileA.name} vs ${fileB.name}`, `${'\u2500'.repeat(50)}`, ]; let onlyA: string[] = []; let onlyB: string[] = []; let different: string[] = []; let same = 0; for (const key of Array.from(allKeys).sort()) { const hasA = fileA.vars.has(key); const hasB = fileB.vars.has(key); if (hasA && !hasB) { onlyA.push(key); } else if (!hasA && hasB) { onlyB.push(key); } else if (hasA && hasB) { const valA = fileA.vars.get(key)!; const valB = fileB.vars.get(key)!; if (valA !== valB) { different.push(` ${key}: ${maskValue(valA)} \u2192 ${maskValue(valB)}`); } else { same++; } } } if (onlyA.length > 0) lines.push(`\n\u{1F7E2} Only in ${fileA.name}:\n ${onlyA.join(", ")}`); if (onlyB.length > 0) lines.push(`\n\u{1F535} Only in ${fileB.name}:\n ${onlyB.join(", ")}`); if (different.length > 0) lines.push(`\n\u{1F7E1} Different values:\n${different.join("\n")}`); lines.push(`\n\u2705 ${same} variables identical`); return lines.join("\n"); } function checkMissingVars(envFiles: EnvFile[]): string { if (envFiles.length < 2) return "Need at least 2 .env files to check for missing variables."; // Collect all keys across all files const allKeys = new Set(); for (const f of envFiles) { for (const key of f.vars.keys()) { allKeys.add(key); } } const lines: string[] = [ `\u{1F50D} Missing Variables Check`, `${'\u2500'.repeat(50)}`, `Files: ${envFiles.map((f) => f.name).join(", ")}`, `Total unique keys: ${allKeys.size}`, "", ]; let missingCount = 0; for (const key of Array.from(allKeys).sort()) { const presentIn: string[] = []; const missingFrom: string[] = []; for (const f of envFiles) { if (f.vars.has(key)) presentIn.push(f.name); else missingFrom.push(f.name); } if (missingFrom.length > 0) { lines.push(` \u26A0\uFE0F ${key}: missing from ${missingFrom.join(", ")}`); missingCount++; } } if (missingCount === 0) { lines.push("\u2705 All variables present in all env files!"); } else { lines.push(`\n\u{1F4CA} ${missingCount} variables missing across environments`); } return lines.join("\n"); } function syncEnvFiles(source: EnvFile, target: EnvFile): { added: string[]; content: string } { const added: string[] = []; const targetContent = existsSync(target.path) ? readFileSync(target.path, "utf-8") : ""; let newContent = targetContent; for (const [key, value] of source.vars) { if (!target.vars.has(key)) { added.push(key); const appendLine = `\n# Synced from ${source.name}\n${key}=${value}`; newContent += appendLine; } } return { added, content: newContent }; } pi.registerTool({ name: "env_manage", label: "Environment Manager", description: "Manage .env files: list, diff, check for missing vars, or sync between environments.", parameters: Type.Object({ action: Type.String({ description: "Action: 'list', 'diff', 'check', 'sync'" }), source: Type.Optional(Type.String({ description: "Source env file name for diff/sync" })), target: Type.Optional(Type.String({ description: "Target env file name for diff/sync" })), }), execute: async (args, ctx) => { const envFiles = findEnvFiles(ctx.cwd); const action = args.action.toLowerCase(); switch (action) { case "list": { if (envFiles.length === 0) return { output: "No .env files found in project." }; const list = envFiles .map((f) => ` ${f.name}: ${f.vars.size} variables`) .join("\n"); return { output: `\u{1F4C1} Environment Files:\n${list}` }; } case "diff": { if (envFiles.length < 2) return { error: "Need at least 2 .env files to diff." }; const source = args.source ? envFiles.find((f) => f.name.includes(args.source!)) : envFiles[0]; const target = args.target ? envFiles.find((f) => f.name.includes(args.target!)) : envFiles[1]; if (!source || !target) return { error: "Could not find specified env files." }; return { output: diffEnvFiles(source, target) }; } case "check": { return { output: checkMissingVars(envFiles) }; } case "sync": { if (!args.source || !args.target) return { error: "Specify source and target env files." }; const source = envFiles.find((f) => f.name.includes(args.source!)); const target = envFiles.find((f) => f.name.includes(args.target!)); if (!source || !target) return { error: "Could not find specified env files." }; const { added, content } = syncEnvFiles(source, target); if (added.length === 0) return { output: "\u2705 Target already has all source variables." }; writeFileSync(target.path, content, "utf-8"); return { output: `\u2705 Synced ${added.length} variables to ${target.name}:\n ${added.join(", ")}` }; } default: return { error: `Unknown action '${action}'. Use: list, diff, check, sync` }; } }, }); pi.registerCommand("env", { description: "Manage env files: /env [diff|check|sync ]", handler: async (args, ctx) => { const parts = args.trim().split(/\s+/); const subcommand = (parts[0] || "list").toLowerCase(); const envFiles = findEnvFiles(ctx.cwd); switch (subcommand) { case "list": case "": { if (envFiles.length === 0) { ctx.ui.notify("\u{1F4C1} No .env files found in this project.", "info"); return; } const list = envFiles .map((f) => { const keys = Array.from(f.vars.keys()).slice(0, 5).join(", "); const more = f.vars.size > 5 ? ` +${f.vars.size - 5} more` : ""; return ` \u{1F4C4} ${f.name} (${f.vars.size} vars): ${keys}${more}`; }) .join("\n"); ctx.ui.notify(`\u{1F4C1} Environment Files:\n\n${list}`, "info"); break; } case "diff": { if (envFiles.length < 2) { ctx.ui.notify("Need at least 2 .env files to diff.", "warning"); return; } const source = parts[1] ? envFiles.find((f) => f.name.includes(parts[1])) : envFiles[0]; const target = parts[2] ? envFiles.find((f) => f.name.includes(parts[2])) : envFiles[1]; if (!source || !target) { ctx.ui.notify("Could not find specified env files. Use /env to list them.", "warning"); return; } ctx.ui.notify(diffEnvFiles(source, target), "info"); break; } case "check": { ctx.ui.notify(checkMissingVars(envFiles), "info"); break; } case "sync": { const sourceName = parts[1]; const targetName = parts[2]; if (!sourceName || !targetName) { ctx.ui.notify("Usage: /env sync \n\nExample: /env sync .env.development .env.production", "warning"); return; } const source = envFiles.find((f) => f.name.includes(sourceName)); const target = envFiles.find((f) => f.name.includes(targetName)); if (!source) { ctx.ui.notify(`Could not find env file matching '${sourceName}'.`, "warning"); return; } if (!target) { ctx.ui.notify(`Could not find env file matching '${targetName}'.`, "warning"); return; } const { added, content } = syncEnvFiles(source, target); if (added.length === 0) { ctx.ui.notify("\u2705 Target already has all source variables. Nothing to sync.", "info"); return; } writeFileSync(target.path, content, "utf-8"); ctx.ui.notify(`\u2705 Synced ${added.length} missing variables to ${target.name}:\n ${added.join(", ")}`, "info"); break; } default: ctx.ui.notify( `\u{1F4C1} Environment Manager\n${'\u2500'.repeat(30)}\n\n` + "Commands:\n" + " /env - List all .env files\n" + " /env diff [a] [b] - Compare two env files\n" + " /env check - Find missing vars across files\n" + " /env sync - Sync missing vars from source to target", "info", ); } }, }); }