| |
| |
| |
| |
| |
| |
|
|
| const fs = require('fs'); |
| const path = require('path'); |
| const { commandExists, getClaudeDir, readFile, writeFile } = require('./utils'); |
|
|
| |
| 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' |
| } |
| }; |
|
|
| |
| const DETECTION_PRIORITY = ['pnpm', 'bun', 'yarn', 'npm']; |
|
|
| |
| function getConfigPath() { |
| return path.join(getClaudeDir(), 'package-manager.json'); |
| } |
|
|
| |
| |
| |
| function loadConfig() { |
| const configPath = getConfigPath(); |
| const content = readFile(configPath); |
|
|
| if (content) { |
| try { |
| return JSON.parse(content); |
| } catch { |
| return null; |
| } |
| } |
| return null; |
| } |
|
|
| |
| |
| |
| function saveConfig(config) { |
| const configPath = getConfigPath(); |
| writeFile(configPath, JSON.stringify(config, null, 2)); |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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) { |
| |
| const pmName = pkg.packageManager.split('@')[0]; |
| if (PACKAGE_MANAGERS[pmName]) { |
| return pmName; |
| } |
| } |
| } catch { |
| |
| } |
| } |
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function getAvailablePackageManagers() { |
| const available = []; |
|
|
| for (const pmName of Object.keys(PACKAGE_MANAGERS)) { |
| if (commandExists(pmName)) { |
| available.push(pmName); |
| } |
| } |
|
|
| return available; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function getPackageManager(options = {}) { |
| const { projectDir = process.cwd() } = options; |
|
|
| |
| const envPm = process.env.CLAUDE_PACKAGE_MANAGER; |
| if (envPm && PACKAGE_MANAGERS[envPm]) { |
| return { |
| name: envPm, |
| config: PACKAGE_MANAGERS[envPm], |
| source: 'environment' |
| }; |
| } |
|
|
| |
| 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 { |
| |
| } |
| } |
|
|
| |
| const fromPackageJson = detectFromPackageJson(projectDir); |
| if (fromPackageJson) { |
| return { |
| name: fromPackageJson, |
| config: PACKAGE_MANAGERS[fromPackageJson], |
| source: 'package.json' |
| }; |
| } |
|
|
| |
| const fromLockFile = detectFromLockFile(projectDir); |
| if (fromLockFile) { |
| return { |
| name: fromLockFile, |
| config: PACKAGE_MANAGERS[fromLockFile], |
| source: 'lock-file' |
| }; |
| } |
|
|
| |
| const globalConfig = loadConfig(); |
| if (globalConfig && globalConfig.packageManager && PACKAGE_MANAGERS[globalConfig.packageManager]) { |
| return { |
| name: globalConfig.packageManager, |
| config: PACKAGE_MANAGERS[globalConfig.packageManager], |
| source: 'global-config' |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| return { |
| name: 'npm', |
| config: PACKAGE_MANAGERS.npm, |
| source: 'default' |
| }; |
| } |
|
|
| |
| |
| |
| 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(); |
|
|
| try { |
| saveConfig(config); |
| } catch (err) { |
| throw new Error(`Failed to save package manager preference: ${err.message}`); |
| } |
|
|
| return config; |
| } |
|
|
| |
| |
| |
| 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() |
| }; |
|
|
| try { |
| writeFile(configPath, JSON.stringify(config, null, 2)); |
| } catch (err) { |
| throw new Error(`Failed to save package manager config to ${configPath}: ${err.message}`); |
| } |
| return config; |
| } |
|
|
| |
| |
| const SAFE_NAME_REGEX = /^[@a-zA-Z0-9_./-]+$/; |
|
|
| |
| |
| |
| |
| |
| |
| function getRunCommand(script, options = {}) { |
| if (!script || typeof script !== 'string') { |
| throw new Error('Script name must be a non-empty string'); |
| } |
| if (!SAFE_NAME_REGEX.test(script)) { |
| throw new Error(`Script name contains unsafe characters: ${script}`); |
| } |
|
|
| 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}`; |
| } |
| } |
|
|
| |
| |
| const SAFE_ARGS_REGEX = /^[@a-zA-Z0-9\s_./:=,'"*+-]+$/; |
|
|
| |
| |
| |
| |
| |
| |
| function getExecCommand(binary, args = '', options = {}) { |
| if (!binary || typeof binary !== 'string') { |
| throw new Error('Binary name must be a non-empty string'); |
| } |
| if (!SAFE_NAME_REGEX.test(binary)) { |
| throw new Error(`Binary name contains unsafe characters: ${binary}`); |
| } |
| if (args && typeof args === 'string' && !SAFE_ARGS_REGEX.test(args)) { |
| throw new Error(`Arguments contain unsafe characters: ${args}`); |
| } |
|
|
| const pm = getPackageManager(options); |
| return `${pm.config.execCmd} ${binary}${args ? ' ' + args : ''}`; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function getSelectionPrompt() { |
| let message = '[PackageManager] No package manager preference detected.\n'; |
| message += 'Supported package managers: ' + Object.keys(PACKAGE_MANAGERS).join(', ') + '\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'; |
| message += ' - Or add a lock file to your project (e.g., pnpm-lock.yaml)\n'; |
|
|
| return message; |
| } |
|
|
| |
| function escapeRegex(str) { |
| return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| } |
|
|
| |
| |
| |
| |
| function getCommandPattern(action) { |
| const patterns = []; |
|
|
| |
| const trimmedAction = action.trim(); |
|
|
| if (trimmedAction === 'dev') { |
| patterns.push( |
| 'npm run dev', |
| 'pnpm( run)? dev', |
| 'yarn dev', |
| 'bun run dev' |
| ); |
| } else if (trimmedAction === 'install') { |
| patterns.push( |
| 'npm install', |
| 'pnpm install', |
| 'yarn( install)?', |
| 'bun install' |
| ); |
| } else if (trimmedAction === 'test') { |
| patterns.push( |
| 'npm test', |
| 'pnpm test', |
| 'yarn test', |
| 'bun test' |
| ); |
| } else if (trimmedAction === 'build') { |
| patterns.push( |
| 'npm run build', |
| 'pnpm( run)? build', |
| 'yarn build', |
| 'bun run build' |
| ); |
| } else { |
| |
| const escaped = escapeRegex(trimmedAction); |
| patterns.push( |
| `npm run ${escaped}`, |
| `pnpm( run)? ${escaped}`, |
| `yarn ${escaped}`, |
| `bun run ${escaped}` |
| ); |
| } |
|
|
| return `(${patterns.join('|')})`; |
| } |
|
|
| module.exports = { |
| PACKAGE_MANAGERS, |
| DETECTION_PRIORITY, |
| getPackageManager, |
| setPreferredPackageManager, |
| setProjectPackageManager, |
| getAvailablePackageManagers, |
| detectFromLockFile, |
| detectFromPackageJson, |
| getRunCommand, |
| getExecCommand, |
| getSelectionPrompt, |
| getCommandPattern |
| }; |
|
|