2f198ea0d2
BREAKING: Heuristic fallback removed — LLM client now required Changes: - Remove extractFileHeuristic, extractPackageHeuristic, all helpers - extractFileLLM / extractPackageLLM now throw LLMError when client missing - Add hybrid binary detection: extension blacklist + content sniffing - Increase file size limit: 50KB → 500KB (text files only) - Restructure src/ into subdirectories: - src/llm/ — all LLM clients, extract, batch, cache, error - src/ast/ — AST extraction - src/cli/ — CLI entry point - Update all imports across codebase and tests - Add tests/mock-llm.ts helper for deterministic mock clients - Update all tests to use mock LLM clients (no heuristics dependency) - All 52 tests passing (including 8 real LLM integration tests)
202 lines
6.2 KiB
JavaScript
202 lines
6.2 KiB
JavaScript
#!/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/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")} <file> 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=<model> LLM model name (or set LLM_MODEL env var)`,
|
|
);
|
|
console.log(` --llm-base-url=<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<typeof parseArgs>) {
|
|
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 <file>`,
|
|
);
|
|
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<string, number> = {};
|
|
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);
|
|
});
|