consortium
Version:
Remote control and session sharing CLI for AI coding agents
140 lines (122 loc) • 5.07 kB
JavaScript
import { execFileSync } from 'child_process';
import { fileURLToPath } from 'url';
import { join, dirname } from 'path';
import { existsSync, mkdirSync, appendFileSync, statSync, readFileSync, writeFileSync } from 'fs';
import { homedir } from 'os';
import { createRequire } from 'module';
// --- Phase 0: silent-exit visibility -----------------------------------------
// Boot banner, duration footer, and boot-trace log. Best-effort — never throw
// from the bin wrapper, even if the log directory is unwritable.
const require = createRequire(import.meta.url);
// eslint-disable-next-line @typescript-eslint/no-require-imports
const pkg = require('../package.json');
const VERSION = pkg.version || 'unknown';
function resolveConsortiumHomeDir() {
// Priority: env CONSORTIUM_HOME_DIR > dev-variant heuristic > default.
// Mirrors src/configuration.ts + bin/consortium-dev.mjs.
const envDir = process.env.CONSORTIUM_HOME_DIR;
if (envDir) {
return envDir.replace(/^~/, homedir());
}
// Dev variant (published as dev-hummingbird-sneakers) keys off package name.
const name = typeof pkg.name === 'string' ? pkg.name : '';
if (name === 'dev-hummingbird-sneakers' || process.env.CONSORTIUM_VARIANT === 'dev') {
return join(homedir(), '.consortium-dev');
}
return join(homedir(), '.consortium');
}
const BOOT_TRACE_MAX_BYTES = 1024 * 1024; // 1 MB
function writeBootTrace() {
try {
const homeDir = resolveConsortiumHomeDir();
const logsDir = join(homeDir, 'logs');
if (!existsSync(logsDir)) {
mkdirSync(logsDir, { recursive: true, mode: 0o700 });
}
const tracePath = join(logsDir, 'boot-trace.log');
const entry = JSON.stringify({
ts: new Date().toISOString(),
pid: process.pid,
version: VERSION,
args: process.argv.slice(2),
node: process.versions.node,
platform: `${process.platform}-${process.arch}`,
}) + '\n';
// Rotate by truncating from the front when > 1 MB.
if (existsSync(tracePath)) {
try {
const st = statSync(tracePath);
if (st.size + Buffer.byteLength(entry) > BOOT_TRACE_MAX_BYTES) {
const raw = readFileSync(tracePath, 'utf8');
// Keep the last ~half of the file so we retain recent history.
const half = Math.floor(BOOT_TRACE_MAX_BYTES / 2);
const sliced = raw.length > half ? raw.slice(raw.length - half) : raw;
// Drop any leading partial line from the front-truncation.
const firstNewline = sliced.indexOf('\n');
const trimmed = firstNewline >= 0 ? sliced.slice(firstNewline + 1) : sliced;
writeFileSync(tracePath, trimmed, { mode: 0o600 });
}
} catch {
/* best-effort rotation */
}
}
appendFileSync(tracePath, entry, { mode: 0o600 });
} catch {
/* best-effort — never throw from the bin wrapper */
}
}
function writeStderr(msg) {
try {
process.stderr.write(msg);
} catch {
/* ignore */
}
}
// Boot banner — last-resort signal that the CLI started.
writeStderr(`[consortium] starting v${VERSION} pid=${process.pid}\n`);
writeBootTrace();
const bootStart = process.hrtime.bigint();
function emitExitFooter(code) {
const ms = Number((process.hrtime.bigint() - bootStart) / 1_000_000n);
writeStderr(`[consortium] exited code=${code} duration=${ms}ms\n`);
}
// --- end Phase 0 --------------------------------------------------------------
// Dispatcher note: this wrapper just forwards to dist/index.mjs and lets the
// CLI entrypoint decide what to do. When `process.argv.length <= 2` (i.e. the
// user ran `consortium` with no subcommand) the entrypoint renders the ink
// TUI menu; otherwise the existing subcommand dispatcher handles things.
// Keeping the branch in TypeScript means the TUI imports (ink, react) live
// in the bundled dist and don't need to be duplicated at the bin layer.
// Check if we're already running with the flags
const hasNoWarnings = process.execArgv.includes('--no-warnings');
const hasNoDeprecation = process.execArgv.includes('--no-deprecation');
if (!hasNoWarnings || !hasNoDeprecation) {
// Get path to the actual CLI entrypoint
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const entrypoint = join(projectRoot, 'dist', 'index.mjs');
// Execute the actual CLI directly with the correct flags
let exitCode = 0;
try {
execFileSync(process.execPath, [
'--no-warnings',
'--no-deprecation',
entrypoint,
...process.argv.slice(2)
], {
stdio: 'inherit',
env: process.env
});
} catch (error) {
// execFileSync throws if the process exits with non-zero
exitCode = error.status || 1;
emitExitFooter(exitCode);
process.exit(exitCode);
}
emitExitFooter(exitCode);
} else {
// We're running Node with the flags we wanted, import the CLI entrypoint
// module to avoid creating a new process. Best-effort footer on exit.
process.on('exit', (code) => emitExitFooter(code));
import("../dist/index.mjs");
}