gitarsenal-cli
Version:
CLI tool for creating Modal sandboxes with GitHub repositories
421 lines (355 loc) • 15 kB
JavaScript
#!/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();