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