Comparar commits
14 Commits
44ca9ffc30
...
master
| Autor | SHA1 | Fecha | |
|---|---|---|---|
| 511b545ffb | |||
|
76e9c939e2
|
|||
| 6c575750ff | |||
|
9eb10395ad
|
|||
|
e46dd6a16a
|
|||
|
30d2b35bda
|
|||
| 2fccf3fd48 | |||
|
fdeb3b2a2c
|
|||
| a92b2496e1 | |||
|
05836dadd2
|
|||
| a6dc54c045 | |||
|
d2044f85c5
|
|||
| dfb39d8772 | |||
|
2796185f36
|
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "alepm",
|
"name": "alepm",
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"description": "Advanced and secure Node.js package manager with binary storage, intelligent caching, and comprehensive security features",
|
"description": "Advanced and secure Node.js package manager with binary storage, intelligent caching, and comprehensive security features",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -11,7 +11,10 @@
|
|||||||
"test": "jest",
|
"test": "jest",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"prepublishOnly": "npm test && npm run lint",
|
"prepublishOnly": "npm test && npm run lint",
|
||||||
"postinstall": "echo 'Thanks for installing alepm! Run: alepm --help to get started.'"
|
"postinstall": "echo 'Thanks for installing alepm! Run: alepm --help to get started.'",
|
||||||
|
"hello": "echo 'Hello from alepm!'",
|
||||||
|
"date": "date",
|
||||||
|
"version": "echo 'alepm version:' && node -e \"console.log(require('./package.json').version)\""
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"package-manager",
|
"package-manager",
|
||||||
@@ -48,21 +51,14 @@
|
|||||||
"IMPLEMENTATION.md"
|
"IMPLEMENTATION.md"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^2.2.0",
|
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"commander": "^11.1.0",
|
"commander": "^11.1.0",
|
||||||
"crypto": "^1.0.1",
|
|
||||||
"debug": "^4.4.1",
|
|
||||||
"express": "^5.1.0",
|
|
||||||
"fs-extra": "^11.3.1",
|
"fs-extra": "^11.3.1",
|
||||||
"inquirer": "^8.2.6",
|
"inquirer": "^8.2.6",
|
||||||
"listr2": "^6.6.1",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"node-fetch": "^2.6.12",
|
"node-fetch": "^2.6.12",
|
||||||
"ora": "^5.4.1",
|
"ora": "^5.4.1",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.2",
|
||||||
"tar": "^6.2.1",
|
"tar": "^6.2.1"
|
||||||
"lodash.debounce": "^4.0.8"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.45.0",
|
||||||
|
|||||||
27
src/cache/cache-manager.js
vendido
27
src/cache/cache-manager.js
vendido
@@ -11,13 +11,15 @@ class CacheManager {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.cacheDir = path.join(require('os').homedir(), '.alepm', 'cache');
|
this.cacheDir = path.join(require('os').homedir(), '.alepm', 'cache');
|
||||||
this.metadataFile = path.join(this.cacheDir, 'metadata.json');
|
this.metadataFile = path.join(this.cacheDir, 'metadata.json');
|
||||||
this.init();
|
this._initialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
if (this._initialized) return;
|
||||||
|
|
||||||
await fs.ensureDir(this.cacheDir);
|
await fs.ensureDir(this.cacheDir);
|
||||||
|
|
||||||
if (!fs.existsSync(this.metadataFile)) {
|
if (!await fs.pathExists(this.metadataFile)) {
|
||||||
await this.saveMetadata({
|
await this.saveMetadata({
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
entries: {},
|
entries: {},
|
||||||
@@ -25,9 +27,12 @@ class CacheManager {
|
|||||||
lastCleanup: Date.now()
|
lastCleanup: Date.now()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(packageName, version) {
|
async get(packageName, version) {
|
||||||
|
await this.init();
|
||||||
const key = this.generateKey(packageName, version);
|
const key = this.generateKey(packageName, version);
|
||||||
const metadata = await this.loadMetadata();
|
const metadata = await this.loadMetadata();
|
||||||
|
|
||||||
@@ -38,7 +43,7 @@ class CacheManager {
|
|||||||
const entry = metadata.entries[key];
|
const entry = metadata.entries[key];
|
||||||
const filePath = path.join(this.cacheDir, entry.file);
|
const filePath = path.join(this.cacheDir, entry.file);
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!await fs.pathExists(filePath)) {
|
||||||
// Remove stale entry
|
// Remove stale entry
|
||||||
delete metadata.entries[key];
|
delete metadata.entries[key];
|
||||||
await this.saveMetadata(metadata);
|
await this.saveMetadata(metadata);
|
||||||
@@ -67,6 +72,7 @@ class CacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async has(packageName, version) {
|
async has(packageName, version) {
|
||||||
|
await this.init();
|
||||||
const key = this.generateKey(packageName, version);
|
const key = this.generateKey(packageName, version);
|
||||||
const metadata = await this.loadMetadata();
|
const metadata = await this.loadMetadata();
|
||||||
|
|
||||||
@@ -78,7 +84,7 @@ class CacheManager {
|
|||||||
const filePath = path.join(this.cacheDir, entry.file);
|
const filePath = path.join(this.cacheDir, entry.file);
|
||||||
|
|
||||||
// Check if file exists
|
// Check if file exists
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!await fs.pathExists(filePath)) {
|
||||||
// Remove stale entry
|
// Remove stale entry
|
||||||
delete metadata.entries[key];
|
delete metadata.entries[key];
|
||||||
await this.saveMetadata(metadata);
|
await this.saveMetadata(metadata);
|
||||||
@@ -89,6 +95,7 @@ class CacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async store(packageName, version, data) {
|
async store(packageName, version, data) {
|
||||||
|
await this.init();
|
||||||
const key = this.generateKey(packageName, version);
|
const key = this.generateKey(packageName, version);
|
||||||
const metadata = await this.loadMetadata();
|
const metadata = await this.loadMetadata();
|
||||||
|
|
||||||
@@ -117,7 +124,7 @@ class CacheManager {
|
|||||||
if (metadata.entries[key]) {
|
if (metadata.entries[key]) {
|
||||||
const oldEntry = metadata.entries[key];
|
const oldEntry = metadata.entries[key];
|
||||||
const oldFilePath = path.join(this.cacheDir, oldEntry.file);
|
const oldFilePath = path.join(this.cacheDir, oldEntry.file);
|
||||||
if (fs.existsSync(oldFilePath)) {
|
if (await fs.pathExists(oldFilePath)) {
|
||||||
await fs.remove(oldFilePath);
|
await fs.remove(oldFilePath);
|
||||||
metadata.totalSize -= oldEntry.size;
|
metadata.totalSize -= oldEntry.size;
|
||||||
}
|
}
|
||||||
@@ -135,6 +142,7 @@ class CacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async remove(packageName, version) {
|
async remove(packageName, version) {
|
||||||
|
await this.init();
|
||||||
const key = this.generateKey(packageName, version);
|
const key = this.generateKey(packageName, version);
|
||||||
const metadata = await this.loadMetadata();
|
const metadata = await this.loadMetadata();
|
||||||
|
|
||||||
@@ -145,7 +153,7 @@ class CacheManager {
|
|||||||
const entry = metadata.entries[key];
|
const entry = metadata.entries[key];
|
||||||
const filePath = path.join(this.cacheDir, entry.file);
|
const filePath = path.join(this.cacheDir, entry.file);
|
||||||
|
|
||||||
if (fs.existsSync(filePath)) {
|
if (await fs.pathExists(filePath)) {
|
||||||
await fs.remove(filePath);
|
await fs.remove(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,13 +165,14 @@ class CacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async clean() {
|
async clean() {
|
||||||
|
await this.init();
|
||||||
const metadata = await this.loadMetadata();
|
const metadata = await this.loadMetadata();
|
||||||
let cleanedSize = 0;
|
let cleanedSize = 0;
|
||||||
|
|
||||||
for (const [, entry] of Object.entries(metadata.entries)) {
|
for (const [, entry] of Object.entries(metadata.entries)) {
|
||||||
const filePath = path.join(this.cacheDir, entry.file);
|
const filePath = path.join(this.cacheDir, entry.file);
|
||||||
|
|
||||||
if (fs.existsSync(filePath)) {
|
if (await fs.pathExists(filePath)) {
|
||||||
await fs.remove(filePath);
|
await fs.remove(filePath);
|
||||||
cleanedSize += entry.size;
|
cleanedSize += entry.size;
|
||||||
}
|
}
|
||||||
@@ -182,6 +191,7 @@ class CacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async verify() {
|
async verify() {
|
||||||
|
await this.init();
|
||||||
const metadata = await this.loadMetadata();
|
const metadata = await this.loadMetadata();
|
||||||
const corrupted = [];
|
const corrupted = [];
|
||||||
const missing = [];
|
const missing = [];
|
||||||
@@ -189,7 +199,7 @@ class CacheManager {
|
|||||||
for (const [key, entry] of Object.entries(metadata.entries)) {
|
for (const [key, entry] of Object.entries(metadata.entries)) {
|
||||||
const filePath = path.join(this.cacheDir, entry.file);
|
const filePath = path.join(this.cacheDir, entry.file);
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!await fs.pathExists(filePath)) {
|
||||||
missing.push(key);
|
missing.push(key);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -220,6 +230,7 @@ class CacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getStats() {
|
async getStats() {
|
||||||
|
await this.init();
|
||||||
const metadata = await this.loadMetadata();
|
const metadata = await this.loadMetadata();
|
||||||
const entries = Object.values(metadata.entries);
|
const entries = Object.values(metadata.entries);
|
||||||
|
|
||||||
|
|||||||
15
src/cli.js
15
src/cli.js
@@ -105,6 +105,21 @@ program
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Run command
|
||||||
|
program
|
||||||
|
.command('run <script>')
|
||||||
|
.description('Run a script from package.json')
|
||||||
|
.option('--silent', 'Suppress output from npm scripts')
|
||||||
|
.option('--if-present', 'Don\'t error if script is missing')
|
||||||
|
.action(async (script, options) => {
|
||||||
|
try {
|
||||||
|
await pm.runScript(script, options);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(chalk.red(`Error: ${error.message}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Cache commands
|
// Cache commands
|
||||||
program
|
program
|
||||||
.command('cache')
|
.command('cache')
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const ConfigManager = require('../utils/config-manager');
|
|||||||
const Logger = require('../utils/logger');
|
const Logger = require('../utils/logger');
|
||||||
|
|
||||||
class PackageManager {
|
class PackageManager {
|
||||||
constructor() {
|
constructor(options = {}) {
|
||||||
this.cache = new CacheManager();
|
this.cache = new CacheManager();
|
||||||
this.security = new SecurityManager();
|
this.security = new SecurityManager();
|
||||||
this.storage = new BinaryStorage();
|
this.storage = new BinaryStorage();
|
||||||
@@ -29,7 +29,7 @@ class PackageManager {
|
|||||||
this.resolver.setRegistry(this.registry);
|
this.resolver.setRegistry(this.registry);
|
||||||
this.resolver.setLockManager(this.lock);
|
this.resolver.setLockManager(this.lock);
|
||||||
|
|
||||||
this.projectRoot = this.findProjectRoot();
|
this.projectRoot = options.projectRoot || this.findProjectRoot();
|
||||||
this.globalRoot = path.join(require('os').homedir(), '.alepm');
|
this.globalRoot = path.join(require('os').homedir(), '.alepm');
|
||||||
|
|
||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
@@ -115,7 +115,7 @@ class PackageManager {
|
|||||||
const packageJson = await fs.readJson(packageJsonPath);
|
const packageJson = await fs.readJson(packageJsonPath);
|
||||||
const dependencies = {
|
const dependencies = {
|
||||||
...packageJson.dependencies,
|
...packageJson.dependencies,
|
||||||
...(options.includeDev ? packageJson.devDependencies : {})
|
...(options.includeDev || options.saveDev ? packageJson.devDependencies : {})
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Object.keys(dependencies).length === 0) {
|
if (Object.keys(dependencies).length === 0) {
|
||||||
@@ -125,8 +125,8 @@ class PackageManager {
|
|||||||
|
|
||||||
const packages = Object.entries(dependencies).map(([name, version]) => `${name}@${version}`);
|
const packages = Object.entries(dependencies).map(([name, version]) => `${name}@${version}`);
|
||||||
|
|
||||||
// Call the main installation logic directly, bypassing the package.json check
|
// Call the main installation logic with special flag for package.json installs
|
||||||
return await this.installPackages(packages, { ...options, fromPackageJson: true });
|
return await this.installPackages(packages, { ...options, fromPackageJsonMain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async installPackages(packages, options = {}) {
|
async installPackages(packages, options = {}) {
|
||||||
@@ -134,6 +134,13 @@ class PackageManager {
|
|||||||
return;
|
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)...`));
|
console.log(chalk.blue(`Installing ${packages.length} package(s)...`));
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
@@ -167,6 +174,10 @@ class PackageManager {
|
|||||||
downloadResult = await this.installFromFile(parsed);
|
downloadResult = await this.installFromFile(parsed);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'workspace': {
|
||||||
|
downloadResult = await this.installFromWorkspace(parsed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'registry':
|
case 'registry':
|
||||||
default: {
|
default: {
|
||||||
// Resolve version first
|
// Resolve version first
|
||||||
@@ -212,7 +223,9 @@ class PackageManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Write buffer to temporary file and extract from there
|
// Write buffer to temporary file and extract from there
|
||||||
const tempFile = path.join(os.tmpdir(), `${name}-${Date.now()}.tgz`);
|
// 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);
|
await fs.writeFile(tempFile, packageData);
|
||||||
|
|
||||||
// Extract the tarball directly to the target directory
|
// Extract the tarball directly to the target directory
|
||||||
@@ -250,6 +263,11 @@ class PackageManager {
|
|||||||
|
|
||||||
console.log(chalk.green(`✓ Installed ${name}@${version || 'latest'} from ${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)
|
// Install dependencies recursively (with depth control)
|
||||||
await this.installDependenciesRecursively(name, targetDir, options);
|
await this.installDependenciesRecursively(name, targetDir, options);
|
||||||
|
|
||||||
@@ -270,6 +288,47 @@ class PackageManager {
|
|||||||
// Update lock file
|
// Update lock file
|
||||||
await this.lock.update(results.filter(r => !r.error));
|
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.`));
|
console.log(chalk.green(`Installation completed. ${results.filter(r => !r.error).length} packages installed.`));
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
@@ -289,10 +348,14 @@ class PackageManager {
|
|||||||
...packageJson.dependencies,
|
...packageJson.dependencies,
|
||||||
...(options.includeDev ? packageJson.devDependencies : {})
|
...(options.includeDev ? packageJson.devDependencies : {})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const optionalDependencies = packageJson.optionalDependencies || {};
|
||||||
|
|
||||||
if (!dependencies || Object.keys(dependencies).length === 0) {
|
if (!dependencies || Object.keys(dependencies).length === 0) {
|
||||||
// No dependencies to install
|
// If no regular dependencies, check if there are optional dependencies to install
|
||||||
return;
|
if (Object.keys(optionalDependencies).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize installed packages tracking if not exists
|
// Initialize installed packages tracking if not exists
|
||||||
@@ -309,6 +372,14 @@ class PackageManager {
|
|||||||
|
|
||||||
console.log(chalk.blue(`Installing dependencies for ${packageName}...`));
|
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
|
// Filter out already installed packages to avoid duplicates
|
||||||
const dependenciesToInstall = Object.entries(dependencies).filter(([name, version]) => {
|
const dependenciesToInstall = Object.entries(dependencies).filter(([name, version]) => {
|
||||||
const packageKey = `${name}@${version}`;
|
const packageKey = `${name}@${version}`;
|
||||||
@@ -325,37 +396,74 @@ class PackageManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (dependenciesToInstall.length === 0) {
|
if (dependenciesToInstall.length === 0) {
|
||||||
return;
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare dependency specs for installation
|
// Install optional dependencies (ignore failures)
|
||||||
const dependencySpecs = dependenciesToInstall.map(([name, version]) => {
|
if (Object.keys(optionalDependencies).length > 0) {
|
||||||
// Mark as installed to prevent duplicates
|
const optionalDependenciesToInstall = Object.entries(optionalDependencies).filter(([name, version]) => {
|
||||||
options._installedPackages.add(`${name}@${version}`);
|
const packageKey = `${name}@${version}`;
|
||||||
|
if (options._installedPackages && options._installedPackages.has(packageKey)) {
|
||||||
// Handle various version formats
|
return false; // Skip already installed package
|
||||||
if (version.startsWith('^') || version.startsWith('~') || version.startsWith('>=') || version.startsWith('<=')) {
|
}
|
||||||
return `${name}@${version}`;
|
|
||||||
} else if (version === '*' || version === 'latest') {
|
// Check if package already exists in node_modules
|
||||||
return `${name}@latest`;
|
const targetDir = options.global
|
||||||
} else if (semver.validRange(version)) {
|
? path.join(this.globalRoot, 'node_modules', name)
|
||||||
return `${name}@${version}`;
|
: path.join(this.projectRoot, 'node_modules', name);
|
||||||
} else {
|
|
||||||
// For non-semver versions (git urls, file paths, etc.), use as-is
|
const exists = fs.existsSync(targetDir);
|
||||||
return `${name}@${version}`;
|
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)`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
};
|
|
||||||
|
|
||||||
// Install dependencies
|
|
||||||
await this.installPackages(dependencySpecs, depOptions);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(chalk.yellow(`Failed to install dependencies for ${packageName}: ${error.message}`));
|
console.warn(chalk.yellow(`Failed to install dependencies for ${packageName}: ${error.message}`));
|
||||||
@@ -378,6 +486,11 @@ class PackageManager {
|
|||||||
await this.createGlobalBinLinks(pkg);
|
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}`);
|
this.logger.info(`Installed ${pkg.name}@${pkg.version}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,6 +511,8 @@ class PackageManager {
|
|||||||
// Remove from package.json
|
// Remove from package.json
|
||||||
if (!options.global) {
|
if (!options.global) {
|
||||||
await this.removeFromPackageJson(packageName);
|
await this.removeFromPackageJson(packageName);
|
||||||
|
// Remove local bin links
|
||||||
|
await this.removeLocalBinLinks(packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove global bin links
|
// Remove global bin links
|
||||||
@@ -596,6 +711,138 @@ class PackageManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
async verifyLock() {
|
||||||
const spinner = ora('Verifying lock file...').start();
|
const spinner = ora('Verifying lock file...').start();
|
||||||
|
|
||||||
@@ -668,8 +915,18 @@ class PackageManager {
|
|||||||
return this.parseFileSpec(spec);
|
return this.parseFileSpec(spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle workspace dependencies
|
||||||
|
if (this.isWorkspaceSpec(spec)) {
|
||||||
|
return this.parseWorkspaceSpec(spec);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle standard npm packages
|
// Handle standard npm packages
|
||||||
const match = spec.match(/^(@?[^@]+)(?:@(.+))?$/);
|
// Improved regex to handle scoped packages like @scope/package@version
|
||||||
|
const match = spec.match(/^(@[^/]+\/[^@]+|[^@]+)(?:@(.+))?$/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Invalid package specification: ${spec}`);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: match[1],
|
name: match[1],
|
||||||
version: match[2] || 'latest',
|
version: match[2] || 'latest',
|
||||||
@@ -687,6 +944,16 @@ class PackageManager {
|
|||||||
spec.includes('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) {
|
parseGitSpec(gitSpec) {
|
||||||
let url = gitSpec;
|
let url = gitSpec;
|
||||||
let ref = 'main'; // Changed from 'master' to 'main' (modern default)
|
let ref = 'main'; // Changed from 'master' to 'main' (modern default)
|
||||||
@@ -726,6 +993,23 @@ class PackageManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
parseFileSpec(fileSpec) {
|
||||||
const cleanPath = fileSpec.replace('file:', '');
|
const cleanPath = fileSpec.replace('file:', '');
|
||||||
const name = path.basename(cleanPath);
|
const name = path.basename(cleanPath);
|
||||||
@@ -853,6 +1137,62 @@ class PackageManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
async removeGlobalBinLinks(packageName) {
|
||||||
// Implementation for removing global binary links
|
// Implementation for removing global binary links
|
||||||
const binDir = path.join(this.globalRoot, 'bin');
|
const binDir = path.join(this.globalRoot, 'bin');
|
||||||
@@ -874,6 +1214,37 @@ class PackageManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
async fixVulnerabilities(vulnerabilities) {
|
||||||
const spinner = ora('Fixing vulnerabilities...').start();
|
const spinner = ora('Fixing vulnerabilities...').start();
|
||||||
|
|
||||||
@@ -957,7 +1328,8 @@ class PackageManager {
|
|||||||
|
|
||||||
// Create tarball from cloned directory
|
// Create tarball from cloned directory
|
||||||
const tar = require('tar');
|
const tar = require('tar');
|
||||||
const tarballPath = path.join(os.tmpdir(), `${gitSpec.name}-${Date.now()}.tgz`);
|
const sanitizedName = gitSpec.name.replace(/[@/]/g, '-');
|
||||||
|
const tarballPath = path.join(os.tmpdir(), `${sanitizedName}-${Date.now()}.tgz`);
|
||||||
|
|
||||||
await tar.create({
|
await tar.create({
|
||||||
gzip: true,
|
gzip: true,
|
||||||
@@ -998,7 +1370,8 @@ class PackageManager {
|
|||||||
// Handle directory - create tarball
|
// Handle directory - create tarball
|
||||||
const tar = require('tar');
|
const tar = require('tar');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const tarballPath = path.join(os.tmpdir(), `${fileSpec.name}-${Date.now()}.tgz`);
|
const sanitizedName = fileSpec.name.replace(/[@/]/g, '-');
|
||||||
|
const tarballPath = path.join(os.tmpdir(), `${sanitizedName}-${Date.now()}.tgz`);
|
||||||
|
|
||||||
await tar.create({
|
await tar.create({
|
||||||
gzip: true,
|
gzip: true,
|
||||||
@@ -1017,6 +1390,139 @@ class PackageManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
async runPostInstallScripts(packageName, packageDir) {
|
||||||
try {
|
try {
|
||||||
const packageJsonPath = path.join(packageDir, 'package.json');
|
const packageJsonPath = path.join(packageDir, 'package.json');
|
||||||
|
|||||||
135
tests/run-command.test.js
Archivo normal
135
tests/run-command.test.js
Archivo normal
@@ -0,0 +1,135 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const PackageManager = require('../src/core/package-manager');
|
||||||
|
|
||||||
|
describe('Run Command', () => {
|
||||||
|
let tempDir;
|
||||||
|
let packageManager;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
tempDir = path.join(__dirname, 'temp-run-test');
|
||||||
|
await fs.ensureDir(tempDir);
|
||||||
|
packageManager = new PackageManager({ projectRoot: tempDir });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (await fs.pathExists(tempDir)) {
|
||||||
|
await fs.remove(tempDir);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should run a simple script successfully', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
name: 'test-package',
|
||||||
|
version: '1.0.0',
|
||||||
|
scripts: {
|
||||||
|
hello: 'echo "Hello World"'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await fs.writeJson(path.join(tempDir, 'package.json'), packageJson);
|
||||||
|
|
||||||
|
// This should not throw
|
||||||
|
await packageManager.runScript('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail when script does not exist', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
name: 'test-package',
|
||||||
|
version: '1.0.0',
|
||||||
|
scripts: {
|
||||||
|
hello: 'echo "Hello World"'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await fs.writeJson(path.join(tempDir, 'package.json'), packageJson);
|
||||||
|
|
||||||
|
await expect(packageManager.runScript('nonexistent')).rejects.toThrow(
|
||||||
|
'Script "nonexistent" not found'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not fail when script does not exist with ifPresent option', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
name: 'test-package',
|
||||||
|
version: '1.0.0',
|
||||||
|
scripts: {
|
||||||
|
hello: 'echo "Hello World"'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await fs.writeJson(path.join(tempDir, 'package.json'), packageJson);
|
||||||
|
|
||||||
|
// This should not throw
|
||||||
|
await packageManager.runScript('nonexistent', { ifPresent: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail when package.json does not exist', async () => {
|
||||||
|
// Override ensureInitialized to prevent auto-creation
|
||||||
|
const originalEnsureInitialized = packageManager.ensureInitialized;
|
||||||
|
packageManager.ensureInitialized = async () => {};
|
||||||
|
|
||||||
|
await expect(packageManager.runScript('hello')).rejects.toThrow(
|
||||||
|
'No package.json found in current directory'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore original method
|
||||||
|
packageManager.ensureInitialized = originalEnsureInitialized;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail when package.json has no scripts section', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
name: 'test-package',
|
||||||
|
version: '1.0.0'
|
||||||
|
};
|
||||||
|
|
||||||
|
await fs.writeJson(path.join(tempDir, 'package.json'), packageJson);
|
||||||
|
|
||||||
|
await expect(packageManager.runScript('hello')).rejects.toThrow(
|
||||||
|
'No scripts section found in package.json'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not fail when package.json has no scripts section with ifPresent option', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
name: 'test-package',
|
||||||
|
version: '1.0.0'
|
||||||
|
};
|
||||||
|
|
||||||
|
await fs.writeJson(path.join(tempDir, 'package.json'), packageJson);
|
||||||
|
|
||||||
|
// This should not throw
|
||||||
|
await packageManager.runScript('hello', { ifPresent: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should run script with silent option', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
name: 'test-package',
|
||||||
|
version: '1.0.0',
|
||||||
|
scripts: {
|
||||||
|
hello: 'echo "Hello World"'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await fs.writeJson(path.join(tempDir, 'package.json'), packageJson);
|
||||||
|
|
||||||
|
// This should not throw
|
||||||
|
await packageManager.runScript('hello', { silent: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle script that exits with error code', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
name: 'test-package',
|
||||||
|
version: '1.0.0',
|
||||||
|
scripts: {
|
||||||
|
fail: 'nonexistentcommand123456' // This command doesn't exist
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await fs.writeJson(path.join(tempDir, 'package.json'), packageJson);
|
||||||
|
|
||||||
|
await expect(packageManager.runScript('fail')).rejects.toThrow(
|
||||||
|
'Script "fail" exited with code 127'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Referencia en una nueva incidencia
Block a user