singularity-forge/scripts/validate-pack.js

165 lines
5.8 KiB
JavaScript
Raw Normal View History

// 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 { execSync } from 'node:child_process';
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = resolve(__dirname, '..');
let tarball = null;
let installDir = null;
try {
// --- Guard: workspace packages must not have @gsd/* cross-deps ---
console.log('==> Checking workspace packages for @gsd/* cross-deps...');
const workspaces = ['native', 'pi-agent-core', 'pi-ai', 'pi-coding-agent', 'pi-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('@gsd/'));
if (deps.length) {
console.log(` LEAKED in ${ws}: ${deps.join(', ')}`);
crossFailed = true;
}
}
if (crossFailed) {
console.log('ERROR: Workspace packages have @gsd/* cross-dependencies.');
console.log(' These cause 404s when npm resolves them from the registry.');
process.exit(1);
}
console.log(' No @gsd/* cross-dependencies.');
// --- Pack tarball ---
console.log('==> Packing tarball...');
const packOutput = execSync('npm pack --ignore-scripts', {
cwd: ROOT,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const tarballName = packOutput.trim().split('\n').pop();
tarball = join(ROOT, tarballName);
if (!existsSync(tarball)) {
console.log('ERROR: npm pack produced no tarball');
process.exit(1);
}
const stats = execSync(`du -h "${tarball}"`, { encoding: 'utf8' }).split('\t')[0].trim();
console.log(`==> Tarball: ${tarballName} (${stats} compressed)`);
// --- Check critical files using tar listing ---
console.log('==> Checking critical files...');
const tarList = execSync(`tar tzf "${tarball}"`, { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
const requiredFiles = [
'dist/loader.js',
'packages/pi-coding-agent/dist/index.js',
'scripts/link-workspace-packages.cjs',
];
let missing = false;
for (const required of requiredFiles) {
if (!tarList.includes(`package/${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 = execSync(`npm install "${tarball}"`, {
cwd: installDir,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
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 @gsd/* packages resolved correctly post-install ---
// This catches the Windows-style failure where symlinkSync fails silently and
// node_modules/@gsd/ is never populated, causing ERR_MODULE_NOT_FOUND at runtime.
console.log('==> Verifying @gsd/* workspace package resolution...');
const installedRoot = join(installDir, 'node_modules', 'gsd-pi');
const criticalPkgs = ['pi-coding-agent'];
let resolutionFailed = false;
for (const pkg of criticalPkgs) {
const pkgPath = join(installedRoot, 'node_modules', '@gsd', pkg);
const fallbackPath = join(installedRoot, 'packages', pkg);
if (!existsSync(pkgPath)) {
if (existsSync(fallbackPath)) {
console.log(` MISSING symlink/copy: node_modules/@gsd/${pkg} (packages/${pkg} exists — postinstall may not have run)`);
} else {
console.log(` MISSING: node_modules/@gsd/${pkg} (packages/${pkg} also absent — package is broken)`);
}
resolutionFailed = true;
}
}
if (resolutionFailed) {
console.log('ERROR: @gsd/* 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(' @gsd/* packages are resolvable.');
// --- Run the binary to confirm end-to-end resolution ---
console.log('==> Running installed binary (gsd -v)...');
const loaderPath = join(installedRoot, 'dist', 'loader.js');
try {
const versionOutput = execSync(`node "${loaderPath}" -v`, {
cwd: installDir,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 15000,
}).trim();
console.log(` gsd -v => ${versionOutput}`);
if (!versionOutput.match(/^\d+\.\d+\.\d+/)) {
console.log('ERROR: gsd -v returned unexpected output (expected a version string).');
process.exit(1);
}
} catch (err) {
console.log('ERROR: Running gsd -v 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 });
}
}