Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

scripts/build/load-probe.ts

Raw
// 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<ProbeReport> {
	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<ReturnType<typeof createAgentSession>>["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 <package-root>...");
	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);