251 lines
7.2 KiB
JavaScript
251 lines
7.2 KiB
JavaScript
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
import { createHash } from "node:crypto";
|
|
import { resolve } from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
import { stripPiSubagentBuiltinModelSource } from "./lib/pi-subagents-patch.mjs";
|
|
|
|
const appRoot = resolve(import.meta.dirname, "..");
|
|
const settingsPath = resolve(appRoot, ".feynman", "settings.json");
|
|
const packageJsonPath = resolve(appRoot, "package.json");
|
|
const packageLockPath = resolve(appRoot, "package-lock.json");
|
|
const feynmanDir = resolve(appRoot, ".feynman");
|
|
const workspaceDir = resolve(appRoot, ".feynman", "npm");
|
|
const workspaceNodeModulesDir = resolve(workspaceDir, "node_modules");
|
|
const manifestPath = resolve(workspaceDir, ".runtime-manifest.json");
|
|
const workspacePackageJsonPath = resolve(workspaceDir, "package.json");
|
|
const workspaceArchivePath = resolve(feynmanDir, "runtime-workspace.tgz");
|
|
const PRUNE_VERSION = 4;
|
|
const PINNED_RUNTIME_PACKAGES = [
|
|
"@mariozechner/pi-agent-core",
|
|
"@mariozechner/pi-ai",
|
|
"@mariozechner/pi-coding-agent",
|
|
"@mariozechner/pi-tui",
|
|
];
|
|
|
|
function readPackageSpecs() {
|
|
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
const packageSpecs = Array.isArray(settings.packages)
|
|
? settings.packages
|
|
.filter((value) => typeof value === "string" && value.startsWith("npm:"))
|
|
.map((value) => value.slice(4))
|
|
: [];
|
|
|
|
for (const packageName of PINNED_RUNTIME_PACKAGES) {
|
|
const version = readLockedPackageVersion(packageName);
|
|
if (version) {
|
|
packageSpecs.push(`${packageName}@${version}`);
|
|
}
|
|
}
|
|
|
|
return Array.from(new Set(packageSpecs));
|
|
}
|
|
|
|
function parsePackageName(spec) {
|
|
const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@.+)?$/);
|
|
return match?.[1] ?? spec;
|
|
}
|
|
|
|
function readLockedPackageVersion(packageName) {
|
|
if (!existsSync(packageLockPath)) {
|
|
return undefined;
|
|
}
|
|
try {
|
|
const lockfile = JSON.parse(readFileSync(packageLockPath, "utf8"));
|
|
const entry = lockfile.packages?.[`node_modules/${packageName}`];
|
|
return typeof entry?.version === "string" ? entry.version : undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function arraysMatch(left, right) {
|
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
}
|
|
|
|
function hashFile(path) {
|
|
if (!existsSync(path)) {
|
|
return null;
|
|
}
|
|
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
}
|
|
|
|
function getRuntimeInputHash() {
|
|
const hash = createHash("sha256");
|
|
for (const path of [packageJsonPath, packageLockPath, settingsPath]) {
|
|
hash.update(path);
|
|
hash.update("\0");
|
|
hash.update(hashFile(path) ?? "missing");
|
|
hash.update("\0");
|
|
}
|
|
return hash.digest("hex");
|
|
}
|
|
|
|
function workspaceIsCurrent(packageSpecs) {
|
|
if (!existsSync(manifestPath) || !existsSync(workspaceNodeModulesDir)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
if (!Array.isArray(manifest.packageSpecs) || !arraysMatch(manifest.packageSpecs, packageSpecs)) {
|
|
return false;
|
|
}
|
|
if (manifest.runtimeInputHash !== getRuntimeInputHash()) {
|
|
return false;
|
|
}
|
|
if (
|
|
manifest.nodeAbi !== process.versions.modules ||
|
|
manifest.platform !== process.platform ||
|
|
manifest.arch !== process.arch ||
|
|
manifest.pruneVersion !== PRUNE_VERSION
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return packageSpecs.every((spec) => existsSync(resolve(workspaceNodeModulesDir, parsePackageName(spec))));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function writeWorkspacePackageJson() {
|
|
writeFileSync(
|
|
workspacePackageJsonPath,
|
|
JSON.stringify(
|
|
{
|
|
name: "feynman-runtime",
|
|
private: true,
|
|
},
|
|
null,
|
|
2,
|
|
) + "\n",
|
|
"utf8",
|
|
);
|
|
}
|
|
|
|
function childNpmInstallEnv() {
|
|
return {
|
|
...process.env,
|
|
// `npm pack --dry-run` exports dry-run config to lifecycle scripts. The
|
|
// vendored runtime workspace must still install real node_modules so the
|
|
// publish artifact can be validated without poisoning the archive.
|
|
npm_config_dry_run: "false",
|
|
NPM_CONFIG_DRY_RUN: "false",
|
|
};
|
|
}
|
|
|
|
function prepareWorkspace(packageSpecs) {
|
|
rmSync(workspaceDir, { recursive: true, force: true });
|
|
mkdirSync(workspaceDir, { recursive: true });
|
|
writeWorkspacePackageJson();
|
|
|
|
if (packageSpecs.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const result = spawnSync(
|
|
process.env.npm_execpath ? process.execPath : "npm",
|
|
process.env.npm_execpath
|
|
? [process.env.npm_execpath, "install", "--prefer-offline", "--no-audit", "--no-fund", "--no-dry-run", "--legacy-peer-deps", "--loglevel", "error", "--prefix", workspaceDir, ...packageSpecs]
|
|
: ["install", "--prefer-offline", "--no-audit", "--no-fund", "--no-dry-run", "--legacy-peer-deps", "--loglevel", "error", "--prefix", workspaceDir, ...packageSpecs],
|
|
{ stdio: "inherit", env: childNpmInstallEnv() },
|
|
);
|
|
if (result.status !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
}
|
|
|
|
function writeManifest(packageSpecs) {
|
|
writeFileSync(
|
|
manifestPath,
|
|
JSON.stringify(
|
|
{
|
|
packageSpecs,
|
|
runtimeInputHash: getRuntimeInputHash(),
|
|
generatedAt: new Date().toISOString(),
|
|
nodeAbi: process.versions.modules,
|
|
nodeVersion: process.version,
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
pruneVersion: PRUNE_VERSION,
|
|
},
|
|
null,
|
|
2,
|
|
) + "\n",
|
|
"utf8",
|
|
);
|
|
}
|
|
|
|
function pruneWorkspace() {
|
|
const result = spawnSync(process.execPath, [resolve(appRoot, "scripts", "prune-runtime-deps.mjs"), workspaceDir], {
|
|
stdio: "inherit",
|
|
});
|
|
if (result.status !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
}
|
|
|
|
function stripBundledPiSubagentModelPins() {
|
|
const agentsRoot = resolve(workspaceNodeModulesDir, "pi-subagents", "agents");
|
|
if (!existsSync(agentsRoot)) {
|
|
return false;
|
|
}
|
|
|
|
let changed = false;
|
|
for (const entry of readdirSync(agentsRoot, { withFileTypes: true })) {
|
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
const entryPath = resolve(agentsRoot, entry.name);
|
|
const source = readFileSync(entryPath, "utf8");
|
|
const patched = stripPiSubagentBuiltinModelSource(source);
|
|
if (patched === source) continue;
|
|
writeFileSync(entryPath, patched, "utf8");
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
function archiveIsCurrent() {
|
|
if (!existsSync(workspaceArchivePath) || !existsSync(manifestPath)) {
|
|
return false;
|
|
}
|
|
|
|
return statSync(workspaceArchivePath).mtimeMs >= statSync(manifestPath).mtimeMs;
|
|
}
|
|
|
|
function createWorkspaceArchive() {
|
|
rmSync(workspaceArchivePath, { force: true });
|
|
|
|
const result = spawnSync("tar", ["-czf", workspaceArchivePath, "-C", feynmanDir, "npm"], {
|
|
stdio: "inherit",
|
|
});
|
|
if (result.status !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
}
|
|
|
|
const packageSpecs = readPackageSpecs();
|
|
|
|
if (workspaceIsCurrent(packageSpecs)) {
|
|
console.log("[feynman] vendored runtime workspace already up to date");
|
|
if (stripBundledPiSubagentModelPins()) {
|
|
writeManifest(packageSpecs);
|
|
console.log("[feynman] stripped bundled pi-subagents model pins");
|
|
}
|
|
if (archiveIsCurrent()) {
|
|
process.exit(0);
|
|
}
|
|
console.log("[feynman] refreshing runtime workspace archive...");
|
|
createWorkspaceArchive();
|
|
console.log("[feynman] runtime workspace archive ready");
|
|
process.exit(0);
|
|
}
|
|
|
|
console.log("[feynman] preparing vendored runtime workspace...");
|
|
prepareWorkspace(packageSpecs);
|
|
pruneWorkspace();
|
|
stripBundledPiSubagentModelPins();
|
|
writeManifest(packageSpecs);
|
|
createWorkspaceArchive();
|
|
console.log("[feynman] vendored runtime workspace ready");
|