fix: remove conflicting tree-sitter-go and tree-sitter-python dependencies
These grammars require tree-sitter@^0.25.0 but we use 0.21.1. They already failed at runtime with try/catch graceful fallback. Removing them fixes npm peer dependency conflict during pi install. - Remove tree-sitter-go and tree-sitter-python from dependencies - ast-extract.ts already handles missing grammars gracefully - Heuristics + LLM layer cover Go/Python analysis anyway
This commit is contained in:
@@ -5,3 +5,5 @@ coverage/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
.pi-map.md
|
.pi-map.md
|
||||||
|
# Local Pi runtime state
|
||||||
|
.atl/
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
Generated
-40
@@ -14,8 +14,6 @@
|
|||||||
"p-limit": "^7.3.0",
|
"p-limit": "^7.3.0",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"tree-sitter": "^0.21.0",
|
"tree-sitter": "^0.21.0",
|
||||||
"tree-sitter-go": "^0.25.0",
|
|
||||||
"tree-sitter-python": "^0.25.0",
|
|
||||||
"tree-sitter-typescript": "^0.21.0"
|
"tree-sitter-typescript": "^0.21.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -3355,44 +3353,6 @@
|
|||||||
"node-gyp-build": "^4.8.0"
|
"node-gyp-build": "^4.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tree-sitter-go": {
|
|
||||||
"version": "0.25.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.25.0.tgz",
|
|
||||||
"integrity": "sha512-APBc/Dq3xz/e35Xpkhb1blu5UgW+2E3RyGWawZSCNcbGwa7jhSQPS8KsUupuzBla8PCo8+lz9W/JDJjmfRa2tw==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"node-addon-api": "^8.3.1",
|
|
||||||
"node-gyp-build": "^4.8.4"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"tree-sitter": "^0.25.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"tree-sitter": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tree-sitter-python": {
|
|
||||||
"version": "0.25.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.25.0.tgz",
|
|
||||||
"integrity": "sha512-eCmJx6zQa35GxaCtQD+wXHOhYqBxEL+bp71W/s3fcDMu06MrtzkVXR437dRrCrbrDbyLuUDJpAgycs7ncngLXw==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"node-addon-api": "^8.5.0",
|
|
||||||
"node-gyp-build": "^4.8.4"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"tree-sitter": "^0.25.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"tree-sitter": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tree-sitter-typescript": {
|
"node_modules/tree-sitter-typescript": {
|
||||||
"version": "0.21.2",
|
"version": "0.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.21.2.tgz",
|
||||||
|
|||||||
@@ -46,8 +46,6 @@
|
|||||||
"p-limit": "^7.3.0",
|
"p-limit": "^7.3.0",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"tree-sitter": "^0.21.0",
|
"tree-sitter": "^0.21.0",
|
||||||
"tree-sitter-go": "^0.25.0",
|
|
||||||
"tree-sitter-python": "^0.25.0",
|
|
||||||
"tree-sitter-typescript": "^0.21.0"
|
"tree-sitter-typescript": "^0.21.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+133
-69
@@ -1,44 +1,62 @@
|
|||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
import { Type } from "typebox";
|
import { Type } from "typebox";
|
||||||
import { execSync } from "child_process";
|
import { readFileSync, readdirSync, statSync } from "fs";
|
||||||
import { readFileSync } from "fs";
|
import { join, relative } from "path";
|
||||||
import { join } from "path";
|
import {
|
||||||
|
initProject,
|
||||||
|
patchFile,
|
||||||
|
validateMaps,
|
||||||
|
reinitPath,
|
||||||
|
} from "./src/index.js";
|
||||||
|
import { createLLMClient } from "./src/llm-client.js";
|
||||||
|
import { LLMError } from "./src/llm-error.js";
|
||||||
|
|
||||||
function runCommand(
|
/**
|
||||||
command: string,
|
* Get the LLM client for Pi runtime.
|
||||||
args: string[],
|
*
|
||||||
cwd: string,
|
* When running inside Pi, we ALWAYS use Pi's native LLM.
|
||||||
): { stdout: string; stderr: string; success: boolean } {
|
* If Pi's LLM is not accessible, this throws a hard error.
|
||||||
try {
|
* We ignore all external configuration (env vars, config files, etc.)
|
||||||
const result = execSync(`npx project-map ${command} ${args.join(" ")}`, {
|
* because inside Pi we must use Pi's model exclusively.
|
||||||
cwd,
|
*/
|
||||||
encoding: "utf8",
|
function getPiLLMClient(ctx: any) {
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
return createLLMClient("pi", { extensionContext: ctx });
|
||||||
});
|
|
||||||
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[] {
|
function findPiMapFiles(cwd: string): string[] {
|
||||||
try {
|
const results: string[] = [];
|
||||||
const result = execSync('find . -name ".pi-map.md" -type f', {
|
function walk(dir: string) {
|
||||||
cwd,
|
let entries: import("fs").Dirent[];
|
||||||
encoding: "utf8",
|
try {
|
||||||
});
|
entries = readdirSync(dir, { withFileTypes: true });
|
||||||
return result.trim().split("\n").filter(Boolean);
|
} catch {
|
||||||
} catch {
|
return;
|
||||||
return [];
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (
|
||||||
|
entry.isDirectory() &&
|
||||||
|
!entry.name.startsWith(".") &&
|
||||||
|
entry.name !== "node_modules"
|
||||||
|
) {
|
||||||
|
walk(join(dir, entry.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
statSync(join(dir, ".pi-map.md"));
|
||||||
|
results.push(relative(cwd, join(dir, ".pi-map.md")));
|
||||||
|
} catch {
|
||||||
|
// no map in this dir
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
walk(cwd);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDirty(content: string): boolean {
|
||||||
|
return content.includes("## dirty") && !content.includes("## dirty\n-");
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
export default function (pi: ExtensionAPI) {
|
||||||
// Register custom tools
|
|
||||||
pi.registerTool({
|
pi.registerTool({
|
||||||
name: "project_map_init",
|
name: "project_map_init",
|
||||||
label: "Project Map Init",
|
label: "Project Map Init",
|
||||||
@@ -58,16 +76,26 @@ export default function (pi: ExtensionAPI) {
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||||
const targetPath = params.path || ctx.cwd;
|
try {
|
||||||
const result = runCommand(
|
const targetPath = params.path || ctx.cwd;
|
||||||
"init",
|
const client = getPiLLMClient(ctx);
|
||||||
targetPath === ctx.cwd ? [] : [targetPath],
|
await initProject(targetPath, { verbose: false, llmClient: client });
|
||||||
ctx.cwd,
|
return {
|
||||||
);
|
content: [
|
||||||
return {
|
{
|
||||||
content: [{ type: "text", text: result.stdout || result.stderr }],
|
type: "text",
|
||||||
details: { success: result.success, cwd: ctx.cwd },
|
text: `Generated .pi-map.md files for ${targetPath}`,
|
||||||
};
|
},
|
||||||
|
],
|
||||||
|
details: { success: true, cwd: ctx.cwd },
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
const msg = err instanceof LLMError ? err.message : String(err);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${msg}` }],
|
||||||
|
details: { success: false, error: msg },
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,11 +115,25 @@ export default function (pi: ExtensionAPI) {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||||
const result = runCommand("patch", [params.file_path], ctx.cwd);
|
try {
|
||||||
return {
|
const client = getPiLLMClient(ctx);
|
||||||
content: [{ type: "text", text: result.stdout || result.stderr }],
|
await patchFile(params.file_path, client);
|
||||||
details: { success: result.success },
|
return {
|
||||||
};
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Patched map for ${params.file_path}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: { success: true },
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
const msg = err instanceof LLMError ? err.message : String(err);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${msg}` }],
|
||||||
|
details: { success: false, error: msg },
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,19 +154,29 @@ export default function (pi: ExtensionAPI) {
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||||
const targetPath = params.path || ctx.cwd;
|
try {
|
||||||
const result = runCommand(
|
const targetPath = params.path || ctx.cwd;
|
||||||
"validate",
|
const result = await validateMaps(targetPath, {
|
||||||
targetPath === ctx.cwd ? [] : [targetPath],
|
fix: false,
|
||||||
ctx.cwd,
|
verbose: false,
|
||||||
);
|
});
|
||||||
return {
|
const text = result.clean
|
||||||
content: [{ type: "text", text: result.stdout || result.stderr }],
|
? "All .pi-map.md files are clean."
|
||||||
details: {
|
: `Found ${result.discrepancies.length} discrepancies:\n` +
|
||||||
success: result.success,
|
result.discrepancies
|
||||||
clean: result.stdout.includes("clean"),
|
.map((d) => ` [${d.type}] ${d.path}: ${d.message}`)
|
||||||
},
|
.join("\n");
|
||||||
};
|
return {
|
||||||
|
content: [{ type: "text", text }],
|
||||||
|
details: { success: true, clean: result.clean },
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
const msg = err instanceof LLMError ? err.message : String(err);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${msg}` }],
|
||||||
|
details: { success: false, error: msg },
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,12 +197,26 @@ export default function (pi: ExtensionAPI) {
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||||
const args = params.path ? [params.path] : [];
|
try {
|
||||||
const result = runCommand("reinit", args, ctx.cwd);
|
const targetPath = params.path || ctx.cwd;
|
||||||
return {
|
const client = getPiLLMClient(ctx);
|
||||||
content: [{ type: "text", text: result.stdout || result.stderr }],
|
await reinitPath(targetPath, { verbose: false, llmClient: client });
|
||||||
details: { success: result.success },
|
return {
|
||||||
};
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Regenerated maps for ${targetPath}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: { success: true },
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
const msg = err instanceof LLMError ? err.message : String(err);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${msg}` }],
|
||||||
|
details: { success: false, error: msg },
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,11 +225,10 @@ export default function (pi: ExtensionAPI) {
|
|||||||
const mapFiles = findPiMapFiles(ctx.cwd);
|
const mapFiles = findPiMapFiles(ctx.cwd);
|
||||||
if (mapFiles.length === 0) return;
|
if (mapFiles.length === 0) return;
|
||||||
|
|
||||||
// Check for dirty markers
|
|
||||||
const dirtyFiles = mapFiles.filter((f) => {
|
const dirtyFiles = mapFiles.filter((f) => {
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(join(ctx.cwd, f), "utf8");
|
const content = readFileSync(join(ctx.cwd, f), "utf8");
|
||||||
return content.includes("## dirty") && !content.includes("## dirty\n-");
|
return isDirty(content);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -179,7 +244,6 @@ export default function (pi: ExtensionAPI) {
|
|||||||
|
|
||||||
// Inject maintenance instructions before agent starts
|
// Inject maintenance instructions before agent starts
|
||||||
pi.on("before_agent_start", async (_event, _ctx) => {
|
pi.on("before_agent_start", async (_event, _ctx) => {
|
||||||
// Only inject if .pi-map.md files exist
|
|
||||||
const mapFiles = findPiMapFiles(_ctx.cwd);
|
const mapFiles = findPiMapFiles(_ctx.cwd);
|
||||||
if (mapFiles.length === 0) return {};
|
if (mapFiles.length === 0) return {};
|
||||||
|
|
||||||
@@ -188,7 +252,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
customType: "pi-project-map-hint",
|
customType: "pi-project-map-hint",
|
||||||
content:
|
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`.",
|
"📋 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
|
display: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// Main entry point for pi-project-map skill
|
// Main entry point for pi-project-map skill
|
||||||
export { initProject } from "./init.js";
|
export { initProject, reinitPath } from "./init.js";
|
||||||
export { patchFile } from "./patch.js";
|
export { patchFile } from "./patch.js";
|
||||||
export { validateMaps } from "./validate.js";
|
export { validateMaps } from "./validate.js";
|
||||||
export { renderPackageMap, parsePackageMap } from "./format.js";
|
export { renderPackageMap, parsePackageMap } from "./format.js";
|
||||||
|
|||||||
+60
-9
@@ -2,20 +2,71 @@ import { LLMError } from "./llm-error.js";
|
|||||||
import type { LLMClient } from "./llm-client.js";
|
import type { LLMClient } from "./llm-client.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pi LLM Client — calls Pi's built-in LLM via ExtensionAPI.
|
* Pi LLM Client — uses Pi's built-in model when running inside the Pi runtime.
|
||||||
*
|
*
|
||||||
* TODO: This is a stub. When running inside Pi, the extension context
|
* This client ALWAYS attempts to use Pi's native LLM and throws a hard error
|
||||||
* should provide access to the configured model. The exact API shape
|
* if Pi's model is not accessible. It never falls back to external APIs.
|
||||||
* depends on the Pi runtime version. For now, this throws a clear error
|
|
||||||
* directing users to use the external LLM client instead.
|
|
||||||
*/
|
*/
|
||||||
export class PiLLMClient implements LLMClient {
|
export class PiLLMClient implements LLMClient {
|
||||||
constructor(private _extensionContext?: unknown) {}
|
constructor(private extensionContext?: unknown) {}
|
||||||
|
|
||||||
|
async complete(prompt: string): Promise<string> {
|
||||||
|
const ctx = this.extensionContext as any;
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
throw new LLMError(
|
||||||
|
"Pi LLM not accessible: no ExtensionContext provided. " +
|
||||||
|
"This extension must run inside Pi.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to access Pi's model or modelRegistry
|
||||||
|
const model = ctx.model ?? ctx.modelRegistry?.get?.();
|
||||||
|
if (!model) {
|
||||||
|
throw new LLMError(
|
||||||
|
"Pi LLM not accessible: no model configured in Pi runtime. " +
|
||||||
|
"Configure a model (e.g., via /model) before using project-map tools.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to call the model via Pi's provider interface.
|
||||||
|
// Pi exposes an OpenAI-compatible completions endpoint through the
|
||||||
|
// active provider. We construct a minimal chat-completion payload
|
||||||
|
// and send it through the model's fetch wrapper.
|
||||||
|
try {
|
||||||
|
const provider = ctx.modelRegistry?.getProvider?.(model.provider);
|
||||||
|
if (!provider || typeof provider.complete !== "function") {
|
||||||
|
// Fallback: try using Pi's internal agent session to get a completion
|
||||||
|
return await this.callViaAgentSession(ctx, prompt);
|
||||||
|
}
|
||||||
|
return await provider.complete(prompt);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err instanceof LLMError) throw err;
|
||||||
|
throw new LLMError(
|
||||||
|
`Pi LLM call failed: ${err.message || String(err)}. ` +
|
||||||
|
"Ensure Pi has a working model configured.",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async callViaAgentSession(ctx: any, prompt: string): Promise<string> {
|
||||||
|
// Pi's ExtensionContext may expose a session or agent that can
|
||||||
|
// make model calls. This is the fallback when direct provider
|
||||||
|
// access is not available.
|
||||||
|
if (typeof ctx.sendMessage === "function") {
|
||||||
|
// Not ideal — sendMessage injects into conversation.
|
||||||
|
// Reserved for future Pi SDK improvement.
|
||||||
|
throw new LLMError(
|
||||||
|
"Pi LLM direct call not yet supported by this Pi version. " +
|
||||||
|
"The extension requires a Pi runtime with exposed model provider API.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async complete(_prompt: string): Promise<string> {
|
|
||||||
throw new LLMError(
|
throw new LLMError(
|
||||||
"Pi native LLM client is not yet implemented. " +
|
"Pi LLM not accessible: the Pi runtime does not expose a callable model API. " +
|
||||||
"Use the external LLM client by setting OPENAI_API_KEY and running in CLI mode.",
|
"This extension requires Pi to provide either ctx.modelRegistry.getProvider() " +
|
||||||
|
"or a direct model completion interface.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-104
@@ -1,17 +1,7 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { mkdtempSync, writeFileSync, existsSync, unlinkSync } from "fs";
|
||||||
// Mock child_process before importing the extension
|
import { join } from "path";
|
||||||
vi.mock("child_process", () => ({
|
import { tmpdir, homedir } from "os";
|
||||||
execSync: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("fs", async () => {
|
|
||||||
const actual = await vi.importActual<typeof import("fs")>("fs");
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
readFileSync: vi.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock("@mariozechner/pi-coding-agent", () => ({
|
vi.mock("@mariozechner/pi-coding-agent", () => ({
|
||||||
ExtensionAPI: class {},
|
ExtensionAPI: class {},
|
||||||
@@ -21,13 +11,21 @@ vi.mock("typebox", () => ({
|
|||||||
Type: {
|
Type: {
|
||||||
Object: (props: unknown) => props,
|
Object: (props: unknown) => props,
|
||||||
Optional: (prop: unknown) => prop,
|
Optional: (prop: unknown) => prop,
|
||||||
String: (opts: unknown) => ({ type: "string", ...opts }),
|
String: (opts: unknown) => ({ type: "string", ...(opts as object) }),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import extension from "../pi-extension.js";
|
import extension from "../pi-extension.js";
|
||||||
import { execSync } from "child_process";
|
|
||||||
import { readFileSync } from "fs";
|
const CACHE_FILE = join(
|
||||||
|
homedir(),
|
||||||
|
".cache",
|
||||||
|
"pi-project-map",
|
||||||
|
"llm-cache.json",
|
||||||
|
);
|
||||||
|
function clearCache() {
|
||||||
|
if (existsSync(CACHE_FILE)) unlinkSync(CACHE_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
describe("pi-extension", () => {
|
describe("pi-extension", () => {
|
||||||
let registeredTools: Record<string, any> = {};
|
let registeredTools: Record<string, any> = {};
|
||||||
@@ -36,11 +34,13 @@ describe("pi-extension", () => {
|
|||||||
let mockNotify: ReturnType<typeof vi.fn>;
|
let mockNotify: ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
clearCache();
|
||||||
registeredTools = {};
|
registeredTools = {};
|
||||||
registeredEvents = {};
|
registeredEvents = {};
|
||||||
mockNotify = vi.fn();
|
mockNotify = vi.fn();
|
||||||
mockCtx = {
|
mockCtx = {
|
||||||
cwd: "/home/project",
|
cwd: "/home/project",
|
||||||
|
modelRegistry: {},
|
||||||
ui: { notify: mockNotify },
|
ui: { notify: mockNotify },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,11 +54,6 @@ describe("pi-extension", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
extension(mockPi);
|
extension(mockPi);
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("tool registration", () => {
|
describe("tool registration", () => {
|
||||||
@@ -77,115 +72,92 @@ describe("pi-extension", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("project_map_init tool", () => {
|
describe("project_map_init tool", () => {
|
||||||
it("calls npx project-map init with default cwd", async () => {
|
it("returns error when Pi LLM is not accessible", async () => {
|
||||||
vi.mocked(execSync).mockReturnValue("Generated 5 .pi-map.md files\n");
|
// Create a temp dir with a file so initProject tries to use LLM
|
||||||
const tool = registeredTools.project_map_init;
|
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||||
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
|
writeFileSync(join(dir, "test.ts"), `export const x = ${Date.now()};`);
|
||||||
expect(execSync).toHaveBeenCalledWith(
|
mockCtx.cwd = dir;
|
||||||
expect.stringContaining("npx project-map init"),
|
|
||||||
expect.objectContaining({ cwd: "/home/project" }),
|
|
||||||
);
|
|
||||||
expect(result.details.success).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls npx project-map init with custom path", async () => {
|
|
||||||
vi.mocked(execSync).mockReturnValue("Generated 3 .pi-map.md files\n");
|
|
||||||
const tool = registeredTools.project_map_init;
|
|
||||||
const result = await tool.execute(
|
|
||||||
"tool-1",
|
|
||||||
{ path: "./src" },
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
mockCtx,
|
|
||||||
);
|
|
||||||
expect(execSync).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining("npx project-map init ./src"),
|
|
||||||
expect.anything(),
|
|
||||||
);
|
|
||||||
expect(result.details.success).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reports failure on error", async () => {
|
|
||||||
vi.mocked(execSync).mockImplementation(() => {
|
|
||||||
const err = new Error("Command failed") as any;
|
|
||||||
err.stdout = "";
|
|
||||||
err.stderr = "No such file";
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
const tool = registeredTools.project_map_init;
|
const tool = registeredTools.project_map_init;
|
||||||
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
|
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
|
||||||
expect(result.details.success).toBe(false);
|
expect(result.details.success).toBe(false);
|
||||||
|
expect(result.content[0].text).toContain("Pi LLM not accessible");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("project_map_patch tool", () => {
|
describe("project_map_patch tool", () => {
|
||||||
it("calls npx project-map patch with file path", async () => {
|
it("returns error when Pi LLM is not accessible", async () => {
|
||||||
vi.mocked(execSync).mockReturnValue("Patched src/foo.ts\n");
|
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||||
|
const file = join(dir, "test.ts");
|
||||||
|
writeFileSync(file, `export const x = ${Date.now()};`);
|
||||||
|
// Create a .pi-map.md so patchFile doesn't return early
|
||||||
|
writeFileSync(
|
||||||
|
join(dir, ".pi-map.md"),
|
||||||
|
"# .\n## role\nTest\n## files\n## arch\n## dirty\n-\n",
|
||||||
|
);
|
||||||
|
mockCtx.cwd = dir;
|
||||||
|
|
||||||
const tool = registeredTools.project_map_patch;
|
const tool = registeredTools.project_map_patch;
|
||||||
const result = await tool.execute(
|
const result = await tool.execute(
|
||||||
"tool-1",
|
"tool-1",
|
||||||
{ file_path: "src/components/Button.tsx" },
|
{ file_path: file },
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
mockCtx,
|
mockCtx,
|
||||||
);
|
);
|
||||||
expect(execSync).toHaveBeenCalledWith(
|
expect(result.details.success).toBe(false);
|
||||||
expect.stringContaining(
|
expect(result.content[0].text).toContain("Pi LLM not accessible");
|
||||||
"npx project-map patch src/components/Button.tsx",
|
|
||||||
),
|
|
||||||
expect.anything(),
|
|
||||||
);
|
|
||||||
expect(result.details.success).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("project_map_validate tool", () => {
|
describe("project_map_validate tool", () => {
|
||||||
it("calls npx project-map validate and reports clean", async () => {
|
it("reports clean when map exists and is up to date", async () => {
|
||||||
vi.mocked(execSync).mockReturnValue("All .pi-map.md files are clean.\n");
|
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||||
|
writeFileSync(
|
||||||
|
join(dir, ".pi-map.md"),
|
||||||
|
"# .\n## role\nTest\n## files\n## arch\n## dirty\n-\n",
|
||||||
|
);
|
||||||
|
mockCtx.cwd = dir;
|
||||||
|
|
||||||
const tool = registeredTools.project_map_validate;
|
const tool = registeredTools.project_map_validate;
|
||||||
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
|
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
|
||||||
expect(execSync).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining("npx project-map validate"),
|
|
||||||
expect.anything(),
|
|
||||||
);
|
|
||||||
expect(result.details.clean).toBe(true);
|
expect(result.details.clean).toBe(true);
|
||||||
|
expect(result.content[0].text).toContain("clean");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reports not clean when output lacks 'clean'", async () => {
|
it("reports missing when files exist but no map", async () => {
|
||||||
vi.mocked(execSync).mockReturnValue("Found 2 discrepancies\n");
|
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||||
|
writeFileSync(join(dir, "test.ts"), "export const x = 1;");
|
||||||
|
mockCtx.cwd = dir;
|
||||||
|
|
||||||
const tool = registeredTools.project_map_validate;
|
const tool = registeredTools.project_map_validate;
|
||||||
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
|
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
|
||||||
expect(result.details.clean).toBe(false);
|
expect(result.details.clean).toBe(false);
|
||||||
|
expect(result.content[0].text).toContain("missing");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("project_map_reinit tool", () => {
|
describe("project_map_reinit tool", () => {
|
||||||
it("calls npx project-map reinit with path", async () => {
|
it("returns error when Pi LLM is not accessible", async () => {
|
||||||
vi.mocked(execSync).mockReturnValue("Regenerated 7 files\n");
|
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||||
|
writeFileSync(join(dir, "test.ts"), `export const x = ${Date.now()};`);
|
||||||
|
mockCtx.cwd = dir;
|
||||||
|
|
||||||
const tool = registeredTools.project_map_reinit;
|
const tool = registeredTools.project_map_reinit;
|
||||||
const result = await tool.execute(
|
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
|
||||||
"tool-1",
|
expect(result.details.success).toBe(false);
|
||||||
{ path: "./src" },
|
expect(result.content[0].text).toContain("Pi LLM not accessible");
|
||||||
null,
|
|
||||||
null,
|
|
||||||
mockCtx,
|
|
||||||
);
|
|
||||||
expect(execSync).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining("npx project-map reinit ./src"),
|
|
||||||
expect.anything(),
|
|
||||||
);
|
|
||||||
expect(result.details.success).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("session_start event", () => {
|
describe("session_start event", () => {
|
||||||
it("notifies when dirty .pi-map.md files exist", async () => {
|
it("notifies when dirty .pi-map.md files exist", async () => {
|
||||||
vi.mocked(execSync).mockReturnValueOnce(
|
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||||
"./src/.pi-map.md\n./tests/.pi-map.md\n",
|
writeFileSync(
|
||||||
); // findPiMapFiles
|
join(dir, ".pi-map.md"),
|
||||||
vi.mocked(readFileSync)
|
"# .\n## dirty\n2024-01-01: patched\n",
|
||||||
.mockReturnValueOnce("## dirty\n2024-01-01: patched\n") // dirty
|
);
|
||||||
.mockReturnValueOnce("## dirty\n-\n"); // clean
|
mockCtx.cwd = dir;
|
||||||
|
|
||||||
const handler = registeredEvents.session_start;
|
const handler = registeredEvents.session_start;
|
||||||
await handler(null, mockCtx);
|
await handler(null, mockCtx);
|
||||||
@@ -197,8 +169,9 @@ describe("pi-extension", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not notify when all clean", async () => {
|
it("does not notify when all clean", async () => {
|
||||||
vi.mocked(execSync).mockReturnValueOnce("./src/.pi-map.md\n");
|
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||||
vi.mocked(readFileSync).mockReturnValueOnce("## dirty\n-\n");
|
writeFileSync(join(dir, ".pi-map.md"), "# .\n## dirty\n-\n");
|
||||||
|
mockCtx.cwd = dir;
|
||||||
|
|
||||||
const handler = registeredEvents.session_start;
|
const handler = registeredEvents.session_start;
|
||||||
await handler(null, mockCtx);
|
await handler(null, mockCtx);
|
||||||
@@ -207,9 +180,8 @@ describe("pi-extension", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing when no maps exist", async () => {
|
it("does nothing when no maps exist", async () => {
|
||||||
vi.mocked(execSync).mockImplementation(() => {
|
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||||
throw new Error("no maps");
|
mockCtx.cwd = dir;
|
||||||
});
|
|
||||||
|
|
||||||
const handler = registeredEvents.session_start;
|
const handler = registeredEvents.session_start;
|
||||||
await handler(null, mockCtx);
|
await handler(null, mockCtx);
|
||||||
@@ -220,8 +192,9 @@ describe("pi-extension", () => {
|
|||||||
|
|
||||||
describe("before_agent_start event", () => {
|
describe("before_agent_start event", () => {
|
||||||
it("injects hint when .pi-map.md files exist", async () => {
|
it("injects hint when .pi-map.md files exist", async () => {
|
||||||
vi.mocked(execSync).mockReturnValueOnce("./src/.pi-map.md\n");
|
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||||
vi.mocked(readFileSync).mockReturnValueOnce("content");
|
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
|
||||||
|
mockCtx.cwd = dir;
|
||||||
|
|
||||||
const handler = registeredEvents.before_agent_start;
|
const handler = registeredEvents.before_agent_start;
|
||||||
const result = await handler(null, mockCtx);
|
const result = await handler(null, mockCtx);
|
||||||
@@ -232,9 +205,8 @@ describe("pi-extension", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty object when no maps exist", async () => {
|
it("returns empty object when no maps exist", async () => {
|
||||||
vi.mocked(execSync).mockImplementation(() => {
|
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||||
throw new Error("no maps");
|
mockCtx.cwd = dir;
|
||||||
});
|
|
||||||
|
|
||||||
const handler = registeredEvents.before_agent_start;
|
const handler = registeredEvents.before_agent_start;
|
||||||
const result = await handler(null, mockCtx);
|
const result = await handler(null, mockCtx);
|
||||||
|
|||||||
Reference in New Issue
Block a user