@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
1,170 lines (1,169 loc) • 48.6 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.DependencyConflictDetector = exports.ConflictType = void 0;
exports.getDependencyConflictDetector = getDependencyConflictDetector;
exports.detectDependencyConflicts = detectDependencyConflicts;
const events_1 = require("events");
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const semver = __importStar(require("semver"));
const child_process_1 = require("child_process");
var ConflictType;
(function (ConflictType) {
ConflictType["VERSION_MISMATCH"] = "version_mismatch";
ConflictType["PEER_DEPENDENCY_UNMET"] = "peer_dependency_unmet";
ConflictType["DUPLICATE_PACKAGE"] = "duplicate_package";
ConflictType["CIRCULAR_DEPENDENCY"] = "circular_dependency";
ConflictType["INCOMPATIBLE_ENGINES"] = "incompatible_engines";
ConflictType["LICENSE_CONFLICT"] = "license_conflict";
ConflictType["SECURITY_VULNERABILITY"] = "security_vulnerability";
ConflictType["DEPRECATED_PACKAGE"] = "deprecated_package";
})(ConflictType || (exports.ConflictType = ConflictType = {}));
class DependencyConflictDetector extends events_1.EventEmitter {
constructor() {
super();
this.packageCache = new Map();
this.visitedPackages = new Set();
this.licenseCompatibility = new Map();
this.defaultOptions = {
checkPeerDependencies: true,
checkEngines: true,
checkLicenses: true,
checkSecurity: true,
checkCircular: true,
maxDepth: 10,
includeDevDependencies: false,
includeOptionalDependencies: false,
strictVersioning: false
};
this.initializeLicenseCompatibility();
}
initializeLicenseCompatibility() {
// Define license compatibility rules
this.licenseCompatibility.set('MIT', ['MIT', 'ISC', 'BSD', 'Apache-2.0', 'CC0-1.0']);
this.licenseCompatibility.set('Apache-2.0', ['MIT', 'ISC', 'BSD', 'Apache-2.0']);
this.licenseCompatibility.set('GPL-3.0', ['GPL-3.0', 'AGPL-3.0']);
this.licenseCompatibility.set('ISC', ['MIT', 'ISC', 'BSD', 'Apache-2.0']);
this.licenseCompatibility.set('BSD', ['MIT', 'ISC', 'BSD', 'Apache-2.0']);
}
async detectConflicts(projectPath, options = {}) {
this.emit('detection:start', { projectPath, options });
const opts = { ...this.defaultOptions, ...options };
const conflicts = [];
try {
// Load package.json
const packageJsonPath = path.join(projectPath, 'package.json');
if (!await fs.pathExists(packageJsonPath)) {
throw new Error('No package.json found in project');
}
const packageJson = await fs.readJson(packageJsonPath);
// Build dependency tree
this.dependencyTree = await this.buildDependencyTree(projectPath, packageJson, opts);
// Detect various types of conflicts
if (opts.checkPeerDependencies) {
const peerConflicts = await this.detectPeerDependencyConflicts(this.dependencyTree, opts);
conflicts.push(...peerConflicts);
}
const versionConflicts = await this.detectVersionConflicts(this.dependencyTree, opts);
conflicts.push(...versionConflicts);
const duplicates = await this.detectDuplicatePackages(this.dependencyTree, opts);
conflicts.push(...duplicates);
if (opts.checkCircular) {
const circular = await this.detectCircularDependencies(this.dependencyTree, opts);
conflicts.push(...circular);
}
if (opts.checkEngines) {
const engineConflicts = await this.detectEngineConflicts(this.dependencyTree, opts);
conflicts.push(...engineConflicts);
}
if (opts.checkLicenses) {
const licenseConflicts = await this.detectLicenseConflicts(this.dependencyTree, opts);
conflicts.push(...licenseConflicts);
}
if (opts.checkSecurity) {
const securityIssues = await this.detectSecurityVulnerabilities(projectPath, opts);
conflicts.push(...securityIssues);
}
// Generate report
const report = this.generateReport(conflicts, this.dependencyTree);
this.emit('detection:complete', report);
return report;
}
catch (error) {
this.emit('detection:error', error);
throw error;
}
}
async buildDependencyTree(projectPath, packageJson, options, parent, depth = 0) {
if (depth > (options.maxDepth || 10)) {
return {
name: packageJson.name,
version: packageJson.version,
dependencies: new Map(),
parent
};
}
const tree = {
name: packageJson.name,
version: packageJson.version,
dependencies: new Map(),
parent
};
// Collect all dependencies
const allDeps = {
...packageJson.dependencies,
...(options.includeDevDependencies ? packageJson.devDependencies : {}),
...(options.includeOptionalDependencies ? packageJson.optionalDependencies : {})
};
// Process each dependency
for (const [depName, depVersion] of Object.entries(allDeps)) {
const key = `${depName}@${depVersion}`;
// Avoid circular references
if (this.visitedPackages.has(key)) {
continue;
}
this.visitedPackages.add(key);
try {
const depInfo = await this.getPackageInfo(depName, depVersion, projectPath);
if (depInfo) {
const subTree = await this.buildDependencyTree(projectPath, depInfo, options, tree, depth + 1);
tree.dependencies.set(depName, subTree);
}
}
catch (error) {
// Package not found or error loading
this.emit('dependency:error', { name: depName, version: depVersion, error });
}
}
return tree;
}
async getPackageInfo(name, version, projectPath) {
const cacheKey = `${name}@${version}`;
if (this.packageCache.has(cacheKey)) {
return this.packageCache.get(cacheKey);
}
try {
// Try to load from node_modules
const packagePath = path.join(projectPath, 'node_modules', name, 'package.json');
if (await fs.pathExists(packagePath)) {
const packageJson = await fs.readJson(packagePath);
const info = {
name: packageJson.name,
version: packageJson.version,
description: packageJson.description,
engines: packageJson.engines,
license: packageJson.license,
deprecated: packageJson.deprecated,
repository: packageJson.repository,
dependencies: packageJson.dependencies,
peerDependencies: packageJson.peerDependencies,
devDependencies: packageJson.devDependencies,
optionalDependencies: packageJson.optionalDependencies
};
this.packageCache.set(cacheKey, info);
return info;
}
// If not in node_modules, try npm view (slower)
const npmInfo = await this.getNpmPackageInfo(name, version);
if (npmInfo) {
this.packageCache.set(cacheKey, npmInfo);
return npmInfo;
}
return null;
}
catch (error) {
return null;
}
}
async getNpmPackageInfo(name, version) {
try {
const output = (0, child_process_1.execSync)(`npm view ${name}@${version} --json`, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
});
const data = JSON.parse(output);
return {
name: data.name,
version: data.version,
description: data.description,
engines: data.engines,
license: data.license,
deprecated: data.deprecated,
repository: data.repository,
dependencies: data.dependencies,
peerDependencies: data.peerDependencies,
devDependencies: data.devDependencies,
optionalDependencies: data.optionalDependencies
};
}
catch {
return null;
}
}
async detectVersionConflicts(tree, options) {
const conflicts = [];
const versionMap = new Map();
// Collect all versions of each package
this.collectVersions(tree, versionMap);
// Find conflicts
for (const [packageName, versions] of versionMap) {
if (versions.length <= 1)
continue;
// Check if versions are compatible
const uniqueVersions = [...new Set(versions.map(v => v.version))];
if (uniqueVersions.length > 1) {
const incompatible = this.findIncompatibleVersions(uniqueVersions, options.strictVersioning);
if (incompatible.length > 0) {
const conflict = {
name: packageName,
type: ConflictType.VERSION_MISMATCH,
severity: this.calculateVersionConflictSeverity(incompatible),
packages: versions.map(v => ({
name: packageName,
version: v.version,
requiredBy: v.requiredBy
})),
description: `Multiple incompatible versions of ${packageName} detected`,
impact: [
'May cause runtime errors',
'Increased bundle size',
'Unpredictable behavior'
],
resolutions: this.generateVersionResolutions(packageName, versions, incompatible),
autoResolvable: true
};
conflicts.push(conflict);
}
}
}
return conflicts;
}
collectVersions(tree, versionMap, parentPath = '') {
const currentPath = parentPath ? `${parentPath} > ${tree.name}` : tree.name;
for (const [depName, depTree] of tree.dependencies) {
if (!versionMap.has(depName)) {
versionMap.set(depName, []);
}
const versions = versionMap.get(depName);
const existing = versions.find(v => v.version === depTree.version);
if (existing) {
existing.requiredBy.push(currentPath);
}
else {
versions.push({
version: depTree.version,
requiredBy: [currentPath]
});
}
// Recurse
this.collectVersions(depTree, versionMap, currentPath);
}
}
findIncompatibleVersions(versions, strict) {
if (strict) {
// In strict mode, all different versions are incompatible
return versions.length > 1 ? versions : [];
}
const incompatible = [];
for (let i = 0; i < versions.length; i++) {
for (let j = i + 1; j < versions.length; j++) {
if (!semver.intersects(versions[i], versions[j])) {
if (!incompatible.includes(versions[i]))
incompatible.push(versions[i]);
if (!incompatible.includes(versions[j]))
incompatible.push(versions[j]);
}
}
}
return incompatible;
}
calculateVersionConflictSeverity(incompatibleVersions) {
if (incompatibleVersions.length > 3)
return 'critical';
// Check if major versions differ
const majorVersions = incompatibleVersions.map(v => semver.major(v));
const uniqueMajors = [...new Set(majorVersions)];
if (uniqueMajors.length > 1)
return 'high';
// Check if minor versions differ significantly
const minorVersions = incompatibleVersions.map(v => semver.minor(v));
const minorDiff = Math.max(...minorVersions) - Math.min(...minorVersions);
if (minorDiff > 5)
return 'medium';
return 'low';
}
generateVersionResolutions(packageName, versions, incompatible) {
const resolutions = [];
// Find the most recent compatible version
const allVersions = versions.map(v => v.version);
const latestVersion = allVersions.sort(semver.rcompare)[0];
// Resolution 1: Upgrade all to latest
resolutions.push({
type: 'upgrade',
description: `Upgrade all instances to ${latestVersion}`,
command: `npm install ${packageName}@${latestVersion}`,
risk: 'medium',
automated: true,
steps: [
`Update package.json to use ${packageName}@${latestVersion}`,
'Run npm install',
'Test thoroughly for breaking changes'
]
});
// Resolution 2: Use resolutions/overrides
resolutions.push({
type: 'override',
description: 'Force a single version using package manager overrides',
config: {
npm: {
overrides: {
[packageName]: latestVersion
}
},
yarn: {
resolutions: {
[packageName]: latestVersion
}
},
pnpm: {
overrides: {
[packageName]: latestVersion
}
}
},
risk: 'high',
automated: true,
steps: [
'Add overrides/resolutions to package.json',
'Delete node_modules and lock file',
'Reinstall dependencies'
]
});
// Resolution 3: Find a compatible version range
const compatibleRange = this.findCompatibleRange(allVersions);
if (compatibleRange) {
resolutions.push({
type: 'downgrade',
description: `Use compatible version range ${compatibleRange}`,
command: `npm install ${packageName}@"${compatibleRange}"`,
risk: 'low',
automated: true
});
}
return resolutions;
}
findCompatibleRange(versions) {
// Try to find a version range that satisfies all
for (const version of versions) {
const range = `^${version}`;
if (versions.every(v => semver.satisfies(v, range))) {
return range;
}
}
// Try with tilde
for (const version of versions) {
const range = `~${version}`;
if (versions.every(v => semver.satisfies(v, range))) {
return range;
}
}
return null;
}
async detectPeerDependencyConflicts(tree, options) {
const conflicts = [];
const peerDeps = new Map();
// Collect all peer dependencies
await this.collectPeerDependencies(tree, peerDeps);
// Check if peer dependencies are satisfied
for (const [depName, requirements] of peerDeps) {
const installed = this.findInstalledVersion(tree, depName);
const unmet = requirements.filter(req => {
if (!installed)
return true;
return !semver.satisfies(installed, req.required);
});
if (unmet.length > 0) {
const conflict = {
name: depName,
type: ConflictType.PEER_DEPENDENCY_UNMET,
severity: 'high',
packages: unmet.map(u => ({
name: depName,
version: u.required,
requiredBy: [u.requiredBy],
constraints: [u.required]
})),
description: `Peer dependency ${depName} is not satisfied`,
impact: [
'Package may not function correctly',
'Runtime errors possible',
'Feature incompatibility'
],
resolutions: this.generatePeerDependencyResolutions(depName, unmet, installed),
autoResolvable: true
};
conflicts.push(conflict);
}
}
return conflicts;
}
async collectPeerDependencies(tree, peerDeps, path = '') {
const currentPath = path ? `${path} > ${tree.name}` : tree.name;
// Get package info from cache
const packageInfo = this.packageCache.get(`${tree.name}@${tree.version}`);
if (packageInfo?.peerDependencies) {
for (const [depName, depVersion] of Object.entries(packageInfo.peerDependencies)) {
if (!peerDeps.has(depName)) {
peerDeps.set(depName, []);
}
peerDeps.get(depName).push({
package: tree.name,
required: depVersion,
requiredBy: currentPath
});
}
}
// Recurse through dependencies
for (const [_, depTree] of tree.dependencies) {
await this.collectPeerDependencies(depTree, peerDeps, currentPath);
}
}
findInstalledVersion(tree, packageName) {
// Check direct dependencies
const directDep = tree.dependencies.get(packageName);
if (directDep) {
return directDep.version;
}
// Check nested dependencies
for (const [_, depTree] of tree.dependencies) {
const version = this.findInstalledVersion(depTree, packageName);
if (version)
return version;
}
return null;
}
generatePeerDependencyResolutions(packageName, unmet, installed) {
const resolutions = [];
// Find a version that satisfies all requirements
const allConstraints = unmet.map(u => u.required);
const satisfyingVersion = this.findSatisfyingVersion(allConstraints);
if (satisfyingVersion) {
resolutions.push({
type: 'upgrade',
description: `Install ${packageName}@${satisfyingVersion} to satisfy all peer dependencies`,
command: `npm install ${packageName}@${satisfyingVersion}`,
risk: 'low',
automated: true
});
}
// Add as direct dependency
resolutions.push({
type: 'upgrade',
description: `Add ${packageName} as a direct dependency`,
command: `npm install ${packageName}`,
risk: 'medium',
automated: true,
steps: [
'This will add the package to your dependencies',
'May increase bundle size',
'Ensures peer dependency is always available'
]
});
// Suggest updating the requiring packages
const requiringPackages = [...new Set(unmet.map(u => u.package))];
resolutions.push({
type: 'upgrade',
description: `Update packages that require ${packageName}`,
command: requiringPackages.map(p => `npm update ${p}`).join(' && '),
risk: 'medium',
automated: false,
steps: [
'Check for updates to requiring packages',
'They may have relaxed peer dependency requirements',
'Test thoroughly after updates'
]
});
return resolutions;
}
findSatisfyingVersion(constraints) {
// This is a simplified version finder
// In production, you'd query npm registry for available versions
try {
// Find intersection of all constraints
let range = constraints[0];
for (let i = 1; i < constraints.length; i++) {
const intersection = semver.intersects(range, constraints[i]);
if (!intersection)
return null;
// This is simplified - in reality, you'd compute the actual intersection
range = constraints[i];
}
// Return a version that satisfies the range
return semver.minVersion(range)?.format() || null;
}
catch {
return null;
}
}
async detectDuplicatePackages(tree, options) {
const conflicts = [];
const packageLocations = new Map();
// Collect all package instances
this.collectPackageLocations(tree, packageLocations);
// Find duplicates
for (const [packageName, locations] of packageLocations) {
if (locations.length <= 1)
continue;
const uniqueVersions = [...new Set(locations.map(l => l.version))];
// Only consider it a duplicate if same version appears multiple times
if (uniqueVersions.length < locations.length) {
const conflict = {
name: packageName,
type: ConflictType.DUPLICATE_PACKAGE,
severity: 'medium',
packages: locations.map(l => ({
name: packageName,
version: l.version,
requiredBy: [l.path],
location: l.path
})),
description: `Package ${packageName} is duplicated in multiple locations`,
impact: [
'Increased bundle size',
'Longer install times',
'Potential version conflicts'
],
resolutions: this.generateDuplicationResolutions(packageName, locations),
autoResolvable: true
};
conflicts.push(conflict);
}
}
return conflicts;
}
collectPackageLocations(tree, locations, path = '') {
const currentPath = path ? `${path}/${tree.name}` : tree.name;
for (const [depName, depTree] of tree.dependencies) {
if (!locations.has(depName)) {
locations.set(depName, []);
}
locations.get(depName).push({
version: depTree.version,
path: `${currentPath}/${depName}`
});
this.collectPackageLocations(depTree, locations, currentPath);
}
}
generateDuplicationResolutions(packageName, locations) {
const resolutions = [];
// Deduplicate using npm/yarn
resolutions.push({
type: 'upgrade',
description: 'Deduplicate packages using package manager',
command: 'npm dedupe',
risk: 'low',
automated: true,
steps: [
'Runs package manager deduplication',
'Moves packages to highest common location',
'Reduces node_modules size'
]
});
// Use pnpm for better deduplication
resolutions.push({
type: 'replace',
description: 'Switch to pnpm for automatic deduplication',
command: 'npm install -g pnpm && pnpm import && pnpm install',
risk: 'medium',
automated: false,
steps: [
'pnpm uses a content-addressable store',
'Automatically deduplicates packages',
'Significantly reduces disk usage'
]
});
return resolutions;
}
async detectCircularDependencies(tree, options) {
const conflicts = [];
const visited = new Set();
const recursionStack = new Set();
const cycles = [];
// DFS to detect cycles
this.detectCycles(tree, visited, recursionStack, [], cycles);
// Convert cycles to conflicts
for (const cycle of cycles) {
const conflict = {
name: cycle.join(' → '),
type: ConflictType.CIRCULAR_DEPENDENCY,
severity: 'high',
packages: cycle.map(pkg => ({
name: pkg,
version: '',
requiredBy: []
})),
description: `Circular dependency detected: ${cycle.join(' → ')} → ${cycle[0]}`,
impact: [
'Can cause initialization issues',
'Makes code harder to maintain',
'May prevent proper module loading',
'Complicates testing'
],
resolutions: this.generateCircularDependencyResolutions(cycle),
autoResolvable: false
};
conflicts.push(conflict);
}
return conflicts;
}
detectCycles(tree, visited, recursionStack, path, cycles) {
const nodeId = `${tree.name}@${tree.version}`;
visited.add(nodeId);
recursionStack.add(nodeId);
path.push(tree.name);
for (const [depName, depTree] of tree.dependencies) {
const depId = `${depTree.name}@${depTree.version}`;
if (!visited.has(depId)) {
this.detectCycles(depTree, visited, recursionStack, path, cycles);
}
else if (recursionStack.has(depId)) {
// Found a cycle
const cycleStart = path.indexOf(depTree.name);
if (cycleStart !== -1) {
const cycle = path.slice(cycleStart);
cycles.push(cycle);
}
}
}
path.pop();
recursionStack.delete(nodeId);
}
generateCircularDependencyResolutions(cycle) {
return [
{
type: 'remove',
description: 'Refactor code to break circular dependency',
risk: 'high',
automated: false,
steps: [
'Identify the coupling between modules',
'Extract shared code to a separate module',
'Use dependency injection or events',
'Consider restructuring module boundaries'
]
},
{
type: 'replace',
description: 'Use dynamic imports to break the cycle',
risk: 'medium',
automated: false,
steps: [
'Convert static imports to dynamic imports',
'Load modules on-demand',
'May impact performance and type safety'
]
}
];
}
async detectEngineConflicts(tree, options) {
const conflicts = [];
const currentNode = process.version;
const currentNpm = await this.getNpmVersion();
const engineRequirements = [];
// Collect all engine requirements
this.collectEngineRequirements(tree, engineRequirements);
// Check Node.js version conflicts
const nodeConflicts = engineRequirements.filter(req => {
if (!req.engines.node)
return false;
return !semver.satisfies(currentNode, req.engines.node);
});
if (nodeConflicts.length > 0) {
conflicts.push({
name: 'Node.js version',
type: ConflictType.INCOMPATIBLE_ENGINES,
severity: 'critical',
packages: nodeConflicts.map(c => ({
name: c.package,
version: '',
requiredBy: [c.path],
constraints: [`node ${c.engines.node}`]
})),
description: `Current Node.js version (${currentNode}) incompatible with package requirements`,
impact: [
'Packages may not work correctly',
'Build failures possible',
'Runtime errors likely'
],
resolutions: this.generateEngineResolutions('node', currentNode, nodeConflicts),
autoResolvable: false
});
}
// Check npm version conflicts
const npmConflicts = engineRequirements.filter(req => {
if (!req.engines.npm || !currentNpm)
return false;
return !semver.satisfies(currentNpm, req.engines.npm);
});
if (npmConflicts.length > 0) {
conflicts.push({
name: 'npm version',
type: ConflictType.INCOMPATIBLE_ENGINES,
severity: 'high',
packages: npmConflicts.map(c => ({
name: c.package,
version: '',
requiredBy: [c.path],
constraints: [`npm ${c.engines.npm}`]
})),
description: `Current npm version (${currentNpm}) incompatible with package requirements`,
impact: [
'Installation may fail',
'Package scripts may not work'
],
resolutions: this.generateEngineResolutions('npm', currentNpm || '', npmConflicts),
autoResolvable: true
});
}
return conflicts;
}
collectEngineRequirements(tree, requirements, path = '') {
const currentPath = path ? `${path} > ${tree.name}` : tree.name;
const packageInfo = this.packageCache.get(`${tree.name}@${tree.version}`);
if (packageInfo?.engines) {
requirements.push({
package: tree.name,
engines: packageInfo.engines,
path: currentPath
});
}
for (const [_, depTree] of tree.dependencies) {
this.collectEngineRequirements(depTree, requirements, currentPath);
}
}
async getNpmVersion() {
try {
return (0, child_process_1.execSync)('npm --version', { encoding: 'utf8' }).trim();
}
catch {
return null;
}
}
generateEngineResolutions(engine, current, conflicts) {
const resolutions = [];
// Find compatible version
const requirements = conflicts.map(c => c.engines[engine]).filter(Boolean);
const compatibleVersion = this.findSatisfyingVersion(requirements);
if (engine === 'node') {
resolutions.push({
type: 'upgrade',
description: `Use nvm to install Node.js ${compatibleVersion || 'LTS'}`,
command: `nvm install ${compatibleVersion || 'lts/*'} && nvm use ${compatibleVersion || 'lts/*'}`,
risk: 'medium',
automated: false,
steps: [
'Install nvm if not already installed',
'Install compatible Node.js version',
'Switch to the compatible version',
'Update .nvmrc file in project'
]
});
resolutions.push({
type: 'replace',
description: 'Use Docker to ensure consistent environment',
risk: 'low',
automated: false,
steps: [
'Create Dockerfile with compatible Node.js version',
'Use Docker for development and deployment',
'Ensures environment consistency'
]
});
}
else if (engine === 'npm') {
resolutions.push({
type: 'upgrade',
description: `Update npm to ${compatibleVersion || 'latest'}`,
command: `npm install -g npm@${compatibleVersion || 'latest'}`,
risk: 'low',
automated: true
});
}
return resolutions;
}
async detectLicenseConflicts(tree, options) {
const conflicts = [];
const projectLicense = tree.name; // Should get from package.json
const licenses = new Map();
// Collect all licenses
this.collectLicenses(tree, licenses);
// Check for incompatible licenses
const incompatibleLicenses = this.findIncompatibleLicenses(licenses);
for (const [licenseType, packages] of incompatibleLicenses) {
conflicts.push({
name: `${licenseType} license`,
type: ConflictType.LICENSE_CONFLICT,
severity: 'high',
packages: packages.map(p => ({
name: p.package,
version: '',
requiredBy: [p.path]
})),
description: `Potentially incompatible ${licenseType} license detected`,
impact: [
'Legal compliance issues',
'May restrict commercial use',
'Could require source code disclosure'
],
resolutions: this.generateLicenseResolutions(licenseType, packages),
autoResolvable: false
});
}
return conflicts;
}
collectLicenses(tree, licenses, path = '') {
const currentPath = path ? `${path} > ${tree.name}` : tree.name;
const packageInfo = this.packageCache.get(`${tree.name}@${tree.version}`);
if (packageInfo?.license) {
const licenseType = this.normalizeLicense(packageInfo.license);
if (!licenses.has(licenseType)) {
licenses.set(licenseType, []);
}
licenses.get(licenseType).push({
package: tree.name,
license: packageInfo.license,
path: currentPath
});
}
for (const [_, depTree] of tree.dependencies) {
this.collectLicenses(depTree, licenses, currentPath);
}
}
normalizeLicense(license) {
// Normalize common license variations
const normalized = license.toUpperCase().replace(/[^A-Z0-9]/g, '');
const mapping = {
'MIT': 'MIT',
'MITLICENSE': 'MIT',
'APACHE2': 'Apache-2.0',
'APACHE20': 'Apache-2.0',
'GPL3': 'GPL-3.0',
'GPL30': 'GPL-3.0',
'BSD': 'BSD',
'BSD3CLAUSE': 'BSD',
'ISC': 'ISC'
};
return mapping[normalized] || license;
}
findIncompatibleLicenses(licenses) {
const incompatible = new Map();
// Check for GPL in non-GPL project
if (licenses.has('GPL-3.0') || licenses.has('AGPL-3.0')) {
const gplPackages = [
...(licenses.get('GPL-3.0') || []),
...(licenses.get('AGPL-3.0') || [])
];
if (gplPackages.length > 0) {
incompatible.set('GPL', gplPackages);
}
}
return incompatible;
}
generateLicenseResolutions(licenseType, packages) {
const resolutions = [];
resolutions.push({
type: 'replace',
description: 'Find alternative packages with compatible licenses',
risk: 'medium',
automated: false,
steps: [
'Research alternative packages',
'Check license compatibility',
'Test replacement packages',
'Update dependencies'
]
});
resolutions.push({
type: 'remove',
description: 'Remove packages with incompatible licenses',
command: packages.map(p => `npm uninstall ${p.package}`).join(' && '),
risk: 'high',
automated: false,
steps: [
'Assess impact of removing packages',
'Find alternative implementations',
'Remove and test thoroughly'
]
});
return resolutions;
}
async detectSecurityVulnerabilities(projectPath, options) {
const conflicts = [];
try {
// Run npm audit
const auditOutput = (0, child_process_1.execSync)('npm audit --json', {
cwd: projectPath,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
});
const audit = JSON.parse(auditOutput);
if (audit.vulnerabilities) {
for (const [packageName, vuln] of Object.entries(audit.vulnerabilities)) {
const vulnData = vuln;
conflicts.push({
name: packageName,
type: ConflictType.SECURITY_VULNERABILITY,
severity: this.mapAuditSeverity(vulnData.severity),
packages: [{
name: packageName,
version: vulnData.range,
requiredBy: vulnData.via.map((v) => typeof v === 'string' ? v : v.name)
}],
description: vulnData.title || `Security vulnerability in ${packageName}`,
impact: [
'Security risk to application',
'Potential data breach',
'Compliance issues'
],
resolutions: this.generateSecurityResolutions(packageName, vulnData),
autoResolvable: vulnData.fixAvailable
});
}
}
}
catch (error) {
// npm audit failed - not critical
this.emit('security:check:failed', error);
}
return conflicts;
}
mapAuditSeverity(severity) {
switch (severity.toLowerCase()) {
case 'critical': return 'critical';
case 'high': return 'high';
case 'moderate': return 'medium';
case 'low': return 'low';
default: return 'medium';
}
}
generateSecurityResolutions(packageName, vulnData) {
const resolutions = [];
if (vulnData.fixAvailable) {
resolutions.push({
type: 'upgrade',
description: 'Apply automated security fix',
command: 'npm audit fix',
risk: 'low',
automated: true
});
resolutions.push({
type: 'upgrade',
description: 'Force security updates (may include breaking changes)',
command: 'npm audit fix --force',
risk: 'high',
automated: true,
steps: [
'This may update to semver-major versions',
'Test thoroughly after update',
'May require code changes'
]
});
}
resolutions.push({
type: 'upgrade',
description: `Update ${packageName} to patched version`,
command: `npm update ${packageName}`,
risk: 'medium',
automated: true
});
return resolutions;
}
generateReport(conflicts, tree) {
const summary = {
total: conflicts.length,
critical: conflicts.filter(c => c.severity === 'critical').length,
high: conflicts.filter(c => c.severity === 'high').length,
medium: conflicts.filter(c => c.severity === 'medium').length,
low: conflicts.filter(c => c.severity === 'low').length,
autoResolvable: conflicts.filter(c => c.autoResolvable).length
};
const recommendations = this.generateRecommendations(conflicts);
const healthScore = this.calculateHealthScore(conflicts);
return {
conflicts,
summary,
dependencyTree: tree,
recommendations,
healthScore
};
}
generateRecommendations(conflicts) {
const recommendations = [];
if (conflicts.some(c => c.type === ConflictType.VERSION_MISMATCH)) {
recommendations.push('Consider using a single package manager with strict versioning');
recommendations.push('Use lockfiles to ensure consistent installations');
}
if (conflicts.some(c => c.type === ConflictType.DUPLICATE_PACKAGE)) {
recommendations.push('Run deduplication regularly to optimize node_modules');
recommendations.push('Consider using pnpm for automatic deduplication');
}
if (conflicts.some(c => c.type === ConflictType.SECURITY_VULNERABILITY)) {
recommendations.push('Set up automated security scanning in CI/CD');
recommendations.push('Regularly update dependencies to patch vulnerabilities');
}
if (conflicts.some(c => c.type === ConflictType.CIRCULAR_DEPENDENCY)) {
recommendations.push('Review module architecture to minimize coupling');
recommendations.push('Use dependency injection patterns');
}
return recommendations;
}
calculateHealthScore(conflicts) {
let score = 100;
// Deduct points based on severity
score -= conflicts.filter(c => c.severity === 'critical').length * 20;
score -= conflicts.filter(c => c.severity === 'high').length * 10;
score -= conflicts.filter(c => c.severity === 'medium').length * 5;
score -= conflicts.filter(c => c.severity === 'low').length * 2;
// Bonus points for auto-resolvable conflicts
const autoResolvableRatio = conflicts.length > 0
? conflicts.filter(c => c.autoResolvable).length / conflicts.length
: 1;
score += autoResolvableRatio * 10;
return Math.max(0, Math.min(100, score));
}
async resolveConflicts(conflicts, options = {}) {
const resolved = [];
const failed = [];
const skipped = [];
for (const conflict of conflicts) {
if (options.autoOnly && !conflict.autoResolvable) {
skipped.push(conflict.name);
continue;
}
// Find automated resolutions
const autoResolutions = conflict.resolutions.filter(r => r.automated);
if (autoResolutions.length === 0) {
skipped.push(conflict.name);
continue;
}
// Use the lowest risk automated resolution
const resolution = autoResolutions.sort((a, b) => {
const riskScore = { low: 1, medium: 2, high: 3 };
return riskScore[a.risk] - riskScore[b.risk];
})[0];
try {
if (!options.dryRun && resolution.command) {
this.emit('resolution:start', { conflict: conflict.name, resolution });
(0, child_process_1.execSync)(resolution.command, { stdio: 'inherit' });
resolved.push(conflict.name);
this.emit('resolution:success', { conflict: conflict.name });
}
else if (options.dryRun) {
console.log(`Would run: ${resolution.command}`);
resolved.push(conflict.name);
}
}
catch (error) {
failed.push(conflict.name);
this.emit('resolution:failed', { conflict: conflict.name, error });
}
}
return { resolved, failed, skipped };
}
exportReport(report, format = 'markdown') {
if (format === 'json') {
return JSON.stringify(report, null, 2);
}
// Markdown format
const lines = [];
lines.push('# Dependency Conflict Report');
lines.push(`Generated: ${new Date().toISOString()}`);
lines.push('');
lines.push('## Summary');
lines.push(`- Total Conflicts: ${report.summary.total}`);
lines.push(`- Critical: ${report.summary.critical}`);
lines.push(`- High: ${report.summary.high}`);
lines.push(`- Medium: ${report.summary.medium}`);
lines.push(`- Low: ${report.summary.low}`);
lines.push(`- Auto-resolvable: ${report.summary.autoResolvable}`);
lines.push(`- Health Score: ${report.healthScore}/100`);
lines.push('');
if (report.conflicts.length > 0) {
lines.push('## Conflicts');
for (const conflict of report.conflicts) {
lines.push(`### ${conflict.severity.toUpperCase()}: ${conflict.name}`);
lines.push(`**Type**: ${conflict.type.replace(/_/g, ' ')}`);
lines.push(`**Description**: ${conflict.description}`);
if (conflict.impact.length > 0) {
lines.push('**Impact**:');
for (const impact of conflict.impact) {
lines.push(`- ${impact}`);
}
}
if (conflict.resolutions.length > 0) {
lines.push('**Resolutions**:');
for (const resolution of conflict.resolutions) {
lines.push(`- ${resolution.description} (Risk: ${resolution.risk})`);
if (resolution.command) {
lines.push(` \`${resolution.command}\``);
}
}
}
lines.push('');
}
}
if (report.recommendations.length > 0) {
lines.push('## Recommendations');
for (const rec of report.recommendations) {
lines.push(`- ${rec}`);
}
}
return lines.join('\n');
}
}
exports.DependencyConflictDetector = DependencyConflictDetector;
// Global instance
let globalDetector = null;
function getDependencyConflictDetector() {
if (!globalDetector) {
globalDetector = new DependencyConflictDetector();
}
return globalDetector;
}
async function detectDependencyConflicts(projectPath, options) {
const detector = getDependencyConflictDetector();
return detector.detectConflicts(projectPath, options);
}