// Copyright 2025 Chris Bunting
// Brief: Detects package managers in project directories
// Scope: Identifies which package managers are being used in a project
import { PackageManager } from '@mcp-code-analysis/shared-types';
import * as fs from 'fs';
import * as path from 'path';
import { Logger } from '../utils/Logger.js';
export interface PackageManagerDetection {
manager: PackageManager;
confidence: number;
files: string[];
}
export class PackageManagerDetector {
private logger: Logger;
private packageManagerSignatures: Map<PackageManager, string[]>;
constructor(logger?: Logger) {
this.logger = logger || new Logger();
this.packageManagerSignatures = new Map([
[PackageManager.NPM, ['package.json', 'package-lock.json', 'node_modules']],
[PackageManager.YARN, ['package.json', 'yarn.lock', 'node_modules']],
[PackageManager.PIP, ['requirements.txt', 'setup.py', 'pyproject.toml', 'Pipfile', 'poetry.lock']],
[PackageManager.CARGO, ['Cargo.toml', 'Cargo.lock']],
[PackageManager.MAVEN, ['pom.xml', 'mvnw', '.mvn']],
[PackageManager.GRADLE, ['build.gradle', 'build.gradle.kts', 'gradlew', '.gradle']],
[PackageManager.GO, ['go.mod', 'go.sum', 'go.work']],
]);
}
async detectPackageManagers(projectPath: string): Promise<PackageManagerDetection[]> {
this.logger.debug(`Detecting package managers in: ${projectPath}`);
if (!fs.existsSync(projectPath)) {
throw new Error(`Project path does not exist: ${projectPath}`);
}
const detections: PackageManagerDetection[] = [];
for (const [manager, signatures] of this.packageManagerSignatures) {
const detection = await this.detectPackageManager(projectPath, manager, signatures);
if (detection.confidence > 0) {
detections.push(detection);
}
}
// Sort by confidence level
detections.sort((a, b) => b.confidence - a.confidence);
this.logger.debug(`Detected package managers: ${detections.map(d => `${d.manager} (${d.confidence})`).join(', ')}`);
return detections;
}
private async detectPackageManager(
projectPath: string,
manager: PackageManager,
signatures: string[]
): Promise<PackageManagerDetection> {
const foundFiles: string[] = [];
let confidence = 0;
for (const signature of signatures) {
const filePath = path.join(projectPath, signature);
if (fs.existsSync(filePath)) {
foundFiles.push(signature);
// Assign confidence based on file importance
if (this.isPrimaryManifest(signature, manager)) {
confidence += 0.6;
} else if (this.isLockFile(signature, manager)) {
confidence += 0.3;
} else {
confidence += 0.1;
}
}
}
// Additional confidence checks
confidence += await this.getAdditionalConfidence(projectPath, manager, foundFiles);
return {
manager,
confidence: Math.min(confidence, 1.0),
files: foundFiles,
};
}
private isPrimaryManifest(filename: string, manager: PackageManager): boolean {
const primaryManifests: Record<PackageManager, string[]> = {
[PackageManager.NPM]: ['package.json'],
[PackageManager.YARN]: ['package.json'],
[PackageManager.PIP]: ['requirements.txt', 'setup.py', 'pyproject.toml'],
[PackageManager.CARGO]: ['Cargo.toml'],
[PackageManager.MAVEN]: ['pom.xml'],
[PackageManager.GRADLE]: ['build.gradle', 'build.gradle.kts'],
[PackageManager.GO]: ['go.mod'],
};
return primaryManifests[manager].includes(filename);
}
private isLockFile(filename: string, manager: PackageManager): boolean {
const lockFiles: Record<PackageManager, string[]> = {
[PackageManager.NPM]: ['package-lock.json'],
[PackageManager.YARN]: ['yarn.lock'],
[PackageManager.PIP]: ['poetry.lock', 'Pipfile.lock'],
[PackageManager.CARGO]: ['Cargo.lock'],
[PackageManager.MAVEN]: [],
[PackageManager.GRADLE]: [],
[PackageManager.GO]: ['go.sum'],
};
return lockFiles[manager].includes(filename);
}
private async getAdditionalConfidence(
projectPath: string,
manager: PackageManager,
foundFiles: string[]
): Promise<number> {
let additionalConfidence = 0;
// Check for workspace/monorepo configurations
if (manager === PackageManager.NPM || manager === PackageManager.YARN) {
if (foundFiles.includes('package.json')) {
const packageJsonPath = path.join(projectPath, 'package.json');
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (packageJson.workspaces) {
additionalConfidence += 0.1;
}
} catch (error) {
this.logger.debug(`Failed to parse package.json: ${error}`);
}
}
}
// Check for Go workspaces
if (manager === PackageManager.GO) {
if (foundFiles.includes('go.work')) {
additionalConfidence += 0.2;
}
}
// Check for Maven wrapper
if (manager === PackageManager.MAVEN) {
if (foundFiles.includes('mvnw') || fs.existsSync(path.join(projectPath, '.mvn'))) {
additionalConfidence += 0.1;
}
}
// Check for Gradle wrapper
if (manager === PackageManager.GRADLE) {
if (foundFiles.includes('gradlew') || fs.existsSync(path.join(projectPath, '.gradle'))) {
additionalConfidence += 0.1;
}
}
return additionalConfidence;
}
async getPrimaryPackageManager(projectPath: string): Promise<PackageManager | null> {
const detections = await this.detectPackageManagers(projectPath);
if (detections.length === 0) {
return null;
}
// Return the manager with highest confidence
return detections[0].manager;
}
async isPackageManagerUsed(projectPath: string, manager: PackageManager): Promise<boolean> {
const detections = await this.detectPackageManagers(projectPath);
return detections.some(d => d.manager === manager);
}
async getPackageManagerFiles(projectPath: string, manager: PackageManager): Promise<string[]> {
const detections = await this.detectPackageManagers(projectPath);
const detection = detections.find(d => d.manager === manager);
return detection ? detection.files : [];
}
}