singularity-forge/scripts/validate-pack.js
Mikael Hugo 21113e18a9 fix: update remaining stale repo and scope refs to singularity-forge
After fixing forensics.md and error-classifier.ts last fire, swept the
rest of the tree for the same class of stale reference:

- scripts/validate-pack.js: criticalPackages list used \`@sf\` and
  \`@sf-build\` scopes — neither exists in node_modules; this is in CI
  (.github/workflows/ci.yml) + prepublishOnly, so the validation step
  was failing to find anything. Now \`@singularity-forge/pi-coding-agent\`
  and \`@singularity-forge/rpc-client\` (the actual scope).
- src/resources/skills/github-workflows/references/gh/SKILL.md: same
  GraphQL bug as forensics.md — owner:"sf-build" name:"sf-2" — and
  three \`gh project\` commands using owner sf-build. The gh issue
  create command above already used singularity-forge/sf-run, so the
  follow-up calls always failed. Also retitled "sf-2 Backlog" to
  "sf-run Backlog".
- src/resources/extensions/sf/bootstrap/system-context.ts: deprecation
  warning linked to https://github.com/sf-build/SF/issues/1492.
- packages/mcp-server/README.md, packages/rpc-client/README.md: 9 refs
  to \`@sf-build/...\` for installable package names — would mislead
  anyone copy-pasting into npm install.
- docs/user-docs/troubleshooting.md (+ zh-CN): GitHub Issues link
  pointed at github.com/sf-build/SF/issues.
- docs/user-docs/getting-started.md (+ zh-CN): clone URL was correct
  but the next \`cd\` was \`cd sf-2/docker\` — won't exist after a
  fresh clone of sf-run.
- docs/dev/ci-cd-pipeline.md: GHCR org was \`sf-build\`.

Code comments containing "sf-2" / "sf-build" in non-active places
(parsers.ts banner, error message URLs in tests, dev-doc absolute
paths from a contributor's Mac) left alone — they're informational
and not addressed by users or runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:01:55 +02:00

217 lines
7.7 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', '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('@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/pi-coding-agent/dist/index.js',
'packages/rpc-client/dist/index.js',
'packages/mcp-server/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', 'sf-run');
const criticalPackages = [
{ scope: '@singularity-forge', name: 'pi-coding-agent' },
{ scope: '@singularity-forge', name: 'rpc-client' },
];
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 bundledWorkflowMcpCliPath = join(installedRoot, 'packages', 'mcp-server', 'dist', 'cli.js');
if (!existsSync(bundledWorkflowMcpCliPath)) {
console.log('ERROR: Bundled workflow MCP CLI missing after install.');
console.log(` Expected: ${bundledWorkflowMcpCliPath}`);
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('');
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 });
}
}