consortium
Version:
Remote control and session sharing CLI for AI coding agents
450 lines (421 loc) • 14.1 kB
JavaScript
/**
* 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.
*/
;
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,
};
}