Pi skill integration, CLI polish, --fix flag, config file support

- SKILL.md: Proper Agent Skills frontmatter with name/description
- pi-extension.ts: Pi extension registering 4 custom tools
  (project_map_init/patch/validate/reinit) with prompt snippets/guidelines
- pi-extension.ts: Auto-detects .pi-map.md files on session start, warns
  about dirty markers, injects maintenance hints before agent start
- package.json: Added pi.extensions and pi.skills entries
- CLI: Added picocolors, clean help screen, progress indicators,
  summary output with timing, colored check/warning icons
- validate.ts: Added --fix flag that regenerates directories with
  discrepancies
- config.ts: Reads .pi-project-map.json from project root with merge
  over defaults
- init.ts: Added optional verbose parameter for programmatic use

All 16 tests pass. TypeScript compiles clean. Build succeeds.
This commit is contained in:
2026-06-09 20:54:59 +02:00
parent ab45859d65
commit 93c2ac60c5
9 changed files with 492 additions and 38 deletions
+108 -8
View File
@@ -3,30 +3,130 @@ 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 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("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`);
}
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 parseValidateArgs(args: string[]): { path: string; fix: boolean } {
let path = ".";
let fix = false;
for (const arg of args.slice(1)) {
if (arg === "--fix") {
fix = true;
} else if (!arg.startsWith("-")) {
path = arg;
}
}
return { path, fix };
}
async function main() {
if (!command || command === "--help" || command === "-h") {
printUsage();
process.exit(0);
}
if (command === "--version" || command === "-v") {
printVersion();
process.exit(0);
}
switch (command) {
case "init":
await initProject(args[1] || ".");
case "init": {
const targetPath = args[1] || ".";
const start = Date.now();
const entries = discoverProject(targetPath);
console.log(`Scanning ${formatCount(entries.length, "directory")}...`);
await initProject(targetPath, { verbose: false });
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":
}
case "patch": {
if (!args[1]) {
console.error(`${pc.red("Error:")} Missing file path. Usage: project-map patch <file>`);
process.exit(1);
}
await patchFile(args[1]);
console.log(`${pc.green("✓")} Patched`);
break;
}
case "validate": {
const result = await validateMaps(args[1] || ".");
const { path, fix } = parseValidateArgs(args);
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":
await reinitPath(args[1] || ".");
case "reinit": {
const targetPath = args[1] || ".";
const start = Date.now();
const entries = discoverProject(targetPath);
console.log(`Regenerating ${formatCount(entries.length, ".pi-map.md file")}...`);
await reinitPath(targetPath, { verbose: false });
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
console.log(`${pc.green("✓")} Regenerated in ${elapsed}s`);
break;
}
default:
console.log(`Usage: project-map <init|patch|validate|reinit> [path]`);
console.error(`${pc.red("Error:")} Unknown command "${command}"`);
console.error(`Run ${pc.cyan("project-map --help")} for usage.`);
process.exit(1);
}
}
main();
main().catch((err) => {
console.error(`${pc.red("Error:")} ${err.message}`);
process.exit(1);
});