UNPKG

tribe-cli

Version:

TRIBE multi-agent development system - Zero to productive with one command

881 lines (753 loc) 33.8 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'); const tribeDir = path.join(homeDir, '.tribe'); // Ensure local bin directory exists if (!fs.existsSync(binDir)) { fs.mkdirSync(binDir, { recursive: true }); } // Ensure TRIBE config directory exists if (!fs.existsSync(tribeDir)) { fs.mkdirSync(tribeDir, { 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 { const tribeDest = path.join(binDir, 'tribe'); // First check if we have a bundled binary const bundledBinary = path.join(__dirname, 'tribe'); if (fs.existsSync(bundledBinary)) { fs.copyFileSync(bundledBinary, tribeDest); fs.chmodSync(tribeDest, '755'); spinner.succeed('TRIBE CLI installed from bundled binary'); return true; } // 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`); 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 from GitHub const tribeUrl = `https://github.com/0zen/0zen/releases/latest/download/tribe-${platform}-${arch}`; try { await downloadFile(tribeUrl, tribeDest); fs.chmodSync(tribeDest, '755'); spinner.succeed('TRIBE CLI installed'); return true; } catch { // Fallback: look for any existing tribe binary const possiblePaths = [ path.join(__dirname, '..', 'tribe-cli'), path.join(__dirname, '..', 'tribe'), './tribe-cli', './tribe' ]; for (const possiblePath of possiblePaths) { if (fs.existsSync(possiblePath)) { fs.copyFileSync(possiblePath, 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 checkClusterExists() { try { // Check if TRIBE namespace exists in any context execSync('kubectl get namespace tribe-system', { stdio: 'ignore' }); return true; } catch { return false; } } async function checkColimaRunning() { try { execSync('colima status', { stdio: 'ignore' }); return true; } catch { return false; } } async function startColimaWithKubernetes() { const spinner = ora('Starting Colima with Kubernetes...').start(); try { // Check if already running if (await checkColimaRunning()) { spinner.succeed('Colima is already running'); return true; } // Start Colima with Kubernetes enabled spinner.text = 'Starting Colima (this may take a few minutes on first run)...'; execSync('colima start --kubernetes --cpu 4 --memory 8 --disk 20', { stdio: 'pipe', env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` } }); // Verify it's working execSync('kubectl version --client', { stdio: 'ignore' }); spinner.succeed('Colima started with Kubernetes'); return true; } catch (error) { spinner.fail('Failed to start Colima with Kubernetes'); log.error(error.message); return false; } } async function deployTribeCluster() { const spinner = ora('Deploying TRIBE cluster...').start(); try { // Create a marker file to indicate first-run deployment const deploymentMarker = path.join(tribeDir, '.cluster-deployed'); // Check if we've already deployed if (fs.existsSync(deploymentMarker)) { // Check if cluster actually exists if (await checkClusterExists()) { spinner.succeed('TRIBE cluster already deployed'); return true; } // Marker exists but cluster doesn't - remove marker and redeploy fs.unlinkSync(deploymentMarker); } // Copy deployment YAML to .tribe directory const sourceYaml = path.join(__dirname, 'tribe-deployment.yaml'); const destYaml = path.join(tribeDir, 'tribe-deployment.yaml'); if (fs.existsSync(sourceYaml)) { fs.copyFileSync(sourceYaml, destYaml); log.info('Copied deployment configuration'); } // Run tribe start command with deployment YAML path spinner.text = 'Running TRIBE cluster deployment...'; const tribePath = path.join(binDir, 'tribe'); // Set environment variable for the deployment YAML location const env = { ...process.env, PATH: `${binDir}:${process.env.PATH}`, TRIBE_DEPLOYMENT_YAML: destYaml }; // Execute tribe start execSync(`${tribePath} start`, { stdio: 'pipe', env: env }); // Create marker file fs.writeFileSync(deploymentMarker, new Date().toISOString()); spinner.succeed('TRIBE cluster deployed successfully'); return true; } catch (error) { spinner.fail('Failed to deploy TRIBE cluster'); log.error(error.message); log.info('You can manually deploy later with: tribe start'); return false; } } async function promptForClusterSetup() { // Simple prompt without external dependencies return new Promise((resolve) => { console.log('\n' + chalk.bold('🚀 TRIBE Cluster Setup')); console.log('\nWould you like to set up the TRIBE cluster now?'); console.log('This will:'); console.log(' • Start Colima with Kubernetes'); console.log(' • Deploy all TRIBE services'); console.log(' • Set up port forwarding'); console.log('\n' + chalk.yellow('Note: This requires ~2GB disk space and may take a few minutes')); process.stdout.write('\nSet up now? [Y/n]: '); process.stdin.resume(); process.stdin.setEncoding('utf8'); process.stdin.once('data', (data) => { process.stdin.pause(); const answer = data.toString().trim().toLowerCase(); resolve(answer === '' || answer === 'y' || answer === 'yes'); }); }); } 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(); if (verified) { // Check if cluster already exists const clusterExists = await checkClusterExists(); if (!clusterExists) { // Prompt for cluster setup const shouldSetup = await promptForClusterSetup(); if (shouldSetup) { console.log(''); // Start Colima with Kubernetes if on macOS if (platform === 'darwin') { const colimaStarted = await startColimaWithKubernetes(); if (!colimaStarted) { log.error('Failed to start Colima. Please run manually:'); console.log(' colima start --kubernetes'); console.log(' tribe start'); process.exit(1); } } // Deploy TRIBE cluster const deployed = await deployTribeCluster(); if (deployed) { console.log('\n' + chalk.bold.green('✨ TRIBE is ready!')); console.log(''); log.info('Quick start:'); console.log(' tribe # Launch interactive CLI'); console.log(' tribe status # Check cluster status'); console.log(' tribe create-task # Create a new task'); console.log(''); log.info('First time? The CLI will guide you through creating your first project!'); } else { log.warning('Cluster deployment failed, but you can try manually:'); console.log(' tribe start'); } } else { console.log('\n' + chalk.bold('Setup Complete!')); log.info('You can set up the cluster later with:'); console.log(' tribe start'); } } else { console.log('\n' + chalk.bold.green('✨ TRIBE is ready!')); log.success('Cluster is already running'); console.log(''); log.info('Commands:'); console.log(' tribe # Launch interactive CLI'); console.log(' tribe status # Check status'); console.log(' tribe create-task # Create a new task'); } } else { console.log('\n' + chalk.bold('Next Steps:')); 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'); log.info('Then run: tribe start'); } } // 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 };