UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

450 lines (421 loc) 14.1 kB
#!/usr/bin/env node /** * verify-platform.cjs * * Phase 1 of the silent-exit elimination effort: an install-time preflight * that inspects the host (platform, arch, libc, glibc version) and the * optional platform-specific binary package (`consortium-code-<plat>`), then * writes a sentinel JSON file the runtime consumes at launch. * * The script: * - Is intentionally dependency-free (runs during `postinstall`, before * devDependencies like chalk are available). ANSI codes inlined. * - NEVER exits non-zero. A failed `postinstall` would break every global * install; the CLI's launch-time preflight (Phase 2) is what actually * gates execution. All problems surface via: * 1. the sentinel file (machine-readable) * 2. the colorized stdout summary (human-readable) * * Sentinel path: <CONSORTIUM_HOME>/preflight.json * CONSORTIUM_HOME defaults to ${HOME}/.consortium (stable) * or ${HOME}/.consortium-dev (dev variant: dev-hummingbird-sneakers). * Overridable via env CONSORTIUM_HOME_DIR. */ 'use strict'; const fs = require('fs'); const path = require('path'); const os = require('os'); const { execSync } = require('child_process'); // --- ANSI helpers (no chalk — postinstall context) ---------------------- const isTTY = process.stdout.isTTY === true; const ansi = (code, text) => (isTTY ? `\u001b[${code}m${text}\u001b[0m` : text); const green = (t) => ansi('32', t); const yellow = (t) => ansi('33', t); const red = (t) => ansi('31', t); const dim = (t) => ansi('2', t); const bold = (t) => ansi('1', t); const SUPPORTED_PLATFORMS = new Set([ 'darwin-arm64', 'darwin-x64', 'linux-x64', 'linux-arm64', // Windows is supported (daemon runs; auto-update via the desktop/Tauri // bundle). It has no required native binary package, so it falls into the // non-blocking "platform-pkg optional" branch like macOS — only multiplexer // (tmux/zellij) features are unavailable, which is a feature-level note, not // a platform gate. 'win32-x64', 'win32-arm64', ]); // Platforms where the binary package is expected to exist. macOS binaries // may legitimately be absent if installed by a user who skipped optional // deps, but darwin is still in the supported set — flag it as an error only // on Linux where the CLI truly cannot function without a native binary. const PLATFORM_PKG_REQUIRED = new Set(['linux-x64', 'linux-arm64']); /** * Read this package's own package.json to determine cli name/version and * therefore which home directory to use for the sentinel. */ function readOwnPackageJson() { try { const p = path.resolve(__dirname, '..', 'package.json'); return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_e) { return { name: 'consortium', version: '0.0.0' }; } } function resolveConsortiumHome(cliPackageName) { if (process.env.CONSORTIUM_HOME_DIR) { return process.env.CONSORTIUM_HOME_DIR; } const home = os.homedir(); if (cliPackageName === 'dev-hummingbird-sneakers') { return path.join(home, '.consortium-dev'); } return path.join(home, '.consortium'); } function detectMusl() { try { if (fs.existsSync('/etc/alpine-release')) return true; } catch (_e) { /* best-effort */ } try { const report = typeof process.report?.getReport === 'function' ? process.report.getReport() : null; if (report && report.header && !report.header.glibcVersionRuntime) { // Only treat a missing glibc field as musl if we're actually on Linux — // macOS / Windows reports also omit it. if (process.platform === 'linux') return true; } } catch (_e) { /* best-effort */ } return false; } function detectGlibcVersion() { // 1. process.report — fastest, most accurate try { const report = typeof process.report?.getReport === 'function' ? process.report.getReport() : null; const v = report?.header?.glibcVersionRuntime; if (v && typeof v === 'string') return v.trim(); } catch (_e) { /* fall through */ } // 2. getconf GNU_LIBC_VERSION -> "glibc 2.31" try { const out = execSync('getconf GNU_LIBC_VERSION', { stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000, }) .toString() .trim(); const m = out.match(/(\d+\.\d+(?:\.\d+)?)/); if (m) return m[1]; } catch (_e) { /* fall through */ } // 3. ldd --version | head -1 -> "ldd (GNU libc) 2.31" try { const out = execSync('ldd --version', { stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000, }) .toString() .split('\n')[0]; const m = out.match(/(\d+\.\d+(?:\.\d+)?)/); if (m) return m[1]; } catch (_e) { /* fall through */ } return null; } /** * Parse a glibc version string like "2.31" into a comparable tuple. * Returns null if unparseable. */ function parseGlibc(v) { if (!v || typeof v !== 'string') return null; const m = v.match(/(\d+)\.(\d+)(?:\.(\d+))?/); if (!m) return null; return [Number(m[1]), Number(m[2]), Number(m[3] || 0)]; } function compareGlibc(a, b) { const A = parseGlibc(a); const B = parseGlibc(b); if (!A || !B) return 0; for (let i = 0; i < 3; i++) { if (A[i] !== B[i]) return A[i] - B[i]; } return 0; } /** * Try to resolve the consortium-code-<plat> package. Returns * { name, version, dir, binaryPath } on success, or null on failure. */ function resolvePlatformPackage(platformKey) { const pkgName = `consortium-code-${platformKey}`; let pkgJsonPath; try { pkgJsonPath = require.resolve(`${pkgName}/package.json`, { paths: [ path.resolve(__dirname, '..'), path.resolve(__dirname, '..', '..'), process.cwd(), ], }); } catch (_e) { return { name: pkgName, present: false }; } try { const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); const dir = path.dirname(pkgJsonPath); // Best-effort binary discovery: prefer "bin" entry, fall back to // anything under the package dir named consortium-code*. let binaryPath = null; if (pkg.bin) { if (typeof pkg.bin === 'string') { binaryPath = path.join(dir, pkg.bin); } else if (typeof pkg.bin === 'object') { const first = Object.values(pkg.bin)[0]; if (typeof first === 'string') binaryPath = path.join(dir, first); } } if (!binaryPath || !fs.existsSync(binaryPath)) { // Scan top level for a file starting with "consortium-code". try { const entries = fs.readdirSync(dir); const match = entries.find((e) => /^consortium-code/.test(e)); if (match) binaryPath = path.join(dir, match); } catch (_e) { /* ignore */ } } return { name: pkgName, version: pkg.version || null, dir, binaryPath: binaryPath && fs.existsSync(binaryPath) ? binaryPath : null, present: true, }; } catch (_e) { return { name: pkgName, present: false }; } } /** * Inspect an ELF binary with `readelf -V` and return the highest GLIBC_x.yy * symbol reference. Best-effort — returns null on any error. */ function detectBinaryGlibcRequired(binaryPath) { if (!binaryPath) return null; try { const out = execSync(`readelf -V ${JSON.stringify(binaryPath)}`, { stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, maxBuffer: 16 * 1024 * 1024, }).toString(); const re = /GLIBC_(\d+\.\d+(?:\.\d+)?)/g; let highest = null; let m; while ((m = re.exec(out)) !== null) { if (!highest || compareGlibc(m[1], highest) > 0) highest = m[1]; } return highest; } catch (_e) { return null; } } function writeSentinel(sentinel, homeDir) { try { fs.mkdirSync(homeDir, { recursive: true }); } catch (_e) { /* best-effort */ } const sentinelPath = path.join(homeDir, 'preflight.json'); try { fs.writeFileSync(sentinelPath, JSON.stringify(sentinel, null, 2) + '\n', { encoding: 'utf8', }); } catch (e) { // If the sentinel itself cannot be written, surface it — but still exit 0. process.stdout.write( red('✗') + ` Could not write preflight sentinel to ${sentinelPath}: ${e.message}\n` ); return null; } return sentinelPath; } function printSummary(sentinel, sentinelPath) { const parts = []; parts.push(bold('Consortium preflight')); parts.push(` platform: ${sentinel.platform}`); parts.push( ` supported: ${sentinel.supported ? green('yes') : red('no')}` ); if (process.platform === 'linux') { parts.push(` musl: ${sentinel.musl ? red('yes') : green('no')}`); parts.push( ` glibc: ${sentinel.glibcVersion || dim('unknown')}` + (sentinel.binaryGlibcRequired ? ` (binary requires ${sentinel.binaryGlibcRequired})` : '') ); } parts.push( ` platform package: ${ sentinel.platformPackagePresent ? green( `${sentinel.platformPackageName}@${sentinel.platformPackageVersion || '?'}` ) : yellow(`${sentinel.platformPackageName} (absent)`) }` ); for (const w of sentinel.warnings) { parts.push(` ${yellow('!')} ${w.code}: ${w.message}`); if (w.remediation) parts.push(` ${dim('→')} ${w.remediation}`); } for (const e of sentinel.errors) { parts.push(` ${red('✗')} ${e.code}: ${e.message}`); if (e.remediation) parts.push(` ${dim('→')} ${e.remediation}`); } if (sentinel.errors.length === 0 && sentinel.warnings.length === 0) { parts.push(` ${green('✓')} all checks passed`); } if (sentinelPath) { parts.push(dim(` sentinel: ${sentinelPath}`)); } parts.push(`Run \`consortium doctor\` for details`); process.stdout.write(parts.join('\n') + '\n'); } function main() { const ownPkg = readOwnPackageJson(); const cliPackageName = ownPkg.name || 'consortium'; const cliVersion = ownPkg.version || '0.0.0'; const homeDir = resolveConsortiumHome(cliPackageName); const platformKey = `${process.platform}-${process.arch}`; const supported = SUPPORTED_PLATFORMS.has(platformKey); const warnings = []; const errors = []; // --- musl / glibc detection (Linux only) --- const musl = process.platform === 'linux' ? detectMusl() : false; const glibcVersion = process.platform === 'linux' && !musl ? detectGlibcVersion() : null; if (musl) { errors.push({ code: 'MUSL_UNSUPPORTED', message: 'This system uses musl libc (Alpine Linux or similar). The prebuilt ' + 'consortium-code binaries are glibc-only.', remediation: 'Use the Docker fallback image or install on a glibc distro ' + '(Ubuntu, Debian, Fedora, etc.). See https://consortium.dev/docs/install', }); } // --- Resolve platform package --- let platformPkg = { name: `consortium-code-${platformKey}`, present: false }; if (supported) { platformPkg = resolvePlatformPackage(platformKey); } if (!platformPkg.present) { if (supported && PLATFORM_PKG_REQUIRED.has(platformKey)) { errors.push({ code: 'PLATFORM_PKG_MISSING', message: `The platform binary package ${platformPkg.name} is not installed. ` + 'This usually means npm skipped optionalDependencies.', remediation: `npm install -g ${platformPkg.name}`, }); } else if (supported) { // darwin — warn but don't error. Some install paths legitimately omit // the binary package (local dev, fresh macOS with skipped optionals). warnings.push({ code: 'PLATFORM_PKG_MISSING', message: `The platform binary package ${platformPkg.name} is not installed.`, remediation: `npm install -g ${platformPkg.name}`, }); } else { errors.push({ code: 'PLATFORM_UNSUPPORTED', message: `Platform ${platformKey} is not supported. Supported: ` + `${Array.from(SUPPORTED_PLATFORMS).sort().join(', ')}.`, remediation: 'Run Consortium via the Docker fallback image, or on a supported OS/arch.', }); } } // --- glibc comparison --- let binaryGlibcRequired = null; if ( process.platform === 'linux' && !musl && platformPkg.present && platformPkg.binaryPath ) { binaryGlibcRequired = detectBinaryGlibcRequired(platformPkg.binaryPath); if (binaryGlibcRequired && glibcVersion) { if (compareGlibc(glibcVersion, binaryGlibcRequired) < 0) { warnings.push({ code: 'GLIBC_LOW', message: `Host glibc ${glibcVersion} is older than the binary requirement ` + `${binaryGlibcRequired}. The CLI may fail to launch with a ` + `GLIBC_x.yy not found error.`, remediation: 'Upgrade to a newer distribution (Ubuntu 22.04+, Debian 12+, ' + 'Fedora 36+), or use the Docker fallback image.', }); } } } const sentinel = { schemaVersion: 1, platform: platformKey, supported, musl, glibcVersion: glibcVersion || null, binaryGlibcRequired: binaryGlibcRequired || null, platformPackageName: platformPkg.name, platformPackageVersion: platformPkg.version || null, platformPackagePresent: Boolean(platformPkg.present), warnings, errors, cliVersion, cliPackageName, generatedAt: new Date().toISOString(), }; const sentinelPath = writeSentinel(sentinel, homeDir); try { printSummary(sentinel, sentinelPath); } catch (e) { // Never let the summary printer break install. process.stdout.write( dim(`preflight summary failed to render: ${e.message}\n`) ); } // ALWAYS exit 0. Runtime gates on the sentinel. process.exit(0); } // Export internals for the standalone test. Only `main()` runs when the // script is invoked directly. if (require.main === module) { main(); } else { module.exports = { main, parseGlibc, compareGlibc, detectMusl, detectGlibcVersion, resolvePlatformPackage, detectBinaryGlibcRequired, resolveConsortiumHome, SUPPORTED_PLATFORMS, }; }