// Loads a generated extension package through Pi's real loader and a print-mode // session, then reports registrations as JSON on the last stdout line. // Runs as a child process so the probed package sees a clean module graph. import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { getModels } from "@earendil-works/pi-ai/compat"; import { createAgentSession, DefaultResourceLoader, SessionManager, } from "@earendil-works/pi-coding-agent"; import { type ProbeReport, registrationConflicts } from "./conflicts.ts"; async function probe(packageRoots: string[]): Promise { const cwd = mkdtempSync(join(tmpdir(), "pi-ext-probe-cwd-")); const agentDir = process.env.PI_CODING_AGENT_DIR ?? join(cwd, "agent"); if (process.env.PI_EXT_PROBE_USER_SETTINGS) { mkdirSync(agentDir, { recursive: true }); writeFileSync( join(agentDir, "settings.json"), process.env.PI_EXT_PROBE_USER_SETTINGS, ); } const errors: string[] = []; const packageNames: string[] = []; const entries = packageRoots.flatMap((root) => { const pkg = JSON.parse( readFileSync(join(root, "package.json"), "utf8"), ) as { name?: string; pi?: { extensions?: string[] }; }; packageNames.push(pkg.name ?? root); return (pkg.pi?.extensions ?? ["index.js"]).map((entry) => resolve(root, entry), ); }); let session: | Awaited>["session"] | undefined; try { const loader = new DefaultResourceLoader({ cwd, agentDir, noExtensions: true, noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true, additionalExtensionPaths: entries, }); await loader.reload(); const loaded = loader.getExtensions(); errors.push(...loaded.errors.map((error) => String(error))); const model = getModels("openai")[0] ?? getModels("anthropic")[0]; if (!model) throw new Error("Pi has no built-in model for an in-memory session."); ({ session } = await createAgentSession({ cwd, agentDir, model, resourceLoader: loader, sessionManager: SessionManager.inMemory(cwd), })); await session.bindExtensions({ mode: "print", onError: (error) => errors.push(String(error)), }); const activeTools = session.getActiveToolNames(); const extensions = loaded.extensions.map((extension) => ({ path: extension.path, tools: [...extension.tools.keys()].sort(), commands: [...extension.commands.keys()].sort(), flags: [...extension.flags.keys()].sort(), activeTools: [...extension.tools.keys()] .filter((name) => activeTools.includes(name)) .sort(), })); const skills = session.resourceLoader .getSkills() .skills.map((skill) => skill.filePath) .sort(); await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); return { errors, extensions, skills, conflicts: registrationConflicts(extensions, packageNames, skills), }; } finally { session?.dispose(); rmSync(cwd, { recursive: true, force: true }); } } const roots = process.argv.slice(2); if (!roots.length) { console.error("usage: node scripts/build/load-probe.ts ..."); process.exit(2); } const report = await probe(roots).catch((error: unknown) => ({ errors: [ error instanceof Error ? (error.stack ?? error.message) : String(error), ], extensions: [], skills: [], conflicts: [], })); console.log(`PI_EXT_PROBE=${JSON.stringify(report)}`); process.exit(0);