singularity-forge/scripts/validate-pack.js
Mikael Hugo 02a4339a51 refactor: rename pi-* packages to forge-native names (Phase 1)
Rename all four packages/pi-* directories to forge-native names,
stripping the 'pi' identity and establishing forge's own:

- packages/pi-coding-agent → packages/coding-agent
- packages/pi-ai → packages/ai
- packages/pi-agent-core → packages/agent-core
- packages/pi-tui → packages/tui

Package names updated:
- @singularity-forge/pi-coding-agent → @singularity-forge/coding-agent
- @singularity-forge/pi-ai → @singularity-forge/ai
- @singularity-forge/pi-agent-core → @singularity-forge/agent-core
- @singularity-forge/pi-tui → @singularity-forge/tui

All import references, bare string references, path references,
internal variable names (_bundledPi*), and dist files updated.
@mariozechner/pi-* third-party compat aliases preserved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-10 11:28:01 +02:00

291 lines
8.2 KiB
JavaScript

// validate-pack.js — Verify the npm tarball is installable before publishing.
//
// Usage: npm run validate-pack (or node scripts/validate-pack.js)
// Exit 0 = safe to publish, Exit 1 = broken package.
import { execFileSync } from "node:child_process";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
const __filename = import.meta.filename;
const __dirname = import.meta.dirname;
const ROOT = resolve(__dirname, "..");
let tarball = null;
let installDir = null;
let npmCacheDir = null;
const DEFAULT_MAX_BUFFER = 50 * 1024 * 1024;
function getNpmCommand() {
return process.platform === "win32" ? "npm.cmd" : "npm";
}
function runNpm(args, options = {}) {
return execFileSync(getNpmCommand(), args, {
cwd: ROOT,
encoding: "utf8",
shell: process.platform === "win32",
stdio: ["pipe", "pipe", "pipe"],
maxBuffer: DEFAULT_MAX_BUFFER,
env: {
...process.env,
npm_config_cache: npmCacheDir ?? process.env.npm_config_cache,
},
...options,
});
}
function formatBytes(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
try {
npmCacheDir = mkdtempSync(join(tmpdir(), "validate-pack-npm-cache-"));
mkdirSync(npmCacheDir, { recursive: true });
// --- Guard: workspace packages must not have @singularity-forge/* cross-deps ---
console.log(
"==> Checking workspace packages for @singularity-forge/* cross-deps...",
);
const workspaces = [
"native",
"agent-core",
"ai",
"coding-agent",
"tui",
];
let crossFailed = false;
for (const ws of workspaces) {
const pkgPath = join(ROOT, "packages", ws, "package.json");
if (!existsSync(pkgPath)) continue;
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
const deps = Object.keys(pkg.dependencies || {}).filter((d) =>
d.startsWith("@singularity-forge/"),
);
if (deps.length) {
console.log(` LEAKED in ${ws}: ${deps.join(", ")}`);
crossFailed = true;
}
}
if (crossFailed) {
console.log(
"ERROR: Workspace packages have @singularity-forge/* cross-dependencies.",
);
console.log(
" These cause 404s when npm resolves them from the registry.",
);
process.exit(1);
}
console.log(" No @singularity-forge/* cross-dependencies.");
// --- Pack tarball ---
console.log("==> Packing tarball...");
const packOutput = runNpm(["pack", "--json", "--ignore-scripts"]);
const packEntries = JSON.parse(packOutput);
const packEntry = Array.isArray(packEntries) ? packEntries[0] : null;
const tarballName = packEntry?.filename;
tarball = join(ROOT, tarballName);
if (!existsSync(tarball)) {
console.log("ERROR: npm pack produced no tarball");
process.exit(1);
}
const stats = statSync(tarball);
console.log(
`==> Tarball: ${tarballName} (${formatBytes(stats.size)} compressed)`,
);
// --- Check critical files using npm pack metadata ---
console.log("==> Checking critical files...");
const packedFiles = new Set(
Array.isArray(packEntry?.files)
? packEntry.files.map((entry) => entry?.path).filter(Boolean)
: [],
);
const requiredFiles = [
"dist/loader.js",
"packages/coding-agent/dist/index.js",
"packages/rpc-client/dist/index.js",
"packages/daemon/dist/cli.js",
"scripts/link-workspace-packages.cjs",
"dist/web/standalone/server.js",
];
let missing = false;
for (const required of requiredFiles) {
if (!packedFiles.has(required)) {
console.log(` MISSING: ${required}`);
missing = true;
}
}
if (missing) {
console.log("ERROR: Critical files missing from tarball.");
process.exit(1);
}
console.log(" Critical files present.");
// --- Install test ---
console.log("==> Testing install in isolated directory...");
installDir = mkdtempSync(join(tmpdir(), "validate-pack-"));
writeFileSync(
join(installDir, "package.json"),
JSON.stringify(
{ name: "test-install", version: "1.0.0", private: true },
null,
2,
),
);
try {
const installOutput = execFileSync(getNpmCommand(), ["install", tarball], {
cwd: installDir,
encoding: "utf8",
shell: process.platform === "win32",
stdio: ["pipe", "pipe", "pipe"],
maxBuffer: DEFAULT_MAX_BUFFER,
env: {
...process.env,
npm_config_cache: npmCacheDir,
},
});
console.log(installOutput);
console.log("==> Install succeeded.");
} catch (err) {
console.log("");
console.log("ERROR: npm install of tarball failed.");
if (err.stdout) console.log(err.stdout);
if (err.stderr) console.log(err.stderr);
process.exit(1);
}
// --- Verify @singularity-forge/* packages resolved correctly post-install ---
// This catches the Windows-style failure where symlinkSync fails silently and
// node_modules/@singularity-forge/ is never populated, causing ERR_MODULE_NOT_FOUND at runtime.
console.log(
"==> Verifying @singularity-forge/* workspace package resolution...",
);
const installedRoot = join(installDir, "node_modules", "singularity-forge");
const criticalPackages = [
{ scope: "@singularity-forge", name: "coding-agent" },
{ scope: "@singularity-forge", name: "rpc-client" },
{ scope: "@singularity-forge", name: "daemon" },
];
let resolutionFailed = false;
for (const pkg of criticalPackages) {
const pkgPath = join(installedRoot, "node_modules", pkg.scope, pkg.name);
const fallbackPath = join(installedRoot, "packages", pkg.name);
if (!existsSync(pkgPath)) {
if (existsSync(fallbackPath)) {
console.log(
` MISSING symlink/copy: node_modules/${pkg.scope}/${pkg.name} (packages/${pkg.name} exists — postinstall may not have run)`,
);
} else {
console.log(
` MISSING: node_modules/${pkg.scope}/${pkg.name} (packages/${pkg.name} also absent — package is broken)`,
);
}
resolutionFailed = true;
}
}
if (resolutionFailed) {
console.log(
"ERROR: @singularity-forge/* packages are not resolvable after install.",
);
console.log(
" This will cause ERR_MODULE_NOT_FOUND on first run (especially on Windows).",
);
process.exit(1);
}
console.log(" @singularity-forge/* packages are resolvable.");
// --- Run the binary to confirm end-to-end resolution ---
console.log("==> Running installed binary (sf -v)...");
const loaderPath = join(installedRoot, "dist", "loader.js");
const daemonCliPath = join(
installedRoot,
"packages",
"daemon",
"dist",
"cli.js",
);
if (!existsSync(daemonCliPath)) {
console.log("ERROR: Bundled daemon CLI missing after install.");
console.log(` Expected: ${daemonCliPath}`);
process.exit(1);
}
try {
const versionOutput = execFileSync(process.execPath, [loaderPath, "-v"], {
cwd: installDir,
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
timeout: 15000,
maxBuffer: DEFAULT_MAX_BUFFER,
}).trim();
console.log(` sf -v => ${versionOutput}`);
if (!versionOutput.match(/^\d+\.\d+\.\d+/)) {
console.log(
"ERROR: sf -v returned unexpected output (expected a version string).",
);
process.exit(1);
}
} catch (err) {
console.log("ERROR: Running sf -v failed after install.");
if (err.stdout) console.log(err.stdout);
if (err.stderr) console.log(err.stderr);
process.exit(1);
}
console.log("==> Running installed daemon binary (sf-server --help)...");
try {
const helpOutput = execFileSync(
process.execPath,
[daemonCliPath, "--help"],
{
cwd: installDir,
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
timeout: 15000,
maxBuffer: DEFAULT_MAX_BUFFER,
},
);
if (!helpOutput.includes("Usage: sf-server")) {
console.log("ERROR: sf-server --help returned unexpected output.");
process.exit(1);
}
} catch (err) {
console.log("ERROR: Running sf-server --help failed after install.");
if (err.stdout) console.log(err.stdout);
if (err.stderr) console.log(err.stderr);
process.exit(1);
}
console.log("");
console.log("Package is installable. Safe to publish.");
process.exit(0);
} finally {
if (installDir && existsSync(installDir)) {
rmSync(installDir, { recursive: true, force: true });
}
if (tarball && existsSync(tarball)) {
rmSync(tarball, { force: true });
}
if (npmCacheDir && existsSync(npmCacheDir)) {
rmSync(npmCacheDir, { recursive: true, force: true });
}
}