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>
291 lines
8.2 KiB
JavaScript
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 });
|
|
}
|
|
}
|