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
JavaScript
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;