mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-02-01 04:03:08 +08:00
- Rewrite all bash hooks to Node.js for Windows/macOS/Linux compatibility - Add package manager auto-detection (npm, pnpm, yarn, bun) - Add scripts/lib/ with cross-platform utilities - Add /setup-pm command for package manager configuration - Add comprehensive test suite (62 tests) Co-authored-by: zerx-lab
391 lines
9.4 KiB
JavaScript
391 lines
9.4 KiB
JavaScript
/**
|
|
* 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
|
|
};
|