#!/usr/bin/env node import { initProject } from "./init.js"; import { patchFile } from "./patch.js"; import { validateMaps } from "./validate.js"; import { reinitPath } from "./init.js"; import { discoverProject } from "./discover.js"; import { createLLMClient, LLMError } from "./llm-client.js"; import { loadConfig } from "./config.js"; import pc from "picocolors"; const args = process.argv.slice(2); const command = args[0]; function printUsage() { console.log(`${pc.bold("project-map")} — hierarchical project analysis for Pi agents `); console.log(`${pc.bold("Usage:")}`); console.log( ` project-map ${pc.cyan("init")} [path] Generate .pi-map.md files for all directories`, ); console.log( ` project-map ${pc.cyan("patch")} Update analysis for a changed file's directory`, ); console.log( ` project-map ${pc.cyan("validate")} [--fix] [path] Check for stale/missing/orphaned entries`, ); console.log( ` project-map ${pc.cyan("reinit")} [path] Force full regeneration`, ); console.log( ` project-map ${pc.cyan("--help")} Show this help message`, ); console.log( ` project-map ${pc.cyan("--version")} Show version\n`, ); console.log(`${pc.bold("Options:")}`); console.log( ` --llm-provider=openai|kimi LLM provider (default: config or openai)`, ); console.log( ` --llm-model= LLM model name (or set LLM_MODEL env var)`, ); console.log(` --llm-base-url= Custom base URL for LLM API\n`); console.log(`${pc.bold("Examples:")}`); console.log(` project-map init`); console.log(` project-map patch src/components/Button.tsx`); console.log(` project-map validate --fix`); console.log(` project-map reinit`); console.log( ` project-map init --llm-provider=kimi --llm-model=kimi-k2-6`, ); } function printVersion() { const pkg = require("../package.json"); console.log(pkg.version); } function formatCount(count: number, label: string): string { const plural = label.endsWith("y") ? `${label.slice(0, -1)}ies` : `${label}${count === 1 ? "" : "s"}`; return `${pc.bold(String(count))} ${count === 1 ? label : plural}`; } function parseArgs(args: string[]): { path: string; fix: boolean; llmProvider?: string; llmModel?: string; llmBaseUrl?: string; positional: string[]; } { let path = "."; let fix = false; let llmProvider: string | undefined; let llmModel: string | undefined; let llmBaseUrl: string | undefined; const positional: string[] = []; for (const arg of args.slice(1)) { if (arg === "--fix") { fix = true; } else if (arg.startsWith("--llm-provider=")) { llmProvider = arg.slice("--llm-provider=".length); } else if (arg.startsWith("--llm-model=")) { llmModel = arg.slice("--llm-model=".length); } else if (arg.startsWith("--llm-base-url=")) { llmBaseUrl = arg.slice("--llm-base-url=".length); } else if (!arg.startsWith("-")) { positional.push(arg); path = arg; } } return { path, fix, llmProvider, llmModel, llmBaseUrl, positional }; } function createClientFromArgs(args: ReturnType) { const config = loadConfig(); const provider = (args.llmProvider || config.llmProvider) as | "openai" | "kimi" | "pi"; return createLLMClient(provider, { model: args.llmModel || config.llmModel || process.env.LLM_MODEL, baseUrl: args.llmBaseUrl || config.llmBaseUrl, }); } async function main() { if (!command || command === "--help" || command === "-h") { printUsage(); process.exit(0); } if (command === "--version" || command === "-v") { printVersion(); process.exit(0); } const parsed = parseArgs(args); switch (command) { case "init": { const targetPath = parsed.positional[0] || "."; const start = Date.now(); const entries = discoverProject(targetPath); console.log(`Scanning ${formatCount(entries.length, "directory")}...`); const client = createClientFromArgs(parsed); await initProject(targetPath, { verbose: false, llmClient: client, cacheDir: targetPath }); const elapsed = ((Date.now() - start) / 1000).toFixed(1); console.log( `${pc.green("✓")} Generated ${formatCount(entries.length, ".pi-map.md file")} in ${elapsed}s`, ); break; } case "patch": { if (!parsed.positional[0]) { console.error( `${pc.red("Error:")} Missing file path. Usage: project-map patch `, ); process.exit(1); } const client = createClientFromArgs(parsed); await patchFile(parsed.positional[0], client, process.cwd()); console.log(`${pc.green("✓")} Patched`); break; } case "validate": { const { path, fix } = parsed; const result = await validateMaps(path, { fix, verbose: true }); if (result.clean) { console.log(`${pc.green("✓")} All .pi-map.md files are clean.`); } else { const counts: Record = {}; for (const d of result.discrepancies) { counts[d.type] = (counts[d.type] || 0) + 1; } const summary = Object.entries(counts) .map(([type, count]) => `${count} ${type}`) .join(", "); const fixMsg = fix && result.fixed !== undefined ? ` (${pc.green("✓")} fixed ${formatCount(result.fixed, "directory")})` : ""; console.log( `${pc.yellow("⚠")} Found ${formatCount(result.discrepancies.length, "discrepancy")}: ${summary}${fixMsg}`, ); } process.exit(result.clean ? 0 : 1); break; } case "reinit": { const targetPath = parsed.positional[0] || "."; const start = Date.now(); const entries = discoverProject(targetPath); console.log( `Regenerating ${formatCount(entries.length, ".pi-map.md file")}...`, ); const client = createClientFromArgs(parsed); await reinitPath(targetPath, { verbose: false, llmClient: client, cacheDir: targetPath }); const elapsed = ((Date.now() - start) / 1000).toFixed(1); console.log(`${pc.green("✓")} Regenerated in ${elapsed}s`); break; } default: console.error(`${pc.red("Error:")} Unknown command "${command}"`); console.error(`Run ${pc.cyan("project-map --help")} for usage.`); process.exit(1); } } main().catch((err) => { if (err instanceof LLMError) { console.error(`${pc.red("LLM Error:")} ${err.message}`); } else { console.error(`${pc.red("Error:")} ${err.message}`); } process.exit(1); });