@humanu/orchestra
Version:
AI-powered Git worktree and tmux session manager with modern TUI
380 lines (327 loc) • 11.3 kB
JavaScript
#!/usr/bin/env node
const { execSync, spawnSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const BINARY_NAME = 'orchestra';
const SHELL_SCRIPTS = ['gwr.sh', 'gw.sh', 'gw-bridge.sh', 'copy_env.sh', 'orchestra-local.sh'];
const SHELL_DIRS = ['shell'];
const SUPPORTED_PLATFORMS = {
'darwin-x64': 'macos-intel',
'darwin-arm64': 'macos-arm64',
'linux-x64': 'linux-x64',
'linux-arm64': 'linux-arm64',
};
const packageRoot = __dirname;
const resourcesDir = path.join(packageRoot, 'resources');
const distDir = path.join(packageRoot, 'dist');
const scriptsDir = path.join(resourcesDir, 'scripts');
const apiDir = path.join(resourcesDir, 'api');
const prebuiltDir = path.join(resourcesDir, 'prebuilt');
const projectRoot = path.resolve(packageRoot, '..', '..');
const hasLocalSource = fs.existsSync(path.join(projectRoot, 'gw-tui'));
function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
function cleanDir(dir) {
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true, force: true });
}
ensureDir(dir);
}
function getPlatformKey() {
const key = `${process.platform}-${process.arch}`;
const mapped = SUPPORTED_PLATFORMS[key];
if (!mapped) {
console.error(`Unsupported platform: ${key}`);
console.error(`Supported platforms: ${Object.keys(SUPPORTED_PLATFORMS).join(', ')}`);
process.exit(1);
}
return mapped;
}
function copyRecursive(src, dest) {
const stats = fs.statSync(src);
if (stats.isDirectory()) {
ensureDir(dest);
for (const entry of fs.readdirSync(src)) {
copyRecursive(path.join(src, entry), path.join(dest, entry));
}
} else {
ensureDir(path.dirname(dest));
fs.copyFileSync(src, dest);
fs.chmodSync(dest, 0o755);
}
}
function copyShellScripts() {
let sourceBase = scriptsDir;
if (!fs.existsSync(sourceBase)) {
if (!hasLocalSource) {
console.error('Shell scripts are missing from the package.');
process.exit(1);
}
sourceBase = projectRoot;
}
for (const script of SHELL_SCRIPTS) {
const sourcePath = path.join(sourceBase, script);
if (!fs.existsSync(sourcePath)) {
console.error(`Required script missing: ${sourcePath}`);
process.exit(1);
}
const destinationPath = path.join(distDir, script);
copyRecursive(sourcePath, destinationPath);
}
for (const dirName of SHELL_DIRS) {
const sourcePath = path.join(sourceBase, dirName);
if (!fs.existsSync(sourcePath)) {
console.error(`Required shell directory missing: ${sourcePath}`);
process.exit(1);
}
const destinationPath = path.join(distDir, dirName);
copyRecursive(sourcePath, destinationPath);
}
}
function copyApiScripts() {
let sourceBase = apiDir;
if (!fs.existsSync(sourceBase)) {
if (!hasLocalSource) {
console.error('API scripts are missing from the package.');
process.exit(1);
}
sourceBase = path.join(projectRoot, 'api');
}
const destApiDir = path.join(distDir, 'api');
ensureDir(destApiDir);
const apiFiles = fs.readdirSync(sourceBase).filter((file) => file.endsWith('.sh'));
for (const file of apiFiles) {
const sourcePath = path.join(sourceBase, file);
const destinationPath = path.join(destApiDir, file);
fs.copyFileSync(sourcePath, destinationPath);
fs.chmodSync(destinationPath, 0o755);
}
}
function linkCompatibilityBinary(binaryPath) {
const gwTuiPath = path.join(distDir, 'gw-tui');
try {
fs.rmSync(gwTuiPath, { force: true });
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
try {
fs.symlinkSync(binaryPath, gwTuiPath);
} catch (err) {
fs.copyFileSync(binaryPath, gwTuiPath);
fs.chmodSync(gwTuiPath, 0o755);
}
}
function installPrebuiltBinary() {
const platform = getPlatformKey();
const source = path.join(prebuiltDir, platform, BINARY_NAME);
if (!fs.existsSync(source)) {
console.log(`⚠️ No prebuilt binary found for ${platform}`);
// Check if we have binaries for other platforms (for debugging)
if (fs.existsSync(prebuiltDir)) {
const availablePlatforms = fs.readdirSync(prebuiltDir).filter(item => {
const platformPath = path.join(prebuiltDir, item);
return fs.statSync(platformPath).isDirectory() &&
fs.existsSync(path.join(platformPath, BINARY_NAME));
});
if (availablePlatforms.length > 0) {
console.log(` Available platforms: ${availablePlatforms.join(', ')}`);
}
}
return false;
}
const destination = path.join(distDir, BINARY_NAME);
fs.copyFileSync(source, destination);
fs.chmodSync(destination, 0o755);
linkCompatibilityBinary(destination);
return true;
}
function buildFromSource() {
if (!hasLocalSource) {
console.error('❌ No prebuilt binary available and source tree not found.');
console.error('');
console.error('This usually happens when:');
console.error('1. The npm package was built on a different platform (e.g., macOS vs Linux)');
console.error('2. The binary for your platform is missing from the package');
console.error('');
console.error('Solutions:');
console.error('1. Install from source (requires Rust toolchain):');
console.error(' git clone https://github.com/humanunsupervised/orchestra.git');
console.error(' cd orchestra');
console.error(' cargo build --release');
console.error('');
console.error('2. Use a pre-built release:');
console.error(' Download from: https://github.com/humanunsupervised/orchestra/releases');
console.error('');
console.error('3. Install Rust and build locally:');
console.error(' curl --proto \'=https\' --tlsv1.2 -sSf https://sh.rustup.rs | sh');
console.error(' source ~/.cargo/env');
console.error(' cargo build --release');
process.exit(1);
}
try {
execSync('cargo --version', { stdio: 'ignore' });
} catch (error) {
console.error('❌ Rust toolchain not found.');
console.error('');
console.error('Install Rust from https://rustup.rs/');
console.error('Then run: curl --proto \'=https\' --tlsv1.2 -sSf https://sh.rustup.rs | sh');
process.exit(1);
}
console.log('🔨 Building from source... (this may take a few minutes)');
console.log(' Working directory:', path.join(projectRoot, 'gw-tui'));
try {
execSync('cargo build --release', { stdio: 'inherit', cwd: path.join(projectRoot, 'gw-tui') });
} catch (error) {
console.error('❌ Build failed.');
console.error('');
console.error('Common issues:');
console.error('- Missing system dependencies (openssl, pkg-config, etc.)');
console.error('- Network issues during dependency download');
console.error('');
console.error('Try installing dependencies:');
console.error('- Ubuntu/Debian: sudo apt-get install libssl-dev pkg-config');
console.error('- macOS: brew install openssl pkg-config');
console.error('- Fedora: sudo dnf install openssl-devel pkgconfig');
process.exit(1);
}
const builtBinary = path.join(projectRoot, 'gw-tui', 'target', 'release', 'gw-tui');
if (!fs.existsSync(builtBinary)) {
console.error('❌ Build finished but gw-tui binary was not produced.');
console.error(' Expected location:', builtBinary);
process.exit(1);
}
const destination = path.join(distDir, BINARY_NAME);
fs.copyFileSync(builtBinary, destination);
fs.chmodSync(destination, 0o755);
linkCompatibilityBinary(destination);
console.log('✅ Successfully built from source!');
}
function assetsReady() {
return fs.existsSync(path.join(distDir, BINARY_NAME)) && fs.existsSync(path.join(distDir, 'gwr.sh'));
}
function printShellWrapperInstructions(binaryPath) {
const distPathEscaped = distDir.split('\\').join('\\\\');
const binaryPathEscaped = binaryPath.split('\\').join('\\\\');
console.log('\n📝 Shell wrapper functions needed for directory switching:\n');
console.log(`# GW Orchestrator shell wrappers
gwr() {
local dist_path="${distPathEscaped}"
local bin_path="${binaryPathEscaped}"
local out="$(GW_TUI_BIN="$bin_path" bash "$dist_path/gwr.sh" "$@")"
local status=$?
local cd_line="$(echo "$out" | grep -m1 '^cd')"
[[ -n $cd_line ]] && eval "$cd_line"
echo "$out" | grep -v '^cd'
return $status
}
gw() {
local dist_path="${distPathEscaped}"
local out="$(bash "$dist_path/gw.sh" "$@")"
local status=$?
local cd_line="$(echo "$out" | grep -m1 '^cd')"
[[ -n $cd_line ]] && eval "$cd_line"
echo "$out" | grep -v '^cd'
return $status
}`);
console.log('\nAdd these to your ~/.bashrc or ~/.zshrc, then run: source ~/.bashrc (or source ~/.zshrc)\n');
}
function performSetup({ quiet = false, showInstructions = false } = {}) {
if (!quiet) {
console.log('Installing Orchestra...\n');
}
cleanDir(distDir);
copyShellScripts();
copyApiScripts();
const binaryInstalled = installPrebuiltBinary();
if (!binaryInstalled) {
buildFromSource();
}
const binaryPath = path.join(distDir, BINARY_NAME);
if (!quiet) {
console.log('\n✅ Installation complete!');
}
if (showInstructions) {
printShellWrapperInstructions(binaryPath);
}
}
function ensureAssets() {
if (!assetsReady()) {
performSetup({ quiet: false, showInstructions: false });
}
}
function runCommand(command, args) {
ensureAssets();
let scriptName;
if (command === 'gw') {
scriptName = 'gw.sh';
} else if (command === 'orchestra-local') {
scriptName = 'orchestra-local.sh';
} else {
scriptName = 'gwr.sh';
}
const scriptPath = path.join(distDir, scriptName);
if (!fs.existsSync(scriptPath)) {
console.error(`Missing script: ${scriptPath}`);
process.exit(1);
}
const env = { ...process.env };
if (command !== 'gw' && command !== 'orchestra-local') {
env.GW_TUI_BIN = path.join(distDir, BINARY_NAME);
}
const result = spawnSync('bash', [scriptPath, ...args], {
stdio: 'inherit',
env,
});
if (result.error) {
console.error(`Failed to launch ${command}:`, result.error.message);
process.exit(result.status ?? 1);
}
process.exit(result.status ?? 0);
}
function parseArgs() {
const argv = process.argv.slice(2);
let invoke = null;
const args = [];
for (let i = 0; i < argv.length; i += 1) {
const value = argv[i];
if (value === '--invoke' && i + 1 < argv.length) {
invoke = argv[i + 1];
i += 1;
continue;
}
if (value.startsWith('--invoke=')) {
invoke = value.slice('--invoke='.length);
continue;
}
args.push(value);
}
return { invoke, args };
}
function main() {
const lifecycle = process.env.npm_lifecycle_event;
const { invoke, args } = parseArgs();
if (lifecycle === 'postinstall') {
performSetup({ quiet: false, showInstructions: true });
return;
}
if (invoke) {
let command;
if (invoke === 'gw') {
command = 'gw';
} else if (invoke === 'orchestra-local') {
command = 'orchestra-local';
} else {
command = 'gwr';
}
runCommand(command, args);
return;
}
// If someone runs `node install.js` manually, treat it as a setup utility.
performSetup({ quiet: false, showInstructions: true });
}
main();