/** * Package Manager Detection and Selection * Automatically detects the preferred package manager or lets user choose * * Supports: npm, pnpm, yarn, bun */ const fs = require('fs'); const path = require('path'); const { commandExists, getClaudeDir, readFile, writeFile, log, runCommand } = require('./utils'); // Package manager definitions const PACKAGE_MANAGERS = { npm: { name: 'npm', lockFile: 'package-lock.json', installCmd: 'npm install', runCmd: 'npm run', execCmd: 'npx', testCmd: 'npm test', buildCmd: 'npm run build', devCmd: 'npm run dev' }, pnpm: { name: 'pnpm', lockFile: 'pnpm-lock.yaml', installCmd: 'pnpm install', runCmd: 'pnpm', execCmd: 'pnpm dlx', testCmd: 'pnpm test', buildCmd: 'pnpm build', devCmd: 'pnpm dev' }, yarn: { name: 'yarn', lockFile: 'yarn.lock', installCmd: 'yarn', runCmd: 'yarn', execCmd: 'yarn dlx', testCmd: 'yarn test', buildCmd: 'yarn build', devCmd: 'yarn dev' }, bun: { name: 'bun', lockFile: 'bun.lockb', installCmd: 'bun install', runCmd: 'bun run', execCmd: 'bunx', testCmd: 'bun test', buildCmd: 'bun run build', devCmd: 'bun run dev' } }; // Priority order for detection const DETECTION_PRIORITY = ['pnpm', 'bun', 'yarn', 'npm']; // Config file path function getConfigPath() { return path.join(getClaudeDir(), 'package-manager.json'); } /** * Load saved package manager configuration */ function loadConfig() { const configPath = getConfigPath(); const content = readFile(configPath); if (content) { try { return JSON.parse(content); } catch { return null; } } return null; } /** * Save package manager configuration */ function saveConfig(config) { const configPath = getConfigPath(); writeFile(configPath, JSON.stringify(config, null, 2)); } /** * Detect package manager from lock file in project directory */ function detectFromLockFile(projectDir = process.cwd()) { for (const pmName of DETECTION_PRIORITY) { const pm = PACKAGE_MANAGERS[pmName]; const lockFilePath = path.join(projectDir, pm.lockFile); if (fs.existsSync(lockFilePath)) { return pmName; } } return null; } /** * Detect package manager from package.json packageManager field */ function detectFromPackageJson(projectDir = process.cwd()) { const packageJsonPath = path.join(projectDir, 'package.json'); const content = readFile(packageJsonPath); if (content) { try { const pkg = JSON.parse(content); if (pkg.packageManager) { // Format: "pnpm@8.6.0" or just "pnpm" const pmName = pkg.packageManager.split('@')[0]; if (PACKAGE_MANAGERS[pmName]) { return pmName; } } } catch { // Invalid package.json } } return null; } /** * Get available package managers (installed on system) */ function getAvailablePackageManagers() { const available = []; for (const pmName of Object.keys(PACKAGE_MANAGERS)) { if (commandExists(pmName)) { available.push(pmName); } } return available; } /** * Get the package manager to use for current project * * Detection priority: * 1. Environment variable CLAUDE_PACKAGE_MANAGER * 2. Project-specific config (in .claude/package-manager.json) * 3. package.json packageManager field * 4. Lock file detection * 5. Global user preference (in ~/.claude/package-manager.json) * 6. First available package manager (by priority) * * @param {object} options - { projectDir, fallbackOrder } * @returns {object} - { name, config, source } */ function getPackageManager(options = {}) { const { projectDir = process.cwd(), fallbackOrder = DETECTION_PRIORITY } = options; // 1. Check environment variable const envPm = process.env.CLAUDE_PACKAGE_MANAGER; if (envPm && PACKAGE_MANAGERS[envPm]) { return { name: envPm, config: PACKAGE_MANAGERS[envPm], source: 'environment' }; } // 2. Check project-specific config const projectConfigPath = path.join(projectDir, '.claude', 'package-manager.json'); const projectConfig = readFile(projectConfigPath); if (projectConfig) { try { const config = JSON.parse(projectConfig); if (config.packageManager && PACKAGE_MANAGERS[config.packageManager]) { return { name: config.packageManager, config: PACKAGE_MANAGERS[config.packageManager], source: 'project-config' }; } } catch { // Invalid config } } // 3. Check package.json packageManager field const fromPackageJson = detectFromPackageJson(projectDir); if (fromPackageJson) { return { name: fromPackageJson, config: PACKAGE_MANAGERS[fromPackageJson], source: 'package.json' }; } // 4. Check lock file const fromLockFile = detectFromLockFile(projectDir); if (fromLockFile) { return { name: fromLockFile, config: PACKAGE_MANAGERS[fromLockFile], source: 'lock-file' }; } // 5. Check global user preference const globalConfig = loadConfig(); if (globalConfig && globalConfig.packageManager && PACKAGE_MANAGERS[globalConfig.packageManager]) { return { name: globalConfig.packageManager, config: PACKAGE_MANAGERS[globalConfig.packageManager], source: 'global-config' }; } // 6. Use first available package manager const available = getAvailablePackageManagers(); for (const pmName of fallbackOrder) { if (available.includes(pmName)) { return { name: pmName, config: PACKAGE_MANAGERS[pmName], source: 'fallback' }; } } // Default to npm (always available with Node.js) return { name: 'npm', config: PACKAGE_MANAGERS.npm, source: 'default' }; } /** * Set user's preferred package manager (global) */ function setPreferredPackageManager(pmName) { if (!PACKAGE_MANAGERS[pmName]) { throw new Error(`Unknown package manager: ${pmName}`); } const config = loadConfig() || {}; config.packageManager = pmName; config.setAt = new Date().toISOString(); saveConfig(config); return config; } /** * Set project's preferred package manager */ function setProjectPackageManager(pmName, projectDir = process.cwd()) { if (!PACKAGE_MANAGERS[pmName]) { throw new Error(`Unknown package manager: ${pmName}`); } const configDir = path.join(projectDir, '.claude'); const configPath = path.join(configDir, 'package-manager.json'); const config = { packageManager: pmName, setAt: new Date().toISOString() }; writeFile(configPath, JSON.stringify(config, null, 2)); return config; } /** * Get the command to run a script * @param {string} script - Script name (e.g., "dev", "build", "test") * @param {object} options - { projectDir } */ function getRunCommand(script, options = {}) { const pm = getPackageManager(options); switch (script) { case 'install': return pm.config.installCmd; case 'test': return pm.config.testCmd; case 'build': return pm.config.buildCmd; case 'dev': return pm.config.devCmd; default: return `${pm.config.runCmd} ${script}`; } } /** * Get the command to execute a package binary * @param {string} binary - Binary name (e.g., "prettier", "eslint") * @param {string} args - Arguments to pass */ function getExecCommand(binary, args = '', options = {}) { const pm = getPackageManager(options); return `${pm.config.execCmd} ${binary}${args ? ' ' + args : ''}`; } /** * Interactive prompt for package manager selection * Returns a message for Claude to show to user */ function getSelectionPrompt() { const available = getAvailablePackageManagers(); const current = getPackageManager(); let message = '[PackageManager] Available package managers:\n'; for (const pmName of available) { const indicator = pmName === current.name ? ' (current)' : ''; message += ` - ${pmName}${indicator}\n`; } message += '\nTo set your preferred package manager:\n'; message += ' - Global: Set CLAUDE_PACKAGE_MANAGER environment variable\n'; message += ' - Or add to ~/.claude/package-manager.json: {"packageManager": "pnpm"}\n'; message += ' - Or add to package.json: {"packageManager": "pnpm@8"}\n'; return message; } /** * Generate a regex pattern that matches commands for all package managers * @param {string} action - Action pattern (e.g., "run dev", "install", "test") */ function getCommandPattern(action) { const patterns = []; if (action === 'dev') { patterns.push( 'npm run dev', 'pnpm( run)? dev', 'yarn dev', 'bun run dev' ); } else if (action === 'install') { patterns.push( 'npm install', 'pnpm install', 'yarn( install)?', 'bun install' ); } else if (action === 'test') { patterns.push( 'npm test', 'pnpm test', 'yarn test', 'bun test' ); } else if (action === 'build') { patterns.push( 'npm run build', 'pnpm( run)? build', 'yarn build', 'bun run build' ); } else { // Generic run command patterns.push( `npm run ${action}`, `pnpm( run)? ${action}`, `yarn ${action}`, `bun run ${action}` ); } return `(${patterns.join('|')})`; } module.exports = { PACKAGE_MANAGERS, DETECTION_PRIORITY, getPackageManager, setPreferredPackageManager, setProjectPackageManager, getAvailablePackageManagers, detectFromLockFile, detectFromPackageJson, getRunCommand, getExecCommand, getSelectionPrompt, getCommandPattern };