repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
scripts/build/verify.ts
Rawimport { spawn } from "node:child_process";
import { cpSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import type { ProbeReport } from "./conflicts.ts";
import { listFiles } from "./publish.ts";
import { isExternal, packageOf } from "./spec.ts";
const HERE = dirname(fileURLToPath(import.meta.url));
const PROBE = join(HERE, "load-probe.ts");
export interface VerifyOptions {
/** Extra bare packages this package may legitimately import. */
externals?: Record<string, string>;
/** User settings written into the probe's isolated agent directory. */
userSettings?: Record<string, unknown>;
timeoutMs?: number;
}
export interface VerifyReport extends ProbeReport {
package: string;
/** Bare specifiers found in emitted JavaScript that violate the external policy. */
forbiddenImports: string[];
ok: boolean;
}
/** Static and dynamic import specifiers of one JavaScript module. */
export function moduleImports(file: string, text: string): string[] {
const specifiers = new Set<string>();
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
const visit = (node: ts.Node): void => {
if (
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
node.moduleSpecifier &&
ts.isStringLiteral(node.moduleSpecifier)
)
specifiers.add(node.moduleSpecifier.text);
if (
ts.isCallExpression(node) &&
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
node.arguments[0] &&
ts.isStringLiteralLike(node.arguments[0])
)
specifiers.add(node.arguments[0].text);
ts.forEachChild(node, visit);
};
visit(source);
return [...specifiers].sort();
}
export function scanImports(packageDir: string): string[] {
const specifiers = new Set<string>();
for (const file of listFiles(packageDir)) {
// Installed external dependency trees own their internal imports.
if (file.startsWith("node_modules/") || !/\.[cm]?js$/u.test(file)) continue;
for (const specifier of moduleImports(
file,
readFileSync(join(packageDir, file), "utf8"),
))
specifiers.add(specifier);
}
return [...specifiers].sort();
}
export function forbiddenImports(
packageDir: string,
externals: Record<string, string> = {},
): string[] {
return scanImports(packageDir).filter((specifier) => {
if (specifier.startsWith("./") || specifier.startsWith("../")) {
return specifier.startsWith("../") || /\.[cm]?ts$/u.test(specifier);
}
if (specifier.startsWith("/") || specifier.startsWith("file:")) return true;
return (
!isExternal(specifier, externals) && !(packageOf(specifier) in externals)
);
});
}
/** Run the Pi load probe over relocated package copies. */
export async function probePackages(
packageDirs: string[],
options: Pick<VerifyOptions, "userSettings" | "timeoutMs"> = {},
): Promise<ProbeReport> {
const timeoutMs = options.timeoutMs ?? 120_000;
const root = mkdtempSync(join(tmpdir(), "pi-ext-verify-"));
try {
const relocated = packageDirs.map((dir) => {
const copy = join(root, "packages", basename(dir));
cpSync(dir, copy, { recursive: true });
return copy;
});
const agentDir = join(root, "agent");
const output = await new Promise<string>((resolveOutput, reject) => {
const child = spawn(
process.execPath,
["--no-warnings", PROBE, ...relocated],
{
cwd: root,
env: {
...process.env,
PI_CODING_AGENT_DIR: agentDir,
PI_OFFLINE: "1",
...(options.userSettings
? {
PI_EXT_PROBE_USER_SETTINGS: JSON.stringify(
options.userSettings,
),
}
: {}),
},
stdio: ["ignore", "pipe", "pipe"],
},
);
let text = "";
const append = (chunk: Buffer) => {
text += chunk.toString("utf8");
};
child.stdout.on("data", append);
child.stderr.on("data", append);
const timer = setTimeout(() => {
child.kill("SIGKILL");
reject(new Error(`load probe timed out after ${timeoutMs}ms\n${text}`));
}, timeoutMs);
child.once("error", (error) => {
clearTimeout(timer);
reject(error);
});
child.once("close", () => {
clearTimeout(timer);
resolveOutput(text);
});
});
const line = output
.split("\n")
.reverse()
.find((candidate) => candidate.startsWith("PI_EXT_PROBE="));
if (!line) throw new Error(`load probe produced no report\n${output}`);
return JSON.parse(line.slice("PI_EXT_PROBE=".length)) as ProbeReport;
} finally {
rmSync(root, { recursive: true, force: true });
}
}
/**
* Verify the complete configured collection relocated together: every package
* loads and no two packages claim the same identity, tool, command, flag, or skill.
*/
export async function verifyCollection(
packageDirs: string[],
options: Pick<VerifyOptions, "userSettings" | "timeoutMs"> = {},
): Promise<ProbeReport & { ok: boolean }> {
const report = await probePackages(
packageDirs.map((dir) => resolve(dir)),
options,
);
return {
...report,
ok: report.errors.length === 0 && report.conflicts.length === 0,
};
}
/** Verify one generated package relocated outside the checkout. */
export async function verifyPackage(
packageDir: string,
options: VerifyOptions = {},
): Promise<VerifyReport> {
const dir = resolve(packageDir);
const forbidden = forbiddenImports(dir, options.externals);
const report = await probePackages([dir], options);
return {
package: dir,
...report,
forbiddenImports: forbidden,
ok: report.errors.length === 0 && forbidden.length === 0,
};
}