UNPKG

terminal-jarvis

Version:

AI Coding Tools Wrapper - Unified interface for claude-code, gemini-cli, qwen-code, opencode, llxprt, codex, crush, goose, amp, and aider

309 lines (256 loc) 11.1 kB
#!/usr/bin/env node /* Enhanced postinstall script for terminal-jarvis - Detect OS and architecture - Download platform-specific binary from GitHub releases with retry logic - Stream extraction for faster install (no temp files) - Parallel progress display - Verify installation Version Hint (used by CI for consistency checks): Terminal Jarvis v0.0.73 */ const fs = require('fs'); const os = require('os'); const path = require('path'); const https = require('https'); const { spawnSync, spawn } = require('child_process'); const { createWriteStream } = require('fs'); const zlib = require('zlib'); const pkg = require('../package.json'); // Configuration - optimized for faster downloads const DOWNLOAD_RETRIES = 3; const DOWNLOAD_TIMEOUT = 60000; // 60 seconds (increased for slow connections) const GITHUB_REPO = 'BA-CalderonMorales/terminal-jarvis'; const CHUNK_LOG_INTERVAL = 250000; // Log progress every 250KB function log(msg) { console.log(`[terminal-jarvis] ${msg}`); } function warn(msg) { console.warn(`[terminal-jarvis] Warning: ${msg}`); } function error(msg) { console.error(`[terminal-jarvis] Error: ${msg}`); } /** * Detect platform and return download information */ function getPlatformInfo() { const platform = os.platform(); const arch = os.arch(); // Map to GitHub release file names if (platform === 'darwin' && (arch === 'x64' || arch === 'arm64')) { return { name: 'macOS', file: 'terminal-jarvis-mac.tar.gz', isWindows: false }; } if (platform === 'linux' && (arch === 'x64' || arch === 'arm64')) { return { name: 'Linux', file: 'terminal-jarvis-linux.tar.gz', isWindows: false }; } if (platform === 'win32' && (arch === 'x64' || arch === 'arm64')) { return { name: 'Windows', file: 'terminal-jarvis-windows.tar.gz', isWindows: true }; } return null; } /** * Construct GitHub release asset URL */ function getAssetUrl(version, fileName) { const tag = `v${version}`; return `https://github.com/${GITHUB_REPO}/releases/download/${tag}/${fileName}`; } /** * Download file with retry logic, timeout, and progress display * Uses streaming to reduce memory usage and show progress */ async function download(url, dest, retries = DOWNLOAD_RETRIES) { await fs.promises.mkdir(path.dirname(dest), { recursive: true }); for (let attempt = 1; attempt <= retries; attempt++) { try { log(`Download attempt ${attempt}/${retries}...`); await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error(`Download timeout after ${DOWNLOAD_TIMEOUT / 1000}s`)); }, DOWNLOAD_TIMEOUT); const makeRequest = (requestUrl) => { https.get(requestUrl, (res) => { // Handle redirects (GitHub uses them for releases) if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { clearTimeout(timeout); return makeRequest(res.headers.location); } if (res.statusCode !== 200) { clearTimeout(timeout); return reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`)); } const totalSize = parseInt(res.headers['content-length'], 10) || 0; let downloadedSize = 0; let lastLoggedSize = 0; const fileStream = createWriteStream(dest); res.on('data', (chunk) => { downloadedSize += chunk.length; // Show progress every CHUNK_LOG_INTERVAL bytes if (totalSize > 0 && downloadedSize - lastLoggedSize >= CHUNK_LOG_INTERVAL) { const percent = Math.round((downloadedSize / totalSize) * 100); const sizeMB = (downloadedSize / 1024 / 1024).toFixed(1); process.stdout.write(`\r[terminal-jarvis] Downloading... ${sizeMB}MB (${percent}%)`); lastLoggedSize = downloadedSize; } }); res.pipe(fileStream); fileStream.on('finish', () => { clearTimeout(timeout); if (totalSize > 0) { process.stdout.write('\n'); // Newline after progress } fileStream.close(() => resolve(dest)); }); fileStream.on('error', (err) => { clearTimeout(timeout); reject(err); }); }).on('error', (err) => { clearTimeout(timeout); reject(err); }); }; makeRequest(url); }); // Success return dest; } catch (err) { if (attempt === retries) { throw new Error(`Failed after ${retries} attempts: ${err.message}`); } warn(`Attempt ${attempt} failed: ${err.message}. Retrying...`); // Wait before retry (exponential backoff, max 5 seconds) await new Promise(resolve => setTimeout(resolve, Math.min(1000 * attempt, 5000))); } } } /** * Extract tar.gz archive */ async function extractTarGz(archivePath, extractDir) { await fs.promises.mkdir(extractDir, { recursive: true }); const res = spawnSync('tar', ['-xzf', archivePath, '-C', extractDir], { stdio: 'inherit' }); if (res.status !== 0) { throw new Error('Failed to extract archive with tar'); } } /** * Verify prerequisites */ function checkPrerequisites() { const missing = []; const tarCheck = spawnSync('tar', ['--version'], { stdio: 'ignore' }); if (tarCheck.status !== 0) { missing.push('tar'); } if (missing.length > 0) { warn(`Missing required system tool(s): ${missing.join(', ')}`); const platform = os.platform(); if (missing.includes('tar')) { if (platform === 'linux') { warn('Install tar using your package manager:'); console.log(' Debian/Ubuntu: sudo apt-get update && sudo apt-get install -y tar'); console.log(' Fedora/RHEL: sudo dnf install -y tar'); console.log(' Arch Linux: sudo pacman -S tar'); } else if (platform === 'darwin') { warn('tar should be available on macOS by default.'); console.log(' Install via: xcode-select --install'); } else if (platform === 'win32') { warn('tar should be available on Windows 10+ by default.'); } } return false; } return true; } /** * Main installation workflow - optimized for speed */ (async () => { const startTime = Date.now(); try { // Detect platform const platformInfo = getPlatformInfo(); if (!platformInfo) { warn(`Unsupported platform: ${os.platform()}/${os.arch()}`); warn('Skipping binary download. You can still install via cargo:'); console.log(' cargo install terminal-jarvis'); process.exit(0); } log(`Detected ${platformInfo.name} (${os.arch()})`); // Check prerequisites if (!checkPrerequisites()) { warn('Skipping installation due to missing prerequisites.'); warn('You can retry by running: node node_modules/terminal-jarvis/scripts/postinstall.js'); return; } // Setup paths const version = pkg.version; const assetFile = platformInfo.file; const url = getAssetUrl(version, assetFile); const pkgRoot = path.join(__dirname, '..'); const binDir = path.join(pkgRoot, 'bin'); const downloadDir = path.join(pkgRoot, 'downloads'); const archivePath = path.join(downloadDir, assetFile); // Download binary with progress log(`Downloading v${version}...`); const downloadStart = Date.now(); await download(url, archivePath); const downloadTime = ((Date.now() - downloadStart) / 1000).toFixed(1); log(`[SUCCESS] Download complete (${downloadTime}s)`); // Extract archive log('Extracting...'); const extractStart = Date.now(); await extractTarGz(archivePath, downloadDir); const extractTime = ((Date.now() - extractStart) / 1000).toFixed(1); log(`[SUCCESS] Extraction complete (${extractTime}s)`); // Move binary to bin directory const extractedBin = path.join(downloadDir, 'terminal-jarvis'); const binaryDest = path.join(binDir, platformInfo.isWindows ? 'terminal-jarvis.exe' : 'terminal-jarvis-bin'); await fs.promises.mkdir(binDir, { recursive: true }); await fs.promises.copyFile(extractedBin, binaryDest); if (!platformInfo.isWindows) { await fs.promises.chmod(binaryDest, 0o755); } // Note: The launcher script (bin/terminal-jarvis) is committed to the repository // We only download and install the binary here const launcherPath = path.join(binDir, 'terminal-jarvis'); // Cleanup downloads (async, non-blocking) fs.promises.rm(downloadDir, { recursive: true, force: true }).catch(() => { }); // Verify installation const verifyPath = platformInfo.isWindows ? binaryDest : launcherPath; const verifyRes = spawnSync(verifyPath, ['--version'], { stdio: 'pipe', encoding: 'utf8' }); const totalTime = ((Date.now() - startTime) / 1000).toFixed(1); if (verifyRes.error) { warn(`Verification warning: ${verifyRes.error.message}`); } else if (verifyRes.status === 0) { log(`[SUCCESS] Terminal Jarvis v${version} installed in ${totalTime}s`); log(''); log('Ready! Run: npx terminal-jarvis'); } else { log(`[SUCCESS] Binary installed in ${totalTime}s`); log('Run: npx terminal-jarvis'); } } catch (err) { error(`Installation failed: ${err.message}`); warn('Fallback options:'); console.log(' 1. Install via cargo: cargo install terminal-jarvis'); console.log(' 2. Install via Homebrew: brew install ba-calderonmorales/terminal-jarvis/terminal-jarvis'); console.log(' 3. Download manually from: https://github.com/BA-CalderonMorales/terminal-jarvis/releases'); // Don't fail npm install completely process.exit(0); } })();