UNPKG

cvm-cli

Version:

A unified CLI tool for managing PHP, Node.js, and Python versions with virtual environment and dependency management support.

246 lines (201 loc) 6.68 kB
const fs = require('fs-extra'); const path = require('path'); const os = require('os'); const { spawn } = require('child_process'); const fetch = require('node-fetch'); const tar = require('tar'); const chalk = require('chalk'); const ProgressBar = require('progress'); const CVMUtils = require('./utils'); class BaseVersionManager { constructor(language) { this.language = language; this.languageDir = CVMUtils.getLanguageDir(language); } async downloadFile(url, destination, onProgress) { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to download: ${response.statusText}`); } const totalSize = parseInt(response.headers.get('content-length'), 10); let downloadedSize = 0; const progressBar = new ProgressBar( ` Downloading [:bar] :percent :etas`, { complete: '=', incomplete: ' ', width: 40, total: totalSize } ); await fs.ensureDir(path.dirname(destination)); const fileStream = fs.createWriteStream(destination); return new Promise((resolve, reject) => { response.body.on('data', (chunk) => { downloadedSize += chunk.length; progressBar.tick(chunk.length); if (onProgress) { onProgress(downloadedSize, totalSize); } }); response.body.pipe(fileStream); fileStream.on('finish', () => { console.log(); // New line after progress bar resolve(); }); fileStream.on('error', reject); response.body.on('error', reject); }); } async extractTarGz(archivePath, destination) { console.log(chalk.yellow(' Extracting archive...')); await fs.ensureDir(destination); return tar.extract({ file: archivePath, cwd: destination, strip: 1 // Remove the top-level directory from the archive }); } async extractTarXz(archivePath, destination) { console.log(chalk.yellow(' Extracting archive...')); await fs.ensureDir(destination); return new Promise((resolve, reject) => { const child = spawn('tar', ['xf', archivePath, '-C', destination, '--strip-components=1'], { stdio: 'inherit' }); child.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Extraction failed with code ${code}`)); } }); child.on('error', reject); }); } async executeCommand(command, options = {}) { return new Promise((resolve, reject) => { const child = spawn(command, [], { shell: true, stdio: options.silent ? 'pipe' : 'inherit', cwd: options.cwd || process.cwd(), env: { ...process.env, ...options.env } }); let stdout = ''; let stderr = ''; if (options.silent && child.stdout) { child.stdout.on('data', (data) => { stdout += data.toString(); }); } if (options.silent && child.stderr) { child.stderr.on('data', (data) => { stderr += data.toString(); }); } child.on('close', (code) => { if (code === 0) { resolve({ stdout, stderr, code }); } else { reject(new Error(`Command failed with exit code ${code}: ${stderr}`)); } }); child.on('error', reject); }); } getPlatform() { const platform = os.platform(); switch (platform) { case 'darwin': return 'darwin'; case 'win32': return 'win32'; case 'linux': return 'linux'; default: return platform; } } getArch() { const arch = os.arch(); switch (arch) { case 'x64': return 'x64'; case 'arm64': return 'arm64'; case 'ia32': return 'x86'; default: return arch; } } async isVersionInstalled(version) { const versionDir = path.join(this.languageDir, version); return await fs.pathExists(versionDir); } async getInstalledVersions() { if (!await fs.pathExists(this.languageDir)) { return []; } const items = await fs.readdir(this.languageDir); const versions = []; for (const item of items) { const itemPath = path.join(this.languageDir, item); const stat = await fs.stat(itemPath); if (stat.isDirectory()) { versions.push(item); } } return versions.sort(); } async getCurrentVersion() { const config = await CVMUtils.loadConfig(); return config.currentVersions[this.language]; } async setCurrentVersion(version) { const config = await CVMUtils.loadConfig(); config.currentVersions[this.language] = version; await CVMUtils.saveConfig(config); } async addToPath(version) { // This is a simplified implementation // In a real implementation, you'd need to modify shell profiles const versionDir = path.join(this.languageDir, version); const binDir = this.getBinDir(versionDir); if (await fs.pathExists(binDir)) { console.log(chalk.green(`✓ ${this.language} ${version} is now active`)); console.log(chalk.yellow(` Binary path: ${binDir}`)); console.log(chalk.cyan(` Add to your PATH: export PATH="${binDir}:$PATH"`)); } } getBinDir(versionDir) { // Override in subclasses return path.join(versionDir, 'bin'); } // Abstract methods to be implemented by subclasses async getAvailableVersions() { throw new Error('getAvailableVersions must be implemented by subclass'); } async install(version) { throw new Error('install must be implemented by subclass'); } async uninstall(version) { const versionDir = path.join(this.languageDir, version); if (!await fs.pathExists(versionDir)) { console.log(chalk.yellow(`${this.language} ${version} is not installed`)); return false; } console.log(chalk.blue(`Uninstalling ${this.language} ${version}...`)); await fs.remove(versionDir); // If this was the current version, unset it const currentVersion = await this.getCurrentVersion(); if (currentVersion === version) { await this.setCurrentVersion(null); } console.log(chalk.green(`✓ ${this.language} ${version} uninstalled successfully`)); return true; } async use(version) { if (!await this.isVersionInstalled(version)) { console.log(chalk.red(`${this.language} ${version} is not installed`)); console.log(chalk.yellow(`Install it with: cvm install ${this.language} ${version}`)); return false; } await this.setCurrentVersion(version); await this.addToPath(version); return true; } } module.exports = BaseVersionManager;