|
|
|
|
@@ -143,33 +143,55 @@ class PackageManager {
|
|
|
|
|
const parsed = this.parsePackageSpec(packageSpec);
|
|
|
|
|
const name = parsed.name;
|
|
|
|
|
let version = parsed.version;
|
|
|
|
|
console.log(chalk.blue(`Installing ${name}@${version || 'latest'}`));
|
|
|
|
|
const source = parsed.source;
|
|
|
|
|
|
|
|
|
|
console.log(chalk.blue(`Installing ${name}@${version || 'latest'} from ${source}`));
|
|
|
|
|
|
|
|
|
|
// Check cache first
|
|
|
|
|
// Check cache first (only for registry packages)
|
|
|
|
|
let packageData;
|
|
|
|
|
const isFromCache = await this.cache.has(name, version);
|
|
|
|
|
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 {
|
|
|
|
|
// Resolve version first
|
|
|
|
|
const resolvedVersion = await this.registry.resolveVersion(name, version || 'latest');
|
|
|
|
|
// Handle different sources
|
|
|
|
|
let downloadResult;
|
|
|
|
|
|
|
|
|
|
// Security check
|
|
|
|
|
if (options.secure !== false) {
|
|
|
|
|
// Simple security check - skip for now
|
|
|
|
|
switch (source) {
|
|
|
|
|
case 'git': {
|
|
|
|
|
downloadResult = await this.downloadFromGit(parsed);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 'file': {
|
|
|
|
|
downloadResult = await this.installFromFile(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
|
|
|
|
|
const downloadResult = await this.registry.download({ name, version: resolvedVersion });
|
|
|
|
|
// 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
|
|
|
|
|
await this.cache.store(name, resolvedVersion, packageData);
|
|
|
|
|
|
|
|
|
|
// Update the version to the resolved one
|
|
|
|
|
version = resolvedVersion;
|
|
|
|
|
// 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
|
|
|
|
|
@@ -226,11 +248,14 @@ class PackageManager {
|
|
|
|
|
source: isFromCache ? 'cache' : 'registry'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(chalk.green(`✓ Installed ${name}@${version || 'latest'}`));
|
|
|
|
|
console.log(chalk.green(`✓ Installed ${name}@${version || 'latest'} from ${source || (isFromCache ? 'cache' : 'registry')}`));
|
|
|
|
|
|
|
|
|
|
// 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 });
|
|
|
|
|
@@ -633,10 +658,83 @@ class PackageManager {
|
|
|
|
|
|
|
|
|
|
// 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 standard npm packages
|
|
|
|
|
const match = spec.match(/^(@?[^@]+)(?:@(.+))?$/);
|
|
|
|
|
return {
|
|
|
|
|
name: match[1],
|
|
|
|
|
version: match[2] || 'latest'
|
|
|
|
|
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@');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parseFileSpec(fileSpec) {
|
|
|
|
|
const cleanPath = fileSpec.replace('file:', '');
|
|
|
|
|
const name = path.basename(cleanPath);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
name: name,
|
|
|
|
|
version: 'file',
|
|
|
|
|
source: 'file',
|
|
|
|
|
path: cleanPath
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -793,6 +891,207 @@ class PackageManager {
|
|
|
|
|
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 tarballPath = path.join(os.tmpdir(), `${gitSpec.name}-${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 tarballPath = path.join(os.tmpdir(), `${fileSpec.name}-${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 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 || {};
|
|
|
|
|
|
|
|
|
|
// Run postinstall script if it exists
|
|
|
|
|
if (scripts.postinstall) {
|
|
|
|
|
console.log(chalk.blue(`Running postinstall script for ${packageName}...`));
|
|
|
|
|
|
|
|
|
|
const { spawn } = require('child_process');
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve, _reject) => {
|
|
|
|
|
const npmScript = spawn('npm', ['run', 'postinstall'], {
|
|
|
|
|
cwd: packageDir,
|
|
|
|
|
stdio: 'inherit',
|
|
|
|
|
shell: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
npmScript.on('close', (code) => {
|
|
|
|
|
if (code === 0) {
|
|
|
|
|
console.log(chalk.green(`✓ Postinstall script completed for ${packageName}`));
|
|
|
|
|
resolve();
|
|
|
|
|
} else {
|
|
|
|
|
console.warn(chalk.yellow(`⚠ Postinstall script failed for ${packageName} (code: ${code})`));
|
|
|
|
|
resolve(); // Don't fail the installation if postinstall fails
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
npmScript.on('error', (error) => {
|
|
|
|
|
console.warn(chalk.yellow(`⚠ Could not run postinstall script for ${packageName}: ${error.message}`));
|
|
|
|
|
resolve(); // Don't fail the installation if postinstall fails
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run install script if it exists (legacy)
|
|
|
|
|
if (scripts.install) {
|
|
|
|
|
console.log(chalk.blue(`Running install script for ${packageName}...`));
|
|
|
|
|
|
|
|
|
|
const { spawn } = require('child_process');
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve, _reject) => {
|
|
|
|
|
const npmScript = spawn('npm', ['run', 'install'], {
|
|
|
|
|
cwd: packageDir,
|
|
|
|
|
stdio: 'inherit',
|
|
|
|
|
shell: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
npmScript.on('close', (code) => {
|
|
|
|
|
if (code === 0) {
|
|
|
|
|
console.log(chalk.green(`✓ Install script completed for ${packageName}`));
|
|
|
|
|
resolve();
|
|
|
|
|
} else {
|
|
|
|
|
console.warn(chalk.yellow(`⚠ Install script failed for ${packageName} (code: ${code})`));
|
|
|
|
|
resolve(); // Don't fail the installation if install script fails
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
npmScript.on('error', (error) => {
|
|
|
|
|
console.warn(chalk.yellow(`⚠ Could not run install script for ${packageName}: ${error.message}`));
|
|
|
|
|
resolve(); // Don't fail the installation if install script fails
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn(chalk.yellow(`⚠ Error running post-install scripts for ${packageName}: ${error.message}`));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = PackageManager;
|
|
|
|
|
|