mirror of
https://github.com/sweetwisdom/everything-claude-code-zh.git
synced 2026-03-22 06:20:10 +00:00
feat: cross-platform support with Node.js scripts
- 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
This commit is contained in:
390
scripts/lib/package-manager.js
Normal file
390
scripts/lib/package-manager.js
Normal file
@@ -0,0 +1,390 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
Reference in New Issue
Block a user