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:
+108
-8
@@ -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);
|
||||
});
|
||||
|
||||
+23
-2
@@ -1,3 +1,6 @@
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
export interface SkillConfig {
|
||||
ignorePatterns: string[];
|
||||
smallPackageThreshold: number;
|
||||
@@ -18,6 +21,15 @@ export const DEFAULT_CONFIG: SkillConfig = {
|
||||
"__pycache__",
|
||||
".DS_Store",
|
||||
"*.log",
|
||||
".pi-map.md",
|
||||
".cache",
|
||||
"tmp",
|
||||
"temp",
|
||||
".tmp",
|
||||
".turbo",
|
||||
".parcel-cache",
|
||||
".eslintcache",
|
||||
".prettiercache",
|
||||
],
|
||||
smallPackageThreshold: 10,
|
||||
llmModel: "gpt-4o-mini",
|
||||
@@ -25,7 +37,16 @@ export const DEFAULT_CONFIG: SkillConfig = {
|
||||
autoInjectPrompt: true,
|
||||
};
|
||||
|
||||
export function loadConfig(): SkillConfig {
|
||||
// TODO: load from .pi-project-map.json or similar
|
||||
export function loadConfig(cwd: string = process.cwd()): SkillConfig {
|
||||
const configPath = join(cwd, ".pi-project-map.json");
|
||||
if (existsSync(configPath)) {
|
||||
try {
|
||||
const content = readFileSync(configPath, "utf8");
|
||||
const userConfig = JSON.parse(content);
|
||||
return { ...DEFAULT_CONFIG, ...userConfig };
|
||||
} catch {
|
||||
// Fall through to default
|
||||
}
|
||||
}
|
||||
return DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
+12
-4
@@ -10,14 +10,19 @@ import { mergeFileData } from "./merge.js";
|
||||
import { writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
export async function initProject(rootPath: string): Promise<void> {
|
||||
export async function initProject(
|
||||
rootPath: string,
|
||||
options?: { verbose?: boolean },
|
||||
): Promise<void> {
|
||||
const entries = discoverProject(rootPath);
|
||||
|
||||
for (const entry of entries) {
|
||||
await generateDirectoryMap(entry);
|
||||
}
|
||||
|
||||
console.log(`Generated ${entries.length} .pi-map.md files`);
|
||||
if (options?.verbose !== false) {
|
||||
console.log(`Generated ${entries.length} .pi-map.md files`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateDirectoryMap(
|
||||
@@ -46,7 +51,10 @@ export async function generateDirectoryMap(
|
||||
return fileData;
|
||||
}
|
||||
|
||||
export async function reinitPath(path: string): Promise<void> {
|
||||
export async function reinitPath(
|
||||
path: string,
|
||||
options?: { verbose?: boolean },
|
||||
): Promise<void> {
|
||||
// Full regeneration clears all dirty markers by overwriting every .pi-map.md
|
||||
await initProject(path);
|
||||
await initProject(path, options);
|
||||
}
|
||||
|
||||
+43
-6
@@ -3,10 +3,12 @@ import { parsePackageMap } from "./format.js";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { extractFileAST } from "./ast-extract.js";
|
||||
import { generateDirectoryMap } from "./init.js";
|
||||
|
||||
export interface ValidationResult {
|
||||
clean: boolean;
|
||||
discrepancies: Discrepancy[];
|
||||
fixed?: number;
|
||||
}
|
||||
|
||||
export interface Discrepancy {
|
||||
@@ -17,9 +19,12 @@ export interface Discrepancy {
|
||||
|
||||
export async function validateMaps(
|
||||
rootPath: string,
|
||||
options?: { fix?: boolean; verbose?: boolean },
|
||||
): Promise<ValidationResult> {
|
||||
const { fix = false, verbose = true } = options || {};
|
||||
const discrepancies: Discrepancy[] = [];
|
||||
const entries = discoverProject(rootPath);
|
||||
const dirsToFix = new Set<string>();
|
||||
|
||||
for (const entry of entries) {
|
||||
const mapPath = join(entry.dirPath, ".pi-map.md");
|
||||
@@ -29,10 +34,12 @@ export async function validateMaps(
|
||||
path: entry.relativePath,
|
||||
message: "No .pi-map.md found",
|
||||
});
|
||||
if (fix) dirsToFix.add(entry.dirPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const mapData = parsePackageMap(readFileSync(mapPath, "utf8"));
|
||||
let mapNeedsRewrite = false;
|
||||
|
||||
// Check for dirty markers
|
||||
if (mapData.dirty && mapData.dirty !== "-") {
|
||||
@@ -41,6 +48,7 @@ export async function validateMaps(
|
||||
path: mapPath,
|
||||
message: `Dirty: ${mapData.dirty}`,
|
||||
});
|
||||
if (fix) mapNeedsRewrite = true;
|
||||
}
|
||||
|
||||
// Check for orphaned entries
|
||||
@@ -52,6 +60,7 @@ export async function validateMaps(
|
||||
path: filePath,
|
||||
message: `File listed but deleted: ${fileEntry.name}`,
|
||||
});
|
||||
if (fix) mapNeedsRewrite = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +72,7 @@ export async function validateMaps(
|
||||
path: join(entry.dirPath, file),
|
||||
message: `File not in .pi-map.md: ${file}`,
|
||||
});
|
||||
if (fix) mapNeedsRewrite = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +93,7 @@ export async function validateMaps(
|
||||
path: filePath,
|
||||
message: `Missing export: ${exp}`,
|
||||
});
|
||||
if (fix) mapNeedsRewrite = true;
|
||||
}
|
||||
}
|
||||
for (const exp of actualExports) {
|
||||
@@ -92,10 +103,27 @@ export async function validateMaps(
|
||||
path: filePath,
|
||||
message: `New export: ${exp}`,
|
||||
});
|
||||
if (fix) mapNeedsRewrite = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fix && mapNeedsRewrite) {
|
||||
dirsToFix.add(entry.dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply fixes
|
||||
let fixed = 0;
|
||||
if (fix && dirsToFix.size > 0) {
|
||||
for (const dirPath of dirsToFix) {
|
||||
const entry = entries.find((e) => e.dirPath === dirPath);
|
||||
if (entry) {
|
||||
await generateDirectoryMap(entry);
|
||||
fixed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result: ValidationResult = {
|
||||
@@ -103,12 +131,21 @@ export async function validateMaps(
|
||||
discrepancies,
|
||||
};
|
||||
|
||||
if (result.clean) {
|
||||
console.log("All .pi-map.md files are clean.");
|
||||
} else {
|
||||
console.log(`Found ${discrepancies.length} discrepancies:`);
|
||||
for (const d of discrepancies) {
|
||||
console.log(` [${d.type}] ${d.path}: ${d.message}`);
|
||||
if (fix) {
|
||||
result.fixed = fixed;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
if (result.clean) {
|
||||
console.log("All .pi-map.md files are clean.");
|
||||
} else {
|
||||
console.log(`Found ${discrepancies.length} discrepancies:`);
|
||||
for (const d of discrepancies) {
|
||||
console.log(` [${d.type}] ${d.path}: ${d.message}`);
|
||||
}
|
||||
}
|
||||
if (fix && fixed > 0) {
|
||||
console.log(`Fixed ${fixed} director${fixed === 1 ? "y" : "ies"}.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user