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:
@@ -1,24 +1,41 @@
|
||||
---
|
||||
name: pi-project-map
|
||||
description: Generates and maintains hierarchical, machine-readable project analysis files (.pi-map.md) for instant codebase comprehension. Use when working with medium-to-large codebases where understanding architecture, file relationships, and exports without reading every file is valuable. Automatically extracts symbols via AST and LLM heuristics.
|
||||
---
|
||||
|
||||
# pi-project-map
|
||||
|
||||
A Pi skill that generates and maintains a hierarchical, machine-readable analysis of a software project. Each directory gets a `.pi-map.md` file containing architectural context, exported symbols, and dependencies.
|
||||
|
||||
## Tools
|
||||
## What It Does
|
||||
|
||||
### `project-map:init [root]`
|
||||
Runs a full project scan and generates `.pi-map.md` files in every directory.
|
||||
- **Scans** your entire project and creates one `.pi-map.md` per directory
|
||||
- **Extracts** exports, imports, and dependencies via AST parsing (TypeScript, Python, Go) and LLM heuristics
|
||||
- **Updates** incrementally when files change (full rewrite for small packages, section-level patch for large)
|
||||
- **Validates** detects stale entries, missing files, orphaned entries, and changed signatures
|
||||
|
||||
### `project-map:patch <file-path>`
|
||||
Updates the `.pi-map.md` for the directory containing the given file. Uses full rewrite for small packages (< 10 files) or section-level patch for larger packages.
|
||||
## Quick Start
|
||||
|
||||
### `project-map:validate [root]`
|
||||
Checks all `.pi-map.md` files for staleness: missing files, orphaned entries, changed signatures, and dirty markers.
|
||||
```bash
|
||||
# Install globally
|
||||
npm install -g pi-project-map
|
||||
|
||||
### `project-map:reinit [path]`
|
||||
Force full re-initialization of the entire project or a specific subtree. Clears all dirty markers.
|
||||
# Generate analysis files for the entire project
|
||||
project-map init
|
||||
|
||||
# After editing a file, update its directory's analysis
|
||||
project-map patch src/components/Button.tsx
|
||||
|
||||
# Check for staleness
|
||||
project-map validate
|
||||
|
||||
# Force full regeneration
|
||||
project-map reinit
|
||||
```
|
||||
|
||||
## Format
|
||||
|
||||
Each `.pi-map.md` uses dense markdown with conventions:
|
||||
Each `.pi-map.md` uses dense markdown optimized for LLM consumption:
|
||||
|
||||
```markdown
|
||||
# pkg/auth
|
||||
@@ -33,6 +50,60 @@ Guard pattern on routes. Tokens short-lived (15m), refresh long-lived (7d). Rota
|
||||
-
|
||||
```
|
||||
|
||||
### Abbreviations
|
||||
|
||||
| Abbreviation | Meaning |
|
||||
|-------------|---------|
|
||||
| `exp:` | Exported symbols |
|
||||
| `dep:` | Dependencies |
|
||||
| `pkg/` | Internal package reference |
|
||||
|
||||
## Tools
|
||||
|
||||
### `project-map:init [root]`
|
||||
Runs a full project scan and generates `.pi-map.md` files in every directory.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
project-map init
|
||||
project-map init ~/my-project
|
||||
```
|
||||
|
||||
### `project-map:patch <file-path>`
|
||||
Updates the `.pi-map.md` for the directory containing the given file.
|
||||
|
||||
**Behavior:**
|
||||
- Small packages (< 10 files): full rewrite
|
||||
- Large packages (>= 10 files): section-level patch
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
project-map patch src/components/Button.tsx
|
||||
```
|
||||
|
||||
### `project-map:validate [root]`
|
||||
Checks all `.pi-map.md` files for staleness.
|
||||
|
||||
**Detects:**
|
||||
- Missing files (new files not yet in `.pi-map.md`)
|
||||
- Orphaned entries (files listed but deleted)
|
||||
- Stale signatures (exports changed since last scan)
|
||||
- Dirty markers (packages flagged for reconciliation)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
project-map validate
|
||||
```
|
||||
|
||||
### `project-map:reinit [path]`
|
||||
Force full re-initialization. Clears all dirty markers.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
project-map reinit
|
||||
project-map reinit src/components
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Create `.pi-project-map.json` in the project root:
|
||||
@@ -41,16 +112,41 @@ Create `.pi-project-map.json` in the project root:
|
||||
{
|
||||
"ignorePatterns": ["node_modules", ".git"],
|
||||
"smallPackageThreshold": 10,
|
||||
"llmModel": "gpt-4o-mini",
|
||||
"contextBudget": 4000,
|
||||
"autoInjectPrompt": true
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `ignorePatterns` | `node_modules`, `.git`, `dist`, etc. | Additional ignore patterns |
|
||||
| `smallPackageThreshold` | `10` | File count threshold for full rewrite vs patch |
|
||||
| `contextBudget` | `4000` | Max tokens to spend on analysis files |
|
||||
| `autoInjectPrompt` | `true` | Auto-inject maintenance instructions |
|
||||
|
||||
```bash
|
||||
npm install -g pi-project-map
|
||||
```
|
||||
## Agent Instructions
|
||||
|
||||
Then add to your Pi skills configuration.
|
||||
When `.pi-map.md` files exist in the project:
|
||||
|
||||
1. **Read them at session start** to build project understanding without scanning every file
|
||||
2. **Run `project-map:patch <path>`** after editing any source file
|
||||
3. **Run `project-map:validate`** if you suspect staleness before making architectural decisions
|
||||
4. **Trust the analysis** for orientation, but verify critical details by reading source when needed
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Run `project-map:init` after cloning a new repository
|
||||
- Run `project-map:reinit` periodically (daily/weekly) to catch changes made outside the agent
|
||||
- Add `.pi-map.md` to `.gitignore` — they are derived artifacts
|
||||
- For very large projects (> 1000 directories), consider running `init` on subdirectories
|
||||
|
||||
## Supported Languages
|
||||
|
||||
| Language | AST Parsing | Heuristic Extraction |
|
||||
|----------|------------|---------------------|
|
||||
| TypeScript / TSX | Full | Full |
|
||||
| JavaScript / JSX | Full | Full |
|
||||
| Python | Partial | Full |
|
||||
| Go | Partial | Full |
|
||||
| Rust | Partial | Full |
|
||||
| Other | - | Full (filename + regex patterns) |
|
||||
|
||||
Generated
+1
-1
@@ -10,6 +10,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ignore": "^5.3.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"tree-sitter": "^0.21.0",
|
||||
"tree-sitter-go": "^0.25.0",
|
||||
"tree-sitter-python": "^0.25.0",
|
||||
@@ -2860,7 +2861,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
|
||||
@@ -23,6 +23,14 @@
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"pi": {
|
||||
"extensions": [
|
||||
"./pi-extension.ts"
|
||||
],
|
||||
"skills": [
|
||||
"./SKILL.md"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
@@ -34,6 +42,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"ignore": "^5.3.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"tree-sitter": "^0.21.0",
|
||||
"tree-sitter-go": "^0.25.0",
|
||||
"tree-sitter-python": "^0.25.0",
|
||||
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
import { execSync } from "child_process";
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
function runCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
cwd: string,
|
||||
): { stdout: string; stderr: string; success: boolean } {
|
||||
try {
|
||||
const result = execSync(`npx project-map ${command} ${args.join(" ")}`, {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
return { stdout: result, stderr: "", success: true };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
stdout: error.stdout || "",
|
||||
stderr: error.stderr || error.message,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function findPiMapFiles(cwd: string): string[] {
|
||||
try {
|
||||
const result = execSync('find . -name ".pi-map.md" -type f', {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
});
|
||||
return result.trim().split("\n").filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
// Register custom tools
|
||||
pi.registerTool({
|
||||
name: "project_map_init",
|
||||
label: "Project Map Init",
|
||||
description:
|
||||
"Generate .pi-map.md analysis files for the entire project or a subdirectory",
|
||||
promptSnippet:
|
||||
"Initialize project analysis files for codebase understanding",
|
||||
promptGuidelines: [
|
||||
"Use project_map_init when starting work on a new project or after significant restructuring",
|
||||
"Run project_map_init when .pi-map.md files are missing or severely outdated",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
path: Type.Optional(
|
||||
Type.String({
|
||||
description: "Project root path (default: current directory)",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const targetPath = params.path || ctx.cwd;
|
||||
const result = runCommand("init", targetPath === ctx.cwd ? [] : [targetPath], ctx.cwd);
|
||||
return {
|
||||
content: [{ type: "text", text: result.stdout || result.stderr }],
|
||||
details: { success: result.success, cwd: ctx.cwd },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "project_map_patch",
|
||||
label: "Project Map Patch",
|
||||
description: "Update .pi-map.md for the directory containing a changed file",
|
||||
promptSnippet: "Update project analysis after editing a source file",
|
||||
promptGuidelines: [
|
||||
"Use project_map_patch immediately after editing any source file",
|
||||
"Pass the absolute or relative path of the modified file",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
file_path: Type.String({
|
||||
description: "Path to the modified file",
|
||||
}),
|
||||
}),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const result = runCommand("patch", [params.file_path], ctx.cwd);
|
||||
return {
|
||||
content: [{ type: "text", text: result.stdout || result.stderr }],
|
||||
details: { success: result.success },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "project_map_validate",
|
||||
label: "Project Map Validate",
|
||||
description: "Check all .pi-map.md files for staleness and discrepancies",
|
||||
promptSnippet: "Validate project analysis files for accuracy",
|
||||
promptGuidelines: [
|
||||
"Use project_map_validate before making architectural decisions if you suspect stale data",
|
||||
"Use project_map_validate to detect files that were deleted or added outside the agent",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
path: Type.Optional(
|
||||
Type.String({
|
||||
description: "Project root path (default: current directory)",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const targetPath = params.path || ctx.cwd;
|
||||
const result = runCommand("validate", targetPath === ctx.cwd ? [] : [targetPath], ctx.cwd);
|
||||
return {
|
||||
content: [{ type: "text", text: result.stdout || result.stderr }],
|
||||
details: { success: result.success, clean: result.stdout.includes("clean") },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "project_map_reinit",
|
||||
label: "Project Map Reinit",
|
||||
description: "Force full regeneration of all .pi-map.md files",
|
||||
promptSnippet: "Force full regeneration of project analysis files",
|
||||
promptGuidelines: [
|
||||
"Use project_map_reinit when validation shows widespread staleness",
|
||||
"Use project_map_reinit after pulling major changes from version control",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
path: Type.Optional(
|
||||
Type.String({
|
||||
description: "Path to regenerate (default: entire project)",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const args = params.path ? [params.path] : [];
|
||||
const result = runCommand("reinit", args, ctx.cwd);
|
||||
return {
|
||||
content: [{ type: "text", text: result.stdout || result.stderr }],
|
||||
details: { success: result.success },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Auto-load .pi-map.md files on session start
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
const mapFiles = findPiMapFiles(ctx.cwd);
|
||||
if (mapFiles.length === 0) return;
|
||||
|
||||
// Check for dirty markers
|
||||
const dirtyFiles = mapFiles.filter((f) => {
|
||||
try {
|
||||
const content = readFileSync(join(ctx.cwd, f), "utf8");
|
||||
return content.includes("## dirty") && !content.includes("## dirty\n-");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (dirtyFiles.length > 0) {
|
||||
ctx.ui.notify(
|
||||
`pi-project-map: ${dirtyFiles.length} dirty packages detected. Run project_map_validate or project_map_reinit.`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Inject maintenance instructions before agent starts
|
||||
pi.on("before_agent_start", async (_event, _ctx) => {
|
||||
// Only inject if .pi-map.md files exist
|
||||
const mapFiles = findPiMapFiles(_ctx.cwd);
|
||||
if (mapFiles.length === 0) return {};
|
||||
|
||||
return {
|
||||
message: {
|
||||
customType: "pi-project-map-hint",
|
||||
content:
|
||||
"📋 Project map active: If you modify any source file, run `project_map_patch` with the file path. If you suspect staleness, run `project_map_validate`.",
|
||||
display: false, // Don't show in UI, only in LLM context
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
+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;
|
||||
}
|
||||
|
||||
+11
-3
@@ -10,15 +10,20 @@ 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);
|
||||
}
|
||||
|
||||
if (options?.verbose !== false) {
|
||||
console.log(`Generated ${entries.length} .pi-map.md files`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateDirectoryMap(
|
||||
entry: DirectoryEntry,
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,6 +131,11 @@ export async function validateMaps(
|
||||
discrepancies,
|
||||
};
|
||||
|
||||
if (fix) {
|
||||
result.fixed = fixed;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
if (result.clean) {
|
||||
console.log("All .pi-map.md files are clean.");
|
||||
} else {
|
||||
@@ -111,6 +144,10 @@ export async function validateMaps(
|
||||
console.log(` [${d.type}] ${d.path}: ${d.message}`);
|
||||
}
|
||||
}
|
||||
if (fix && fixed > 0) {
|
||||
console.log(`Fixed ${fixed} director${fixed === 1 ? "y" : "ies"}.`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,5 +15,5 @@
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist", "pi-extension.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user