UNPKG

alepm

Version:

Advanced and secure Node.js package manager with binary storage, intelligent caching, and comprehensive security features

1,537 lines (1,248 loc) 52.6 kB
const path = require('path'); const fs = require('fs-extra'); const chalk = require('chalk'); const semver = require('semver'); const ora = require('ora'); const inquirer = require('inquirer'); const CacheManager = require('../cache/cache-manager'); const SecurityManager = require('../security/security-manager'); const BinaryStorage = require('../storage/binary-storage'); const LockManager = require('./lock-manager'); const Registry = require('./registry'); const DependencyResolver = require('./dependency-resolver'); const ConfigManager = require('../utils/config-manager'); const Logger = require('../utils/logger'); class PackageManager { constructor(options = {}) { this.cache = new CacheManager(); this.security = new SecurityManager(); this.storage = new BinaryStorage(); this.lock = new LockManager(); this.registry = new Registry(); this.resolver = new DependencyResolver(); this.config = new ConfigManager(); this.logger = new Logger(); // Configure resolver dependencies this.resolver.setRegistry(this.registry); this.resolver.setLockManager(this.lock); this.projectRoot = options.projectRoot || this.findProjectRoot(); this.globalRoot = path.join(require('os').homedir(), '.alepm'); this.initialized = false; } async initialize() { if (this.initialized) return; await this.config.init(); await this.cache.init(); await this.storage.init(); this.initialized = true; } findProjectRoot() { let current = process.cwd(); while (current !== path.dirname(current)) { if (fs.existsSync(path.join(current, 'package.json'))) { return current; } current = path.dirname(current); } return process.cwd(); } async ensureInitialized() { // Check if package.json already exists const packageJsonPath = path.join(this.projectRoot, 'package.json'); const packageJsonExists = fs.existsSync(packageJsonPath); // If package.json exists, we don't need to initialize if (packageJsonExists) { // Just ensure the .alepm directory exists for our internal use const alePmDir = path.join(this.projectRoot, '.alepm'); if (!fs.existsSync(alePmDir)) { await fs.ensureDir(alePmDir); } this.initialized = true; return; } // If no package.json and not initialized, run full init if (!this.initialized) { await this.initProject(); } } async install(packages = [], options = {}) { // Ensure initialization await this.ensureInitialized(); try { // If no packages specified, install from package.json if (packages.length === 0) { const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (!fs.existsSync(packageJsonPath)) { throw new Error('No package.json found and no packages specified to install'); } return await this.installFromPackageJson(options); } // Install specified packages return await this.installPackages(packages, options); } catch (error) { console.error(chalk.red(`Installation failed: ${error.message}`)); throw error; } } async installFromPackageJson(options = {}) { const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (!fs.existsSync(packageJsonPath)) { throw new Error('package.json not found'); } const packageJson = await fs.readJson(packageJsonPath); const dependencies = { ...packageJson.dependencies, ...(options.includeDev || options.saveDev ? packageJson.devDependencies : {}) }; if (Object.keys(dependencies).length === 0) { console.log(chalk.yellow('No dependencies found in package.json')); return; } const packages = Object.entries(dependencies).map(([name, version]) => `${name}@${version}`); // Call the main installation logic with special flag for package.json installs return await this.installPackages(packages, { ...options, fromPackageJsonMain: true }); } async installPackages(packages, options = {}) { if (!packages || packages.length === 0) { return; } // Start timer for installation (for main installations only) const isMainInstall = !options.fromPackageJson && !options._depth; const isPackageJsonMain = options.fromPackageJsonMain; const isRecursiveInstall = options._depth > 0; const shouldShowSummary = (isMainInstall || isPackageJsonMain) && !isRecursiveInstall; const startTime = shouldShowSummary ? Date.now() : null; console.log(chalk.blue(`Installing ${packages.length} package(s)...`)); const results = []; for (const packageSpec of packages) { try { const parsed = this.parsePackageSpec(packageSpec); const name = parsed.name; let version = parsed.version; const source = parsed.source; console.log(chalk.blue(`Installing ${name}@${version || 'latest'} from ${source}`)); // Check cache first (only for registry packages) let packageData; const isFromCache = source === 'registry' && await this.cache.has(name, version); if (isFromCache) { console.log(chalk.green(`Using cached version of ${name}`)); packageData = await this.cache.get(name, version); } else { // Handle different sources let downloadResult; switch (source) { case 'git': { downloadResult = await this.downloadFromGit(parsed); break; } case 'file': { downloadResult = await this.installFromFile(parsed); break; } case 'workspace': { downloadResult = await this.installFromWorkspace(parsed); break; } case 'registry': default: { // Resolve version first const resolvedVersion = await this.registry.resolveVersion(name, version || 'latest'); // Security check if (options.secure !== false) { // Simple security check - skip for now } // Download and store downloadResult = await this.registry.download({ name, version: resolvedVersion }); // Update the version to the resolved one version = resolvedVersion; break; } } packageData = downloadResult.data; // Store in cache (only for registry packages) if (source === 'registry') { await this.cache.store(name, version, packageData); } } // Install the package to node_modules regardless of cache status const targetDir = options.global ? path.join(this.globalRoot, 'node_modules', name) : path.join(this.projectRoot, 'node_modules', name); await fs.ensureDir(path.dirname(targetDir)); // Extract package data to target directory if (Buffer.isBuffer(packageData)) { // Extract tarball using tar const tar = require('tar'); const os = require('os'); // Ensure target directory exists await fs.ensureDir(targetDir); try { // Write buffer to temporary file and extract from there // Sanitize package name for file system const sanitizedName = name.replace(/[@/]/g, '-'); const tempFile = path.join(os.tmpdir(), `${sanitizedName}-${Date.now()}.tgz`); await fs.writeFile(tempFile, packageData); // Extract the tarball directly to the target directory await tar.extract({ file: tempFile, cwd: targetDir, strip: 1 // Remove the 'package' directory level from tarball }); // Clean up temp file await fs.remove(tempFile); } catch (extractError) { console.warn(chalk.yellow(`Failed to extract tarball: ${extractError.message}`)); // Fallback - create basic package structure await fs.writeFile(path.join(targetDir, 'package.json'), JSON.stringify({ name, version: version || 'latest' }, null, 2)); } } else { // Fallback - create basic package structure await fs.ensureDir(targetDir); await fs.writeFile(path.join(targetDir, 'package.json'), JSON.stringify({ name, version: version || 'latest' }, null, 2)); } results.push({ name, version: version || 'latest', source: isFromCache ? 'cache' : 'registry' }); console.log(chalk.green(`✓ Installed ${name}@${version || 'latest'} from ${source || (isFromCache ? 'cache' : 'registry')}`)); // Create binary links for locally installed packages if (!options.global) { await this.createBinaryLinksForPackage(name, targetDir); } // Install dependencies recursively (with depth control) await this.installDependenciesRecursively(name, targetDir, options); // Run postinstall scripts await this.runPostInstallScripts(name, targetDir); } catch (error) { console.error(chalk.red(`✗ Failed to install ${packageSpec}: ${error.message}`)); results.push({ packageSpec, error: error.message }); } } // Update package.json if not global and not from package.json if (!options.global && !options.fromPackageJson) { await this.updatePackageJsonDependencies(results.filter(r => !r.error), options); } // Update lock file await this.lock.update(results.filter(r => !r.error)); // Show installation summary for main installations (including package.json main installs) if (shouldShowSummary) { const successfulInstalls = results.filter(r => !r.error); const failedInstalls = results.filter(r => r.error); // Calculate elapsed time const endTime = Date.now(); const elapsedTime = startTime ? endTime - startTime : 0; const elapsedSeconds = (elapsedTime / 1000).toFixed(2); console.log(''); console.log(chalk.green('📦 Installation Summary:')); console.log(''); if (successfulInstalls.length > 0) { if (successfulInstalls.length === 1 && !isPackageJsonMain) { const result = successfulInstalls[0]; const sourceLabel = result.source === 'cache' ? '(cached)' : `(${result.source})`; console.log(chalk.green(`✓ Successfully installed ${result.name}@${result.version} ${chalk.gray(sourceLabel)}`)); } else { console.log(chalk.green(`✓ Successfully installed ${successfulInstalls.length} package(s):`)); successfulInstalls.forEach(result => { const sourceLabel = result.source === 'cache' ? '(cached)' : `(${result.source})`; console.log(chalk.green(` • ${result.name}@${result.version} ${chalk.gray(sourceLabel)}`)); }); } } if (failedInstalls.length > 0) { console.log(''); console.log(chalk.red(`✗ Failed to install ${failedInstalls.length} package(s):`)); failedInstalls.forEach(result => { console.log(chalk.red(` • ${result.packageSpec}: ${result.error}`)); }); } console.log(''); console.log(chalk.gray(`⏱️ Total time: ${elapsedSeconds}s`)); console.log(''); } console.log(chalk.green(`Installation completed. ${results.filter(r => !r.error).length} packages installed.`)); return results; } async installDependenciesRecursively(packageName, packageDir, options = {}) { try { // Read the package.json of the installed package const packageJsonPath = path.join(packageDir, 'package.json'); if (!await fs.pathExists(packageJsonPath)) { // No package.json found, skip dependency installation return; } const packageJson = await fs.readJson(packageJsonPath); const dependencies = { ...packageJson.dependencies, ...(options.includeDev ? packageJson.devDependencies : {}) }; const optionalDependencies = packageJson.optionalDependencies || {}; if (!dependencies || Object.keys(dependencies).length === 0) { // If no regular dependencies, check if there are optional dependencies to install if (Object.keys(optionalDependencies).length === 0) { return; } } // Initialize installed packages tracking if not exists if (!options._installedPackages) { options._installedPackages = new Set(); } // Limit recursion depth to prevent infinite loops const currentDepth = options._depth || 0; if (currentDepth > 8) { // Increased from 5 to 8 for better dependency coverage // Silent skip for deep dependencies to avoid log noise return; } console.log(chalk.blue(`Installing dependencies for ${packageName}...`)); // Install dependencies recursively by calling installPackages const depOptions = { ...options, fromPackageJson: true, // Prevent updating package.json _depth: currentDepth + 1, // Increment depth _installedPackages: options._installedPackages // Pass along installed packages set }; // Filter out already installed packages to avoid duplicates const dependenciesToInstall = Object.entries(dependencies).filter(([name, version]) => { const packageKey = `${name}@${version}`; if (options._installedPackages.has(packageKey)) { return false; // Skip already installed package } // Check if package already exists in node_modules const targetDir = options.global ? path.join(this.globalRoot, 'node_modules', name) : path.join(this.projectRoot, 'node_modules', name); return !fs.existsSync(targetDir); }); if (dependenciesToInstall.length === 0) { // No regular dependencies to install, but continue to check optional dependencies } else { // Prepare dependency specs for installation const dependencySpecs = dependenciesToInstall.map(([name, version]) => { // Mark as installed to prevent duplicates options._installedPackages.add(`${name}@${version}`); // Handle various version formats if (version.startsWith('^') || version.startsWith('~') || version.startsWith('>=') || version.startsWith('<=')) { return `${name}@${version}`; } else if (version === '*' || version === 'latest') { return `${name}@latest`; } else if (semver.validRange(version)) { return `${name}@${version}`; } else { // For non-semver versions (git urls, file paths, etc.), use as-is return `${name}@${version}`; } }); // Install dependencies await this.installPackages(dependencySpecs, depOptions); } // Install optional dependencies (ignore failures) if (Object.keys(optionalDependencies).length > 0) { const optionalDependenciesToInstall = Object.entries(optionalDependencies).filter(([name, version]) => { const packageKey = `${name}@${version}`; if (options._installedPackages && options._installedPackages.has(packageKey)) { return false; // Skip already installed package } // Check if package already exists in node_modules const targetDir = options.global ? path.join(this.globalRoot, 'node_modules', name) : path.join(this.projectRoot, 'node_modules', name); const exists = fs.existsSync(targetDir); return !exists; }); if (optionalDependenciesToInstall.length > 0) { const optionalSpecs = optionalDependenciesToInstall.map(([name, version]) => { // Mark as installed to prevent duplicates if (options._installedPackages) { options._installedPackages.add(`${name}@${version}`); } // Handle various version formats if (version.startsWith('^') || version.startsWith('~') || version.startsWith('>=') || version.startsWith('<=')) { return `${name}@${version}`; } else if (version === '*' || version === 'latest') { return `${name}@latest`; } else if (semver.validRange(version)) { return `${name}@${version}`; } else { return `${name}@${version}`; } }); // Install optional dependencies (ignore failures) try { await this.installPackages(optionalSpecs, depOptions); } catch (error) { console.warn(chalk.yellow(`Some optional dependencies for ${packageName} could not be installed (this is usually safe to ignore)`)); } } } } catch (error) { console.warn(chalk.yellow(`Failed to install dependencies for ${packageName}: ${error.message}`)); } } async installPackage(pkg, options = {}) { const targetDir = options.global ? path.join(this.globalRoot, 'node_modules', pkg.name) : path.join(this.projectRoot, 'node_modules', pkg.name); // Get package data from cache const packageData = await this.cache.get(pkg.name, pkg.version); // Extract to target directory using binary storage await this.storage.extract(packageData, targetDir); // Create binary links if global if (options.global && pkg.bin) { await this.createGlobalBinLinks(pkg); } // Create local binary links if not global if (!options.global && pkg.bin) { await this.createLocalBinLinks(pkg); } this.logger.info(`Installed ${pkg.name}@${pkg.version}`); } async uninstall(packages, options = {}) { await this.ensureInitialized(); const spinner = ora('Uninstalling packages...').start(); try { for (const packageName of packages) { const targetDir = options.global ? path.join(this.globalRoot, 'node_modules', packageName) : path.join(this.projectRoot, 'node_modules', packageName); if (fs.existsSync(targetDir)) { await fs.remove(targetDir); // Remove from package.json if (!options.global) { await this.removeFromPackageJson(packageName); // Remove local bin links await this.removeLocalBinLinks(packageName); } // Remove global bin links if (options.global) { await this.removeGlobalBinLinks(packageName); } } } // Update lock file await this.lock.remove(packages, options); spinner.stop(); console.log(chalk.green(`✓ Uninstalled ${packages.length} packages`)); } catch (error) { spinner.stop(); throw error; } } async update(packages = [], options = {}) { await this.ensureInitialized(); const spinner = ora('Checking for updates...').start(); try { const installedPackages = await this.getInstalledPackages(options); const toUpdate = packages.length === 0 ? installedPackages : packages; const updates = []; for (const packageName of toUpdate) { const current = installedPackages.find(p => p.name === packageName); if (current) { const latest = await this.registry.getLatestVersion(packageName); if (semver.gt(latest, current.version)) { updates.push({ name: packageName, from: current.version, to: latest }); } } } if (updates.length === 0) { spinner.stop(); console.log(chalk.green('All packages are up to date')); return; } spinner.stop(); const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: `Update ${updates.length} packages?`, default: true }]); if (confirm) { const updateSpecs = updates.map(u => `${u.name}@${u.to}`); await this.install(updateSpecs, { ...options, update: true }); } } catch (error) { spinner.stop(); throw error; } } async list(options = {}) { const installedPackages = await this.getInstalledPackages(options); if (installedPackages.length === 0) { console.log(chalk.yellow('No packages installed')); return; } const tree = this.buildDependencyTree(installedPackages, parseInt(options.depth)); this.printDependencyTree(tree); } async search(query, options = {}) { await this.ensureInitialized(); const spinner = ora(`Searching for "${query}"...`).start(); try { const results = await this.registry.search(query, { limit: parseInt(options.limit) }); spinner.stop(); if (results.length === 0) { console.log(chalk.yellow('No packages found')); return; } console.log(chalk.bold(`\nFound ${results.length} packages:\n`)); for (const pkg of results) { console.log(chalk.cyan(pkg.name) + chalk.gray(` v${pkg.version}`)); console.log(chalk.gray(` ${pkg.description}`)); console.log(chalk.gray(` ${pkg.keywords?.join(', ') || ''}\n`)); } } catch (error) { spinner.stop(); throw error; } } async info(packageName) { await this.ensureInitialized(); const spinner = ora(`Getting info for ${packageName}...`).start(); try { const info = await this.registry.getPackageInfo(packageName); spinner.stop(); console.log(chalk.bold.cyan(`\n${info.name}@${info.version}\n`)); console.log(chalk.gray(info.description)); console.log(chalk.gray(`Homepage: ${info.homepage}`)); console.log(chalk.gray(`License: ${info.license}`)); console.log(chalk.gray(`Dependencies: ${Object.keys(info.dependencies || {}).length}`)); console.log(chalk.gray(`Last modified: ${new Date(info.time.modified).toLocaleDateString()}`)); } catch (error) { spinner.stop(); throw error; } } async cleanCache() { const spinner = ora('Cleaning cache...').start(); try { const cleaned = await this.cache.clean(); spinner.stop(); console.log(chalk.green(`✓ Cleaned cache (freed ${this.formatBytes(cleaned)} of space)`)); } catch (error) { spinner.stop(); throw error; } } async verifyCache() { const spinner = ora('Verifying cache integrity...').start(); try { const result = await this.cache.verify(); spinner.stop(); if (result.corrupted.length === 0) { console.log(chalk.green('✓ Cache integrity verified')); } else { console.log(chalk.yellow(`⚠ Found ${result.corrupted.length} corrupted entries`)); console.log(chalk.gray('Run "alepm cache clean" to fix')); } } catch (error) { spinner.stop(); throw error; } } async audit(options = {}) { const spinner = ora('Auditing packages for vulnerabilities...').start(); try { const installedPackages = await this.getInstalledPackages(); const vulnerabilities = await this.security.audit(installedPackages); spinner.stop(); if (vulnerabilities.length === 0) { console.log(chalk.green('✓ No vulnerabilities found')); return; } console.log(chalk.red(`⚠ Found ${vulnerabilities.length} vulnerabilities:`)); for (const vuln of vulnerabilities) { console.log(chalk.red(` ${vuln.severity.toUpperCase()}: ${vuln.title}`)); console.log(chalk.gray(` Package: ${vuln.module_name}@${vuln.version}`)); console.log(chalk.gray(` Path: ${vuln.path.join(' > ')}`)); } if (options.fix) { await this.fixVulnerabilities(vulnerabilities); } } catch (error) { spinner.stop(); throw error; } } async runScript(scriptName, options = {}) { await this.ensureInitialized(); const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (!await fs.pathExists(packageJsonPath)) { throw new Error('No package.json found in current directory'); } const packageJson = await fs.readJson(packageJsonPath); if (!packageJson.scripts) { if (options.ifPresent) { console.log(chalk.yellow('No scripts section found in package.json')); return; } throw new Error('No scripts section found in package.json'); } const script = packageJson.scripts[scriptName]; if (!script) { if (options.ifPresent) { console.log(chalk.yellow(`Script "${scriptName}" not found`)); return; } // Show available scripts const availableScripts = Object.keys(packageJson.scripts); console.log(chalk.red(`Script "${scriptName}" not found.`)); if (availableScripts.length > 0) { console.log(chalk.blue('Available scripts:')); availableScripts.forEach(name => { console.log(chalk.gray(` ${name}: ${packageJson.scripts[name]}`)); }); } throw new Error(`Script "${scriptName}" not found`); } console.log(chalk.blue(`Running script: ${scriptName}`)); console.log(chalk.gray(`> ${script}`)); console.log(''); const { spawn } = require('child_process'); return new Promise((resolve, reject) => { // Use cross-platform approach const childProcess = spawn(script, [], { cwd: this.projectRoot, stdio: options.silent ? 'pipe' : 'inherit', shell: true, env: { ...process.env, // Add node_modules/.bin to PATH PATH: `${path.join(this.projectRoot, 'node_modules', '.bin')}${path.delimiter}${process.env.PATH}` } }); // Initialize output variables for silent mode let stdout = ''; let stderr = ''; // Handle silent mode if (options.silent) { if (childProcess.stdout) { childProcess.stdout.on('data', (data) => { stdout += data.toString(); }); } if (childProcess.stderr) { childProcess.stderr.on('data', (data) => { stderr += data.toString(); }); } } // Handle process completion let completed = false; const handleCompletion = (code, signal) => { if (completed) return; completed = true; if (options.silent) { if (stdout && stdout.trim()) { console.log(stdout.trim()); } if (stderr && stderr.trim()) { console.error(stderr.trim()); } } if (code === 0) { if (!options.silent) { console.log(chalk.green(`✓ Script "${scriptName}" completed successfully`)); } resolve(); } else { const errorMsg = signal ? `Script "${scriptName}" was terminated by signal ${signal}` : `Script "${scriptName}" exited with code ${code}`; reject(new Error(errorMsg)); } }; childProcess.on('close', handleCompletion); childProcess.on('exit', handleCompletion); childProcess.on('error', (error) => { if (completed) return; completed = true; reject(new Error(`Failed to run script "${scriptName}": ${error.message}`)); }); // Handle process termination signals const cleanup = () => { if (!completed && !childProcess.killed) { childProcess.kill('SIGTERM'); setTimeout(() => { if (!childProcess.killed) { childProcess.kill('SIGKILL'); } }, 5000); } }; process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); }); } async verifyLock() { const spinner = ora('Verifying lock file...').start(); try { const isValid = await this.lock.verify(); spinner.stop(); if (isValid) { console.log(chalk.green('✓ Lock file is valid')); } else { console.log(chalk.red('✗ Lock file is invalid or outdated')); } } catch (error) { spinner.stop(); throw error; } } async setConfig(key, value) { await this.config.set(key, value); console.log(chalk.green(`✓ Set ${key} = ${value}`)); } async getConfig(key) { const value = await this.config.get(key); console.log(`${key} = ${value || 'undefined'}`); } async initProject(options = {}) { if (!options.yes) { const answers = await inquirer.prompt([ { name: 'name', message: 'Package name:', default: path.basename(this.projectRoot) }, { name: 'version', message: 'Version:', default: '1.0.0' }, { name: 'description', message: 'Description:' }, { name: 'entry', message: 'Entry point:', default: 'index.js' }, { name: 'author', message: 'Author:' }, { name: 'license', message: 'License:', default: 'MIT' } ]); const packageJson = { name: answers.name, version: answers.version, description: answers.description, main: answers.entry, scripts: { test: 'echo "Error: no test specified" && exit 1' }, author: answers.author, license: answers.license }; await fs.writeJson(path.join(this.projectRoot, 'package.json'), packageJson, { spaces: 2 }); } // Initialize lock file await this.lock.init(); console.log(chalk.green('✓ Initialized project')); } // Helper methods parsePackageSpec(spec) { // Handle git repositories if (this.isGitUrl(spec)) { return this.parseGitSpec(spec); } // Handle file paths if (spec.startsWith('file:') || spec.startsWith('./') || spec.startsWith('../') || spec.startsWith('/')) { return this.parseFileSpec(spec); } // Handle workspace dependencies if (this.isWorkspaceSpec(spec)) { return this.parseWorkspaceSpec(spec); } // Handle standard npm packages // Improved regex to handle scoped packages like @scope/package@version const match = spec.match(/^(@[^/]+\/[^@]+|[^@]+)(?:@(.+))?$/); if (!match) { throw new Error(`Invalid package specification: ${spec}`); } return { name: match[1], version: match[2] || 'latest', source: 'registry' }; } isGitUrl(spec) { return spec.includes('github.com') || spec.includes('gitlab.com') || spec.includes('bitbucket.org') || spec.startsWith('git+') || spec.startsWith('git://') || spec.endsWith('.git') || spec.includes('git@'); } isWorkspaceSpec(spec) { // Check if it's a workspace dependency const match = spec.match(/^(@?[^@]+)@(.+)$/); if (match) { const version = match[2]; return version.startsWith('workspace:'); } return false; } parseGitSpec(gitSpec) { let url = gitSpec; let ref = 'main'; // Changed from 'master' to 'main' (modern default) let name = null; // Extract reference (branch/tag/commit) if (url.includes('#')) { const parts = url.split('#'); url = parts[0]; ref = parts[1]; } // Extract package name from URL if (url.includes('github.com/')) { const match = url.match(/github\.com\/([^/]+)\/([^/]+?)(\.git)?$/); if (match) { name = match[2].replace('.git', ''); } } else if (url.includes('gitlab.com/')) { const match = url.match(/gitlab\.com\/([^/]+)\/([^/]+?)(\.git)?$/); if (match) { name = match[2].replace('.git', ''); } } else if (url.includes('bitbucket.org/')) { const match = url.match(/bitbucket\.org\/([^/]+)\/([^/]+?)(\.git)?$/); if (match) { name = match[2].replace('.git', ''); } } return { name: name || 'git-package', version: ref, source: 'git', url: url, ref: ref }; } parseWorkspaceSpec(spec) { const match = spec.match(/^(@?[^@]+)@workspace:(.*)$/); if (!match) { throw new Error(`Invalid workspace spec: ${spec}`); } const name = match[1]; const workspaceVersion = match[2]; // Could be *, ^1.0.0, ~1.0.0, etc. return { name: name, version: workspaceVersion, source: 'workspace', originalSpec: spec }; } parseFileSpec(fileSpec) { const cleanPath = fileSpec.replace('file:', ''); const name = path.basename(cleanPath); return { name: name, version: 'file', source: 'file', path: cleanPath }; } async getInstalledPackages(options = {}) { const nodeModulesPath = options.global ? path.join(this.globalRoot, 'node_modules') : path.join(this.projectRoot, 'node_modules'); if (!fs.existsSync(nodeModulesPath)) { return []; } const packages = []; const dirs = await fs.readdir(nodeModulesPath); for (const dir of dirs) { if (dir.startsWith('.')) continue; const packageJsonPath = path.join(nodeModulesPath, dir, 'package.json'); if (fs.existsSync(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); packages.push({ name: packageJson.name, version: packageJson.version }); } } return packages; } formatBytes(bytes) { const sizes = ['B', 'KB', 'MB', 'GB']; if (bytes === 0) return '0 B'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; } buildDependencyTree(packages, depth) { // Simplified tree building return packages.map(pkg => ({ name: pkg.name, version: pkg.version, children: depth > 0 ? [] : null })); } printDependencyTree(tree) { for (const pkg of tree) { console.log(`${pkg.name}@${pkg.version}`); } } async updatePackageJson(packageSpecs, options) { const packageJsonPath = path.join(this.projectRoot, 'package.json'); const packageJson = await fs.readJson(packageJsonPath); const targetField = options.saveDev ? 'devDependencies' : 'dependencies'; if (!packageJson[targetField]) { packageJson[targetField] = {}; } for (const spec of packageSpecs) { const version = options.saveExact ? spec.version : `^${spec.version}`; packageJson[targetField][spec.name] = version; } await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } async updatePackageJsonDependencies(installedPackages, options) { const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (!fs.existsSync(packageJsonPath)) { return; } const packageJson = await fs.readJson(packageJsonPath); const targetField = options.saveDev ? 'devDependencies' : 'dependencies'; if (!packageJson[targetField]) { packageJson[targetField] = {}; } for (const pkg of installedPackages) { if (pkg.name && pkg.version) { const version = options.saveExact ? pkg.version : `^${pkg.version}`; packageJson[targetField][pkg.name] = version; } } await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } async removeFromPackageJson(packageName) { const packageJsonPath = path.join(this.projectRoot, 'package.json'); const packageJson = await fs.readJson(packageJsonPath); delete packageJson.dependencies?.[packageName]; delete packageJson.devDependencies?.[packageName]; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } async createGlobalBinLinks(pkg) { // Implementation for creating global binary links const binDir = path.join(this.globalRoot, 'bin'); await fs.ensureDir(binDir); if (pkg.bin) { for (const [binName, binPath] of Object.entries(pkg.bin)) { const linkPath = path.join(binDir, binName); const targetPath = path.join(this.globalRoot, 'node_modules', pkg.name, binPath); await fs.ensureSymlink(targetPath, linkPath); } } } async createLocalBinLinks(pkg) { // Implementation for creating local binary links in node_modules/.bin const binDir = path.join(this.projectRoot, 'node_modules', '.bin'); await fs.ensureDir(binDir); if (pkg.bin) { // Handle both string and object formats for bin field const binEntries = typeof pkg.bin === 'string' ? [[pkg.name, pkg.bin]] : Object.entries(pkg.bin); for (const [binName, binPath] of binEntries) { const linkPath = path.join(binDir, binName); const targetPath = path.join(this.projectRoot, 'node_modules', pkg.name, binPath); try { // Remove existing link if it exists if (await fs.pathExists(linkPath)) { await fs.remove(linkPath); } // Create the symlink await fs.ensureSymlink(targetPath, linkPath); // Make the target executable (Unix/Linux only) if (process.platform !== 'win32') { await fs.chmod(targetPath, '755'); } console.log(chalk.gray(` Created bin link: ${binName} -> ${binPath}`)); } catch (error) { console.warn(chalk.yellow(`Warning: Failed to create bin link for ${binName}: ${error.message}`)); } } } } async createBinaryLinksForPackage(packageName, packageDir) { try { const packageJsonPath = path.join(packageDir, 'package.json'); if (!await fs.pathExists(packageJsonPath)) { return; } const packageJson = await fs.readJson(packageJsonPath); if (packageJson.bin) { console.log(chalk.blue(`Creating local bin links for ${packageName}`)); await this.createLocalBinLinks(packageJson); } } catch (error) { console.warn(chalk.yellow(`Warning: Failed to create binary links for ${packageName}: ${error.message}`)); } } async removeGlobalBinLinks(packageName) { // Implementation for removing global binary links const binDir = path.join(this.globalRoot, 'bin'); const packageDir = path.join(this.globalRoot, 'node_modules', packageName); if (fs.existsSync(packageDir)) { const packageJsonPath = path.join(packageDir, 'package.json'); if (fs.existsSync(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); if (packageJson.bin) { for (const binName of Object.keys(packageJson.bin)) { const linkPath = path.join(binDir, binName); if (fs.existsSync(linkPath)) { await fs.remove(linkPath); } } } } } } async removeLocalBinLinks(packageName) { // Implementation for removing local binary links from node_modules/.bin const binDir = path.join(this.projectRoot, 'node_modules', '.bin'); const packageDir = path.join(this.projectRoot, 'node_modules', packageName); if (await fs.pathExists(packageDir)) { const packageJsonPath = path.join(packageDir, 'package.json'); if (await fs.pathExists(packageJsonPath)) { try { const packageJson = await fs.readJson(packageJsonPath); if (packageJson.bin) { // Handle both string and object formats for bin field const binEntries = typeof packageJson.bin === 'string' ? [[packageJson.name, packageJson.bin]] : Object.entries(packageJson.bin); for (const [binName] of binEntries) { const linkPath = path.join(binDir, binName); if (await fs.pathExists(linkPath)) { await fs.remove(linkPath); console.log(chalk.gray(` Removed bin link: ${binName}`)); } } } } catch (error) { console.warn(chalk.yellow(`Warning: Failed to read package.json for ${packageName}: ${error.message}`)); } } } } async fixVulnerabilities(vulnerabilities) { const spinner = ora('Fixing vulnerabilities...').start(); try { for (const vuln of vulnerabilities) { if (vuln.fixAvailable) { await this.install([`${vuln.module_name}@${vuln.fixVersion}`], { update: true }); } } spinner.stop(); console.log(chalk.green('✓ Fixed available vulnerabilities')); } catch (error) { spinner.stop(); throw error; } } async downloadFromGit(gitSpec) { const { spawn } = require('child_process'); const os = require('os'); console.log(chalk.blue(`Cloning from Git: ${gitSpec.url}#${gitSpec.ref}`)); // Create temporary directory for cloning const tempDir = path.join(os.tmpdir(), `alepm-git-${Date.now()}`); try { // Try to clone with specified branch first let cloneSuccessful = false; try { await new Promise((resolve, reject) => { const git = spawn('git', ['clone', '--depth', '1', '--branch', gitSpec.ref, gitSpec.url, tempDir], { stdio: 'pipe' }); git.on('close', (code) => { if (code === 0) { cloneSuccessful = true; resolve(); } else { reject(new Error(`Git clone failed with code ${code}`)); } }); git.on('error', reject); }); } catch (error) { // If clone with branch fails, try without specific branch (will use default) console.log(chalk.yellow(`Branch ${gitSpec.ref} not found, trying default branch...`)); await new Promise((resolve, reject) => { const git = spawn('git', ['clone', '--depth', '1', gitSpec.url, tempDir], { stdio: 'pipe' }); git.on('close', (code) => { if (code === 0) { cloneSuccessful = true; resolve(); } else { reject(new Error(`Git clone failed with code ${code}`)); } }); git.on('error', reject); }); } if (!cloneSuccessful) { throw new Error('Failed to clone repository'); } // Read package.json to get package name const packageJsonPath = path.join(tempDir, 'package.json'); if (await fs.pathExists(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); gitSpec.name = packageJson.name || gitSpec.name; } // Create tarball from cloned directory const tar = require('tar'); const sanitizedName = gitSpec.name.replace(/[@/]/g, '-'); const tarballPath = path.join(os.tmpdir(), `${sanitizedName}-${Date.now()}.tgz`); await tar.create({ gzip: true, file: tarballPath, cwd: tempDir }, ['.']); // Read tarball as buffer const tarballData = await fs.readFile(tarballPath); // Cleanup await fs.remove(tempDir); await fs.remove(tarballPath); return { data: tarballData }; } catch (error) { // Cleanup on error if (await fs.pathExists(tempDir)) { await fs.remove(tempDir); } throw error; } } async installFromFile(fileSpec) { console.log(chalk.blue(`Installing from file: ${fileSpec.path}`)); const absolutePath = path.resolve(fileSpec.path); if (!await fs.pathExists(absolutePath)) { throw new Error(`File not found: ${absolutePath}`); } const stat = await fs.stat(absolutePath); if (stat.isDirectory()) { // Handle directory - create tarball const tar = require('tar'); const os = require('os'); const sanitizedName = fileSpec.name.replace(/[@/]/g, '-'); const tarballPath = path.join(os.tmpdir(), `${sanitizedName}-${Date.now()}.tgz`); await tar.create({ gzip: true, file: tarballPath, cwd: absolutePath }, ['.']); const tarballData = await fs.readFile(tarballPath); await fs.remove(tarballPath); return { data: tarballData }; } else { // Handle file - assume it's a tarball const tarballData = await fs.readFile(absolutePath); return { data: tarballData }; } } async installFromWorkspace(workspaceSpec) { try { const packageName = workspaceSpec.name; const workspaceVersion = workspaceSpec.version; // Find the workspace package const workspacePackage = await this.findWorkspacePackage(packageName, workspaceVersion); if (!workspacePackage) { throw new Error(`Workspace package ${packageName} not found`); } // Install from the workspace directory const fileSpec = { name: packageName, path: workspacePackage.path, source: 'file' }; return await this.installFromFile(fileSpec); } catch (error) { throw new Error(`Failed to install workspace package ${workspaceSpec.name}: ${error.message}`); } } async findWorkspacePackage(packageName, workspaceVersion) { try { // Read the workspace root package.json to find workspace configuration const rootPackageJsonPath = path.join(this.projectRoot, 'package.json'); if (!await fs.pathExists(rootPackageJsonPath)) { throw new Error('No package.json found in project root'); } const rootPackageJson = await fs.readJson(rootPackageJsonPath); // Check for workspace configuration let workspacePatterns = []; // Handle different workspace configuration formats if (rootPackageJson.workspaces) { if (Array.isArray(rootPackageJson.workspaces)) { workspacePatterns = rootPackageJson.workspaces; } else if (rootPackageJson.workspaces.packages) { workspacePatterns = rootPackageJson.workspaces.packages; } } if (workspacePatterns.length === 0) { throw new Error('No workspace configuration found'); } // Search for the package in workspace directories for (const pattern of workspacePatterns) { const workspaceDirs = await this.expandWorkspacePattern(pattern); for (const workspaceDir of workspaceDirs) { const packageJsonPath = path.join(this.projectRoot, workspaceDir, 'package.json'); if (await fs.pathExists(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); if (packageJson.name === packageName) { // Check if version matches workspace requirement if (this.matchesWorkspaceVersion(packageJson.version, workspaceVersion)) { return { name: packageName, version: packageJson.version, path: path.join(this.projectRoot, workspaceDir), packageJson: packageJson }; } } } } } return null; } catch (error) { throw new Error(`Failed to find workspace package ${packageName}: ${error.message}`); } } async expandWorkspacePattern(pattern) { const glob = require('glob'); try { // Use glob to expand workspace patterns const matches = await new Promise((resolve, reject) => { glob(pattern, { cwd: this.projectRoot }, (err, files) => { if (err) reject(err); else resolve(files); }); }); // Filter to only directories that contain package.json const workspaceDirs = []; for (const match of matches) { const packageJsonPath = path.join(this.projectRoot, match, 'package.json'); if (await fs.pathExists(packageJsonPath)) { workspaceDirs.push(match); } } return workspaceDirs; } catch (error) { console.warn(chalk.yellow(`Warning: Failed to expand workspace pattern ${pattern}: ${error.message}`)); return []; } } matchesWorkspaceVersion(packageVersion, workspaceVersion) { // Handle different workspace version formats if (workspaceVersion === '*') { return true; // Any version matches } if (workspaceVersion.startsWith('^') || workspaceVersion.startsWith('~')) { // Use semver to check if version satisfies range return semver.satisfies(packageVersion, workspaceVersion); } if (workspaceVersion === packageVersion) { return true; // Exact match } // Default to true for other cases (workspace should generally match) return true; } async runPostInstallScripts(packageName, packageDir) { try { const packageJsonPath = path.join(packageDir, 'package.json'); if (!await fs.pathExists(packageJsonPath)) { return; } const packageJson = await fs.readJson(packageJsonPath); const scripts = packageJson.scripts || {};