@_tribe/cli
Version:
TRIBE multi-agent development system CLI installer
686 lines (587 loc) • 26.6 kB
JavaScript
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 };