UNPKG

gitarsenal-cli

Version:

CLI tool for creating Modal sandboxes with GitHub repositories

421 lines (355 loc) 15 kB
#!/usr/bin/env node const fs = require('fs-extra'); const path = require('path'); const { promisify } = require('util'); const { exec } = require('child_process'); const chalk = require('chalk'); const execAsync = promisify(exec); // Path to the Python script in the package const pythonScriptDir = path.join(__dirname, '..', 'python'); const pythonScriptPath = path.join(pythonScriptDir, 'test_modalSandboxScript.py'); // Path to the original Python script const originalScriptPath = path.join(__dirname, '..', '..', '..', 'mcp-server', 'src', 'utils', 'test_modalSandboxScript.py'); // Function to check and install uv async function checkAndInstallUv() { try { // Check if uv is already installed const { stdout } = await execAsync('uv --version'); console.log(chalk.green(`✅ uv is already installed: ${stdout.trim()}`)); return true; } catch (error) { console.log(chalk.yellow('⚠️ uv not found. Attempting to install...')); // Detect platform for appropriate installation methods const platform = process.platform; let installMethods = []; if (platform === 'darwin') { // macOS - prioritize Homebrew installMethods = [ 'curl -LsSf https://astral.sh/uv/install.sh | sh', 'pip install uv', 'pip3 install uv', 'brew install uv', 'cargo install uv' ]; } else if (platform === 'win32') { // Windows - use PowerShell script and pip installMethods = [ 'powershell -c "irm https://astral.sh/uv/install.ps1 | iex"', 'pip install uv', 'pip3 install uv', 'cargo install uv' ]; } else { // Linux and others installMethods = [ 'curl -LsSf https://astral.sh/uv/install.sh | sh', 'pip3 install uv', 'pip install uv', 'cargo install uv' ]; } for (const method of installMethods) { try { console.log(chalk.gray(`🔄 Trying to install uv with: ${method}`)); if (method.includes('curl')) { // For curl installation, we need to handle the shell script await execAsync(method, { env: { ...process.env, SHELL: '/bin/bash' }, stdio: 'inherit' }); } else if (method.includes('powershell')) { // For Windows PowerShell installation await execAsync(method, { shell: 'powershell.exe', stdio: 'inherit' }); } else { await execAsync(method, { stdio: 'inherit' }); } // Verify installation const { stdout } = await execAsync('uv --version'); console.log(chalk.green(`✅ uv installed successfully: ${stdout.trim()}`)); return true; } catch (installError) { console.log(chalk.gray(`⚠️ ${method} failed: ${installError.message}`)); } } return false; } } // Function to create and activate virtual environment using uv async function createVirtualEnvironment() { const packages = ['modal', 'gitingest', 'requests', 'anthropic']; const packageDir = path.join(__dirname, '..'); console.log(chalk.yellow(`📦 Creating virtual environment with uv and installing packages: ${packages.join(', ')}`)); console.log(chalk.gray(`📁 Working directory: ${packageDir}`)); try { // First, ensure uv is available let uvAvailable = false; try { const { stdout } = await execAsync('uv --version'); console.log(chalk.green(`✅ uv found: ${stdout.trim()}`)); uvAvailable = true; } catch (error) { console.log(chalk.yellow('⚠️ uv not found, attempting to install...')); uvAvailable = await checkAndInstallUv(); } if (!uvAvailable) { console.log(chalk.red('❌ uv is required but not available')); return false; } // Check if virtual environment already exists const venvPath = path.join(packageDir, '.venv'); if (await fs.pathExists(venvPath)) { console.log(chalk.yellow('⚠️ Virtual environment already exists, removing it...')); await fs.remove(venvPath); } console.log(chalk.gray(`🔄 Creating virtual environment with uv...`)); // Create virtual environment using uv await execAsync('uv venv', { cwd: packageDir, env: { ...process.env, PYTHONIOENCODING: 'utf-8' }, stdio: 'inherit' }); // Verify virtual environment was created if (!(await fs.pathExists(venvPath))) { throw new Error('Virtual environment was not created'); } console.log(chalk.green('✅ Virtual environment created successfully with uv!')); console.log(chalk.gray(`🔄 Installing packages in virtual environment with uv...`)); // Install packages using uv pip from requirements.txt const requirementsPath = path.join(packageDir, 'python', 'requirements.txt'); if (await fs.pathExists(requirementsPath)) { console.log(chalk.gray(`📦 Installing packages from requirements.txt...`)); await execAsync(`uv pip install -r ${requirementsPath}`, { cwd: packageDir, env: { ...process.env, PYTHONIOENCODING: 'utf-8' }, stdio: 'inherit' }); } else { console.log(chalk.gray(`📦 Installing packages: ${packages.join(', ')}`)); await execAsync(`uv pip install ${packages.join(' ')}`, { cwd: packageDir, env: { ...process.env, PYTHONIOENCODING: 'utf-8' }, stdio: 'inherit' }); } console.log(chalk.green('✅ Python packages installed successfully in virtual environment!')); // Verify packages are installed const pythonPath = path.join(venvPath, 'bin', 'python'); const packagesToVerify = [...packages, 'anthropic']; // Ensure anthropic is checked for (const pkg of packagesToVerify) { try { await execAsync(`${pythonPath} -c "import ${pkg}; print('${pkg} imported successfully')"`); console.log(chalk.green(`✅ ${pkg} verified`)); } catch (error) { console.log(chalk.yellow(`⚠️ ${pkg} verification failed: ${error.message}`)); } } // Create a script to activate the virtual environment const isWindows = process.platform === 'win32'; const activateScript = isWindows ? `@echo off cd /d "%~dp0" call ".venv\\Scripts\\activate.bat" %*` : `#!/bin/bash cd "$(dirname "$0")" source ".venv/bin/activate" exec "$@"`; const activateScriptPath = path.join(packageDir, 'activate_venv' + (isWindows ? '.bat' : '.sh')); await fs.writeFile(activateScriptPath, activateScript); if (!isWindows) { await fs.chmod(activateScriptPath, 0o755); } console.log(chalk.green(`✅ Virtual environment activation script created: ${activateScriptPath}`)); // Create a status file to indicate successful installation const statusFile = path.join(packageDir, '.venv_status.json'); await fs.writeJson(statusFile, { created: new Date().toISOString(), packages: packages, uv_version: (await execAsync('uv --version')).stdout.trim() }); console.log(chalk.green('✅ Virtual environment setup completed successfully!')); return true; } catch (error) { console.log(chalk.red(`❌ Error creating virtual environment with uv: ${error.message}`)); console.log(chalk.yellow('💡 Please run manually:')); console.log(chalk.yellow(' cd /root/.nvm/versions/node/v22.18.0/lib/node_modules/gitarsenal-cli')); console.log(chalk.yellow(' uv venv')); console.log(chalk.yellow(' uv pip install -r python/requirements.txt')); return false; } } // Function to check Python async function checkPython() { const pythonCommands = ['python3', 'python', 'py']; for (const cmd of pythonCommands) { try { const { stdout } = await execAsync(`${cmd} --version`); console.log(chalk.green(`✅ Python found: ${stdout.trim()}`)); return true; } catch (error) { // Continue to next command } } console.log(chalk.red('❌ Python not found. Please install Python 3.7+')); console.log(chalk.yellow('💡 Download from: https://www.python.org/downloads/')); return false; } // Function to check Git async function checkGit() { try { const { stdout } = await execAsync('git --version'); console.log(chalk.green(`✅ Git found: ${stdout.trim()}`)); return true; } catch (error) { console.log(chalk.yellow('⚠️ Git not found. Please install Git:')); console.log(chalk.yellow(' macOS: brew install git')); console.log(chalk.yellow(' Ubuntu: sudo apt-get install git')); console.log(chalk.yellow(' Windows: https://git-scm.com/download/win')); return false; } } async function postinstall() { try { console.log(chalk.blue('📦 Running GitArsenal CLI postinstall script...')); console.log(chalk.gray(`📁 Package directory: ${path.join(__dirname, '..')}`)); // Check Python first console.log(chalk.blue('🔍 Checking Python installation...')); const pythonOk = await checkPython(); if (!pythonOk) { console.log(chalk.red('❌ Python is required for GitArsenal CLI')); process.exit(1); } // Check Git console.log(chalk.blue('🔍 Checking Git installation...')); await checkGit(); // Check and install uv if needed console.log(chalk.blue('🔍 Checking for uv package manager...')); await checkAndInstallUv(); // Install Python packages in virtual environment console.log(chalk.blue('🔍 Installing Python dependencies in virtual environment...')); const venvCreated = await createVirtualEnvironment(); if (!venvCreated) { console.log(chalk.red('❌ Failed to create virtual environment')); process.exit(1); } // Create the Python directory if it doesn't exist await fs.ensureDir(pythonScriptDir); // Check if the Python script already exists and has content let scriptFound = false; if (await fs.pathExists(pythonScriptPath)) { const stats = await fs.stat(pythonScriptPath); // If the file is larger than 5KB, assume it's the full script if (stats.size > 5000) { console.log(chalk.green('✅ Found existing full Python script. Keeping it.')); scriptFound = true; } else { console.log(chalk.yellow('⚠️ Existing Python script appears to be minimal. Looking for full version...')); } } // Only try to copy if the script doesn't exist or is minimal if (!scriptFound) { // Check if the original script exists in a different location if (await fs.pathExists(originalScriptPath)) { console.log(chalk.green('✅ Found original Python script in mcp-server')); await fs.copy(originalScriptPath, pythonScriptPath); scriptFound = true; } else { // Try to find the script in common locations console.log(chalk.yellow('⚠️ Original script not found in expected location. Searching for alternatives...')); const possibleLocations = [ path.join(process.cwd(), 'python', 'test_modalSandboxScript.py'), ]; for (const location of possibleLocations) { if (await fs.pathExists(location) && location !== pythonScriptPath) { console.log(chalk.green(`✅ Found Python script at ${location}`)); await fs.copy(location, pythonScriptPath); scriptFound = true; break; } } } } // If script not found, create it from embedded content if (!scriptFound) { console.log(chalk.yellow('⚠️ Python script not found. Creating from embedded content...')); // Create a minimal version of the script const minimalScript = `#!/usr/bin/env python3 # This is a minimal version of the Modal sandbox script # For full functionality, please ensure the original script is available import os import sys import argparse import subprocess def main(): parser = argparse.ArgumentParser(description='Create a Modal sandbox with GPU') parser.add_argument('--gpu', type=str, default='A10G', help='GPU type (default: A10G)') parser.add_argument('--repo-url', type=str, help='Repository URL to clone') parser.add_argument('--repo-name', type=str, help='Repository name override') parser.add_argument('--volume-name', type=str, help='Name of the Modal volume for persistent storage') parser.add_argument('--commands-file', type=str, help='Path to file containing setup commands (one per line)') args = parser.parse_args() # Check if modal is installed try: subprocess.run(['modal', '--version'], check=True, capture_output=True) except (subprocess.SubprocessError, FileNotFoundError): print("❌ Modal CLI not found. Please install it with: pip install modal") sys.exit(1) # Build the modal command cmd = [ 'modal', 'run', '--gpu', args.gpu.lower(), '--command', f'git clone {args.repo_url} && cd $(basename {args.repo_url} .git) && bash' ] # Execute the command print(f"🚀 Launching Modal sandbox with {args.gpu} GPU...") print(f"📥 Cloning repository: {args.repo_url}") try: subprocess.run(cmd) except Exception as e: print(f"❌ Error: {e}") sys.exit(1) if __name__ == "__main__": main() `; await fs.writeFile(pythonScriptPath, minimalScript); console.log(chalk.yellow('⚠️ Created minimal Python script. For full functionality, please install the original script.')); } // Make the script executable try { await fs.chmod(pythonScriptPath, 0o755); console.log(chalk.green('✅ Made Python script executable')); } catch (error) { console.log(chalk.yellow(`⚠️ Could not make script executable: ${error.message}`)); } // Final success message console.log(chalk.green(` ✅ GitArsenal CLI Installation Complete! ======================================== What was installed: • GitArsenal CLI (npm package) • Virtual environment with Python packages: - Modal - GitIngest - Requests - Anthropic (for Claude fallback) 💡 Next steps: • Run: gitarsenal --help • Run: gitarsenal setup • Visit: https://gitarsenal.dev 💡 To activate the virtual environment: • Unix/macOS: source .venv/bin/activate • Windows: .venv\\Scripts\\activate.bat • Or use: ./activate_venv.sh (Unix/macOS) or activate_venv.bat (Windows) 💡 Claude Fallback: • Set ANTHROPIC_API_KEY for Claude fallback functionality • Run: python test_claude_fallback.py to test Having issues? Run: gitarsenal --debug `)); } catch (error) { console.error(chalk.red(`❌ Error during postinstall: ${error.message}`)); process.exit(1); } } postinstall();