UNPKG

@_tribe/cli

Version:

TRIBE multi-agent development system CLI installer

686 lines (587 loc) 26.6 kB
#!/usr/bin/env node const os = require('os'); const fs = require('fs'); const path = require('path'); const https = require('https'); const { execSync, spawn } = require('child_process'); const chalk = require('chalk'); const ora = require('ora'); const which = require('which'); const platform = os.platform(); const arch = os.arch(); const homeDir = os.homedir(); const binDir = path.join(homeDir, 'bin'); // Ensure local bin directory exists if (!fs.existsSync(binDir)) { fs.mkdirSync(binDir, { recursive: true }); } const log = { success: (msg) => console.log(chalk.green('✓'), msg), error: (msg) => console.log(chalk.red('✗'), msg), warning: (msg) => console.log(chalk.yellow('⚠'), msg), info: (msg) => console.log(chalk.blue('ℹ'), msg), step: (msg) => console.log(chalk.cyan('→'), msg) }; async function checkCommand(cmd) { try { await which(cmd); return true; } catch { return false; } } async function downloadFile(url, dest) { return new Promise((resolve, reject) => { const file = fs.createWriteStream(dest); https.get(url, (response) => { if (response.statusCode === 302 || response.statusCode === 301) { // Handle redirects return downloadFile(response.headers.location, dest).then(resolve, reject); } if (response.statusCode !== 200) { reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`)); return; } response.pipe(file); file.on('finish', () => { file.close(); resolve(); }); }).on('error', reject); }); } async function installDocker() { if (await checkCommand('docker')) { // Check if Docker daemon is running try { execSync('docker info', { stdio: 'ignore' }); log.success('Docker is already working'); return true; } catch { log.warning('Docker CLI found but daemon not running'); } } const spinner = ora('Installing Docker CLI...').start(); try { let dockerUrl; if (platform === 'darwin') { // Docker uses aarch64 instead of arm64 for macOS const dockerArch = arch === 'arm64' ? 'aarch64' : 'x86_64'; dockerUrl = `https://download.docker.com/mac/static/stable/${dockerArch}/docker-24.0.7.tgz`; } else if (platform === 'linux') { dockerUrl = `https://download.docker.com/linux/static/stable/${arch}/docker-24.0.7.tgz`; } else { throw new Error(`Unsupported platform: ${platform}`); } const tempFile = path.join(os.tmpdir(), 'docker.tgz'); await downloadFile(dockerUrl, tempFile); // Extract Docker CLI execSync(`tar -xzf ${tempFile} -C ${os.tmpdir()}`); const dockerBinary = path.join(os.tmpdir(), 'docker', 'docker'); const dockerDest = path.join(binDir, 'docker'); fs.copyFileSync(dockerBinary, dockerDest); fs.chmodSync(dockerDest, '755'); // Cleanup fs.rmSync(tempFile); fs.rmSync(path.join(os.tmpdir(), 'docker'), { recursive: true }); spinner.succeed('Docker CLI installed'); return true; } catch (error) { spinner.fail(`Docker CLI installation failed: ${error.message}`); return false; } } async function installColima() { if (platform !== 'darwin') { log.info('Colima is only needed on macOS - skipping'); return true; } if (await checkCommand('colima')) { log.success('Colima already installed'); return true; } const spinner = ora('Installing Colima...').start(); try { // Strategy 1: Try Homebrew if available (better signing) try { execSync('brew --version', { stdio: 'ignore' }); spinner.text = 'Installing Colima via Homebrew...'; execSync('brew install colima', { stdio: 'ignore' }); spinner.succeed('Colima installed via Homebrew'); return true; } catch (brewError) { // Homebrew not available, continue with direct download spinner.text = 'Installing Colima (direct download)...'; } // Strategy 2: Direct download with Gatekeeper approval const colimaUrl = `https://github.com/abiosoft/colima/releases/latest/download/colima-${platform}-${arch}`; const colimaDest = path.join(binDir, 'colima'); await downloadFile(colimaUrl, colimaDest); fs.chmodSync(colimaDest, '755'); // Try to remove quarantine attribute (macOS Sequoia workaround) try { execSync(`xattr -d com.apple.quarantine ${colimaDest}`, { stdio: 'ignore' }); log.info('Removed quarantine attribute from Colima'); } catch (error) { // Quarantine attribute may not exist, which is fine log.info('Colima installed (quarantine handling not needed)'); } spinner.succeed('Colima installed'); return true; } catch (error) { spinner.fail(`Colima installation failed: ${error.message}`); return false; } } async function installLima() { if (platform !== 'darwin') { return true; // Lima only needed on macOS } if (await checkCommand('limactl')) { log.success('Lima already installed'); return true; } const spinner = ora('Installing Lima...').start(); try { // Strategy 1: Try Homebrew if available (better signing) try { execSync('brew --version', { stdio: 'ignore' }); spinner.text = 'Installing Lima via Homebrew...'; execSync('brew install lima', { stdio: 'ignore' }); spinner.succeed('Lima installed via Homebrew'); return true; } catch (brewError) { // Homebrew not available, continue with direct download spinner.text = 'Installing Lima (direct download)...'; } // Strategy 2: Direct download // Get latest Lima release const response = await fetch('https://api.github.com/repos/lima-vm/lima/releases/latest'); const release = await response.json(); const version = release.tag_name; const archName = arch === 'arm64' ? 'arm64' : 'x86_64'; const limaUrl = `https://github.com/lima-vm/lima/releases/download/${version}/lima-${version.replace('v', '')}-Darwin-${archName}.tar.gz`; const tempFile = path.join(os.tmpdir(), 'lima.tar.gz'); await downloadFile(limaUrl, tempFile); // Extract Lima const extractDir = path.join(os.tmpdir(), 'lima-extract'); fs.mkdirSync(extractDir, { recursive: true }); execSync(`tar -xzf ${tempFile} -C ${extractDir}`); // Lima tarball extracts directly to bin/ directory const limaBinDir = path.join(extractDir, 'bin'); if (fs.existsSync(limaBinDir)) { // Copy all Lima binaries const binaries = fs.readdirSync(limaBinDir); binaries.forEach(binary => { const src = path.join(limaBinDir, binary); const dest = path.join(binDir, binary); fs.copyFileSync(src, dest); fs.chmodSync(dest, '755'); // Remove quarantine attribute try { execSync(`xattr -d com.apple.quarantine ${dest}`, { stdio: 'ignore' }); } catch (error) { // Quarantine attribute may not exist, which is fine } }); } else { throw new Error('Lima binaries not found in expected location'); } // Copy share directory (required for guest agents) const limaShareDir = path.join(extractDir, 'share'); const destShareDir = path.join(homeDir, 'share'); if (fs.existsSync(limaShareDir)) { if (!fs.existsSync(destShareDir)) { fs.mkdirSync(destShareDir, { recursive: true }); } try { // Create lima directory in share const limaDir = path.join(destShareDir, 'lima'); if (!fs.existsSync(limaDir)) { fs.mkdirSync(limaDir, { recursive: true }); } // Strategy 1: Try bundled guest agent (packaged with NPX) const bundledGuestAgent = path.join(__dirname, 'lima-guestagent.Linux-aarch64.gz'); if (fs.existsSync(bundledGuestAgent)) { log.info('Using bundled Lima guest agent'); const guestAgentDest = path.join(limaDir, 'lima-guestagent.Linux-aarch64.gz'); fs.copyFileSync(bundledGuestAgent, guestAgentDest); // Extract the guest agent (Colima needs it uncompressed) try { execSync(`gunzip -f "${guestAgentDest}"`); } catch (gunzipError) { // If gunzip fails, try manual extraction const zlib = require('zlib'); const compressed = fs.readFileSync(guestAgentDest); const decompressed = zlib.gunzipSync(compressed); const extractedPath = guestAgentDest.replace('.gz', ''); fs.writeFileSync(extractedPath, decompressed); fs.unlinkSync(guestAgentDest); // Remove compressed version } // Create basic default template for Colima const templatesDir = path.join(limaDir, 'templates'); if (!fs.existsSync(templatesDir)) { fs.mkdirSync(templatesDir, { recursive: true }); } const defaultTemplate = `# Basic default template for Colima images: - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img" arch: "x86_64" - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img" arch: "aarch64" mounts: - location: "~" writable: false - location: "/tmp/lima" writable: true containerd: system: false user: false `; fs.writeFileSync(path.join(templatesDir, 'default.yaml'), defaultTemplate); log.info('Created basic Lima template'); } else { // Strategy 2: Try from tarball (fallback) const guestAgentSrc = path.join(limaShareDir, 'lima-guestagent.Linux-aarch64.gz'); const templatesSrc = path.join(limaShareDir, 'templates'); if (fs.existsSync(guestAgentSrc)) { // Copy and extract guest agent const guestAgentDest = path.join(limaDir, 'lima-guestagent.Linux-aarch64.gz'); fs.copyFileSync(guestAgentSrc, guestAgentDest); // Extract the guest agent (Colima needs it uncompressed) try { execSync(`gunzip -f "${guestAgentDest}"`); } catch (gunzipError) { // If gunzip fails, try manual extraction const zlib = require('zlib'); const compressed = fs.readFileSync(guestAgentDest); const decompressed = zlib.gunzipSync(compressed); const extractedPath = guestAgentDest.replace('.gz', ''); fs.writeFileSync(extractedPath, decompressed); fs.unlinkSync(guestAgentDest); // Remove compressed version } } else { throw new Error('Lima guest agent not found in tarball or bundled'); } if (fs.existsSync(templatesSrc)) { // Copy templates directory execSync(`cp -R "${templatesSrc}" "${limaDir}/templates"`); } else { // Create minimal template as fallback const templatesDir = path.join(limaDir, 'templates'); fs.mkdirSync(templatesDir, { recursive: true }); fs.writeFileSync(path.join(templatesDir, 'default.yaml'), defaultTemplate); } } } catch (error) { log.warning(`Lima share installation failed: ${error.message}`); log.warning('You may need to install Lima manually: brew install lima'); } } // Cleanup fs.rmSync(tempFile); fs.rmSync(extractDir, { recursive: true }); spinner.succeed('Lima installed'); return true; } catch (error) { spinner.fail(`Lima installation failed: ${error.message}`); log.warning('You may need to install Lima manually: brew install lima'); return false; } } async function installKind() { if (await checkCommand('kind')) { log.success('KIND already installed'); return true; } const spinner = ora('Installing KIND...').start(); try { const kindUrl = `https://kind.sigs.k8s.io/dl/v0.20.0/kind-${platform}-${arch}`; const kindDest = path.join(binDir, 'kind'); await downloadFile(kindUrl, kindDest); fs.chmodSync(kindDest, '755'); spinner.succeed('KIND installed'); return true; } catch (error) { spinner.fail(`KIND installation failed: ${error.message}`); return false; } } async function installKubectl() { if (await checkCommand('kubectl')) { log.success('kubectl already installed'); return true; } const spinner = ora('Installing kubectl...').start(); try { // Get latest stable version const versionResponse = await fetch('https://dl.k8s.io/release/stable.txt'); const version = await versionResponse.text(); const kubectlUrl = `https://dl.k8s.io/release/${version.trim()}/bin/${platform}/${arch}/kubectl`; const kubectlDest = path.join(binDir, 'kubectl'); await downloadFile(kubectlUrl, kubectlDest); fs.chmodSync(kubectlDest, '755'); spinner.succeed('kubectl installed'); return true; } catch (error) { spinner.fail(`kubectl installation failed: ${error.message}`); return false; } } async function installTribeCli() { const spinner = ora('Installing TRIBE CLI...').start(); try { // Check if we have local source const sourceFile = path.join(__dirname, 'cluster-cli.go'); if (fs.existsSync(sourceFile)) { // Build from source try { execSync('go version', { stdio: 'ignore' }); execSync(`cd ${__dirname} && go build -o tribe cluster-cli.go client.go`); const tribeDest = path.join(binDir, 'tribe'); fs.copyFileSync(path.join(__dirname, 'tribe'), tribeDest); fs.chmodSync(tribeDest, '755'); spinner.succeed('TRIBE CLI built from source'); return true; } catch { spinner.warn('Go not available, trying pre-built binary...'); } } // Try pre-built binary const tribeUrl = `https://github.com/0zen/0zen/releases/latest/download/tribe-${platform}-${arch}`; const tribeDest = path.join(binDir, 'tribe'); try { await downloadFile(tribeUrl, tribeDest); fs.chmodSync(tribeDest, '755'); spinner.succeed('TRIBE CLI installed'); return true; } catch { // Fallback: copy local binary if it exists const localBinary = path.join(__dirname, 'tribe'); if (fs.existsSync(localBinary)) { fs.copyFileSync(localBinary, tribeDest); fs.chmodSync(tribeDest, '755'); spinner.succeed('TRIBE CLI installed from local binary'); return true; } throw new Error('No TRIBE CLI binary available'); } } catch (error) { spinner.fail(`TRIBE CLI installation failed: ${error.message}`); return false; } } async function startContainerRuntime() { if (platform !== 'darwin') { return true; // Linux uses Docker daemon directly } // Check if container runtime is already working try { execSync('docker info', { stdio: 'ignore' }); log.success('Container runtime is already working'); return true; } catch { // Try to start Colima with different approaches if (await checkCommand('colima')) { const spinner = ora('Starting Colima container runtime...').start(); try { // Strategy 1: Quick start with minimal resources spinner.text = 'Starting Colima (minimal setup)...'; execSync('colima start --cpu 1 --memory 2 --disk 5 --vm-type=vz', { stdio: 'pipe', timeout: 30000 // 30 second timeout }); // Test if it worked execSync('docker info', { stdio: 'ignore' }); spinner.succeed('Colima started successfully'); return true; } catch (error) { // Strategy 2: Start in background and don't wait try { spinner.text = 'Starting Colima in background...'; const child = spawn(path.join(binDir, 'colima'), ['start', '--cpu', '2', '--memory', '4'], { detached: true, stdio: 'ignore', env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` } }); child.unref(); // Don't wait for completion spinner.succeed('Colima startup initiated (background)'); log.info('Colima is starting in the background'); log.info('Run "colima status" to check progress'); log.info('Run "docker info" to test when ready'); return true; } catch (bgError) { spinner.fail('Failed to start Colima'); log.warning('Container runtime startup failed (likely due to macOS system restrictions)'); log.info('Options to fix:'); log.info(''); log.info('Option 1 - Manual Colima start:'); log.info(' colima start --cpu 2 --memory 4 # Start container runtime'); log.info(' # This downloads a 344MB disk image (may take time)'); log.info(''); log.info('Option 2 - Use Docker Desktop (easier):'); log.info(' Download from: https://docs.docker.com/desktop/install/mac-install/'); log.info(' # Docker Desktop handles all container runtime setup'); log.info(''); log.info('Option 3 - Use Homebrew (recommended):'); log.info(' brew install colima docker'); log.info(' colima start'); log.info(''); return false; } } } } return false; } async function updatePath() { const shell = process.env.SHELL || '/bin/zsh'; const rcFile = shell.includes('zsh') ? '.zshrc' : '.bashrc'; const rcPath = path.join(homeDir, rcFile); const pathExport = `export PATH="${binDir}:$PATH"`; try { const rcContent = fs.existsSync(rcPath) ? fs.readFileSync(rcPath, 'utf8') : ''; if (!rcContent.includes(pathExport)) { fs.appendFileSync(rcPath, `\n# Added by TRIBE CLI installer\n${pathExport}\n`); log.success(`Updated ${rcFile} with PATH`); } } catch (error) { log.warning(`Failed to update ${rcFile}: ${error.message}`); } // Update current process PATH process.env.PATH = `${binDir}:${process.env.PATH}`; } async function verifyInstallation() { const spinner = ora('Verifying installation...').start(); const tools = ['docker', 'kind', 'kubectl', 'tribe']; const results = {}; for (const tool of tools) { results[tool] = await checkCommand(tool); } // Special check for container runtime let containerWorking = false; try { execSync('docker info', { stdio: 'ignore' }); containerWorking = true; } catch (error) { containerWorking = false; } spinner.stop(); console.log('\n' + chalk.bold('Installation Summary:')); tools.forEach(tool => { const status = results[tool] ? chalk.green('✓') : chalk.red('✗'); console.log(`${status} ${tool}`); }); const extraStatus = containerWorking ? chalk.green('✓') : chalk.yellow('⚠'); console.log(`${extraStatus} Container runtime`); if (platform === 'darwin') { const colimaInstalled = await checkCommand('colima'); const limaInstalled = await checkCommand('limactl'); console.log(`${colimaInstalled ? chalk.green('✓') : chalk.yellow('⚠')} Colima`); console.log(`${limaInstalled ? chalk.green('✓') : chalk.yellow('⚠')} Lima`); } return Object.values(results).every(r => r) && containerWorking; } async function main() { console.log(chalk.bold.blue('\n🚀 TRIBE CLI Complete Installer\n')); log.info(`Detected: ${platform} (${arch})`); log.info(`Installing to: ${binDir}`); // Update PATH first await updatePath(); const tasks = [ { name: 'Docker CLI', fn: installDocker }, { name: 'Colima', fn: installColima }, { name: 'Lima', fn: installLima }, { name: 'KIND', fn: installKind }, { name: 'kubectl', fn: installKubectl }, { name: 'TRIBE CLI', fn: installTribeCli } ]; let allSuccess = true; for (const task of tasks) { log.step(`Installing ${task.name}...`); const success = await task.fn(); if (!success) allSuccess = false; } // Try to start container runtime await startContainerRuntime(); // Verify everything const verified = await verifyInstallation(); console.log('\n' + chalk.bold('Next Steps:')); if (verified) { log.success('All components installed successfully! 🎉'); console.log(''); log.info('Quick start:'); console.log(' tribe start # Start TRIBE cluster'); console.log(' tribe status # Check cluster status'); console.log(' tribe deploy-task # Deploy tasks to agents'); console.log(''); log.info('For help: tribe --help'); } else { log.warning('Some components need attention:'); console.log(''); // Check container runtime for guidance let runtimeWorking = false; try { execSync('docker info', { stdio: 'ignore' }); runtimeWorking = true; } catch { runtimeWorking = false; } if (!runtimeWorking) { log.info('Start container runtime:'); console.log(' colima start # macOS'); console.log(' sudo systemctl start docker # Linux'); } console.log(''); log.info('Restart your shell or run: source ~/.zshrc'); } } // Handle fetch polyfill for older Node versions if (!global.fetch) { global.fetch = require('node-fetch'); } if (require.main === module) { const args = process.argv.slice(2); if (args.includes('--help') || args.includes('-h')) { console.log(chalk.bold.blue('TRIBE CLI Installer\n')); console.log('Usage: npx tribe-cli-local [options]\n'); console.log('Options:'); console.log(' --help, -h Show this help message'); console.log(' --verify Only verify existing installation'); console.log(' --dry-run Show what would be installed'); console.log('\nThis installer sets up the complete TRIBE development environment:'); console.log('• Docker CLI + Colima (macOS container runtime)'); console.log('• KIND (Kubernetes in Docker)'); console.log('• kubectl (Kubernetes CLI)'); console.log('• TRIBE CLI (Multi-agent orchestration)'); console.log('\nAfter installation:'); console.log(' tribe start # Start TRIBE cluster'); console.log(' tribe status # Check cluster status'); process.exit(0); } if (args.includes('--verify')) { console.log(chalk.bold.blue('🔍 Verifying TRIBE Installation\n')); verifyInstallation().then(success => { process.exit(success ? 0 : 1); }); return; } if (args.includes('--dry-run')) { console.log(chalk.bold.blue('🔍 TRIBE CLI Installation Preview\n')); log.info(`Platform: ${platform} (${arch})`); log.info(`Install directory: ${binDir}`); console.log('\nWould install:'); console.log('• Docker CLI (if not present)'); console.log('• Colima container runtime (macOS only)'); console.log('• Lima virtualization (macOS only)'); console.log('• KIND - Kubernetes in Docker'); console.log('• kubectl - Kubernetes CLI'); console.log('• TRIBE CLI - Multi-agent system'); process.exit(0); } main().catch(error => { console.error(chalk.red('Installation failed:'), error.message); process.exit(1); }); } module.exports = { main };