UNPKG

@humanu/orchestra

Version:

AI-powered Git worktree and tmux session manager with modern TUI

380 lines (327 loc) 11.3 kB
#!/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();