#!/usr/bin/env node
/**
* build-bootstrap — package pgh into a per-arch installable tarball.
*
* P.10 Phase 0.5i. Produces `pgh-bootstrap-<os>-<arch>.tar.gz` from the
* current pear-git-v3 checkout, containing:
* - bin/pgh, bin/pgh-sidecar (+ any other bin scripts)
* - lib/ (pgh CLI + sidecar + primitives)
* - node_modules/ (hoisted via `npm install` ahead of running this)
* - package.json with a trimmed `pgh-bootstrap` metadata object
*
* This is deliberately NOT a `bare-pack` / esbuild bundle — we want the
* bootstrap to be inspectable on disk after install, and deps are
* resolved by whatever runtime (Node or Bare) the user's `pgh` launcher
* invokes.
*
* Usage:
* node scripts/build-bootstrap.js # infers os+arch
* node scripts/build-bootstrap.js --os linux --arch x64
* node scripts/build-bootstrap.js --out /tmp/out # override output dir
*/
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
import os from 'os'
import { fileURLToPath } from 'url'
const HERE = path.dirname(fileURLToPath(import.meta.url))
const ROOT = path.resolve(HERE, '..')
// ── Args ──────────────────────────────────────────────────────
function getFlag (name, def) {
const i = process.argv.indexOf('--' + name)
if (i === -1 || !process.argv[i + 1]) return def
return process.argv[i + 1]
}
function archLabel (platform, arch) {
const osMap = { linux: 'linux', darwin: 'darwin', win32: 'win32' }
const archMap = { x64: 'x64', arm64: 'arm64' }
return (osMap[platform] || platform) + '-' + (archMap[arch] || arch)
}
const OS = getFlag('os', os.platform())
const ARCH = getFlag('arch', os.arch())
const LABEL = archLabel(OS, ARCH)
const OUT_DIR = path.resolve(getFlag('out', path.join(ROOT, 'dist')))
const VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version
const TARBALL_NAME = `pgh-bootstrap-${LABEL}.tar.gz`
console.log('Building pgh bootstrap tarball')
console.log(' target: ' + LABEL)
console.log(' version: ' + VERSION)
console.log(' out: ' + path.join(OUT_DIR, TARBALL_NAME))
// ── Stage a clean bootstrap tree ─────────────────────────────
fs.mkdirSync(OUT_DIR, { recursive: true })
const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'pgh-bootstrap-'))
const stageRoot = path.join(stage, 'pgh-bootstrap')
fs.mkdirSync(stageRoot)
// Files to copy (paths relative to ROOT)
const INCLUDE = [
'bin/pgh',
'bin/pgh-sidecar',
'lib/pgh',
'lib/pgh-sidecar',
'lib/pgh-runtime.js',
'lib/release-registry.js'
]
function copyRecursive (src, dest) {
const stat = fs.lstatSync(src)
if (stat.isDirectory()) {
fs.mkdirSync(dest, { recursive: true })
for (const name of fs.readdirSync(src)) {
copyRecursive(path.join(src, name), path.join(dest, name))
}
} else if (stat.isSymbolicLink()) {
// Copy the symlink target contents through (bootstrap is flat)
const real = fs.realpathSync(src)
copyRecursive(real, dest)
} else {
fs.mkdirSync(path.dirname(dest), { recursive: true })
fs.copyFileSync(src, dest)
try { fs.chmodSync(dest, stat.mode & 0o777) } catch {}
}
}
for (const rel of INCLUDE) {
const src = path.join(ROOT, rel)
if (!fs.existsSync(src)) {
console.error(' missing: ' + rel + ' — skipping')
continue
}
copyRecursive(src, path.join(stageRoot, rel))
console.log(' + ' + rel)
}
// node_modules — copied if present (hoisted). Otherwise the user must
// run `npm install` inside the bootstrap after extraction.
//
// NOTE: a naive copy of pear-git-v3's node_modules produces a ~2 GB
// tarball because v3 currently embeds 10 sibling modules via file: deps,
// each with their own node_modules. The target for a real pgh bootstrap
// is ~30-40 MB. Closing that gap requires either:
// - extracting pgh to a standalone package with its own minimum
// dependency set (transport + crypto + hyper-overlay + hyper-sync),
// or
// - an --allow <names...> flag here that prunes to a hand-curated list
// of top-level package names (with transitive closure re-installed).
// Deferred to 0.5k or beyond — current build-bootstrap is scaffolding
// that produces valid tarballs, not yet size-optimized.
const allow = getFlag('allow', null)
if (fs.existsSync(path.join(ROOT, 'node_modules'))) {
if (allow) {
const names = allow.split(',')
fs.mkdirSync(path.join(stageRoot, 'node_modules'), { recursive: true })
for (const name of names) {
const src = path.join(ROOT, 'node_modules', name)
if (fs.existsSync(src)) {
copyRecursive(src, path.join(stageRoot, 'node_modules', name))
console.log(' + node_modules/' + name)
} else {
console.log(' SKIP node_modules/' + name + ' (not present — transitive?)')
}
}
} else {
copyRecursive(path.join(ROOT, 'node_modules'), path.join(stageRoot, 'node_modules'))
console.log(' + node_modules (full — use --allow <names> to prune)')
}
} else {
console.log(' (node_modules missing — user will need to npm install after extract)')
}
// Trimmed package.json — strip scripts, devDependencies; add bootstrap metadata
const srcPkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'))
const bootPkg = {
name: 'pgh-bootstrap',
version: srcPkg.version,
type: srcPkg.type,
bin: {
pgh: './bin/pgh',
'pgh-sidecar': './bin/pgh-sidecar'
},
dependencies: srcPkg.dependencies,
pghBootstrap: {
os: OS,
arch: ARCH,
label: LABEL,
builtAt: new Date().toISOString(),
sourceVersion: srcPkg.version
}
}
fs.writeFileSync(path.join(stageRoot, 'package.json'), JSON.stringify(bootPkg, null, 2))
console.log(' + package.json (trimmed)')
// ── Tar it up ────────────────────────────────────────────────
const outFile = path.join(OUT_DIR, TARBALL_NAME)
try {
execSync(`tar -czf "${outFile}" -C "${stage}" pgh-bootstrap`, { stdio: 'inherit' })
} catch (err) {
console.error('tar failed: ' + err.message)
process.exit(1)
}
const size = fs.statSync(outFile).size
console.log('\nDone: ' + outFile + ' (' + (size / 1048576).toFixed(1) + ' MB)')
// Clean up stage
fs.rmSync(stage, { recursive: true, force: true })
// Checksum for verification
try {
const sum = execSync(`sha256sum "${outFile}" 2>/dev/null || shasum -a 256 "${outFile}"`, { encoding: 'utf8' }).trim()
fs.writeFileSync(outFile + '.sha256', sum + '\n')
console.log(' ' + outFile + '.sha256')
} catch {
// no sha256sum / shasum available — skip
}