// 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 }); } }