fix: eliminate child process spawns during session startup (#162)

getAvailablePackageManagers() spawned where.exe/which for each package
manager (npm, pnpm, yarn, bun). During SessionStart hooks, these 4+
child processes combined with Bun's own initialization exceeded the
spawn limit on Windows, freezing the terminal.

Fix: Remove process spawning from the hot path. Steps 1-5 of detection
(env var, project config, package.json, lock file, global config) already
cover all file-based detection. If none match, default to npm without
spawning. Also fix getSelectionPrompt() to list supported PMs without
checking availability.
This commit is contained in:
Affaan Mustafa
2026-02-12 00:01:23 -08:00
parent 0f1597dccf
commit 75ab8e6194
2 changed files with 21 additions and 26 deletions

View File

@@ -66,8 +66,8 @@ async function main() {
const pm = getPackageManager(); const pm = getPackageManager();
log(`[SessionStart] Package manager: ${pm.name} (${pm.source})`); log(`[SessionStart] Package manager: ${pm.name} (${pm.source})`);
// If package manager was detected via fallback, show selection prompt // If no explicit package manager config was found, show selection prompt
if (pm.source === 'fallback' || pm.source === 'default') { if (pm.source === 'default') {
log('[SessionStart] No package manager preference found.'); log('[SessionStart] No package manager preference found.');
log(getSelectionPrompt()); log(getSelectionPrompt());
} }

View File

@@ -127,6 +127,11 @@ function detectFromPackageJson(projectDir = process.cwd()) {
/** /**
* Get available package managers (installed on system) * Get available package managers (installed on system)
*
* WARNING: This spawns child processes (where.exe on Windows, which on Unix)
* for each package manager. Do NOT call this during session startup hooks —
* it can exceed Bun's spawn limit on Windows and freeze the plugin.
* Use detectFromLockFile() or detectFromPackageJson() for hot paths.
*/ */
function getAvailablePackageManagers() { function getAvailablePackageManagers() {
const available = []; const available = [];
@@ -149,7 +154,7 @@ function getAvailablePackageManagers() {
* 3. package.json packageManager field * 3. package.json packageManager field
* 4. Lock file detection * 4. Lock file detection
* 5. Global user preference (in ~/.claude/package-manager.json) * 5. Global user preference (in ~/.claude/package-manager.json)
* 6. First available package manager (by priority) * 6. Default to npm (no child processes spawned)
* *
* @param {object} options - { projectDir, fallbackOrder } * @param {object} options - { projectDir, fallbackOrder }
* @returns {object} - { name, config, source } * @returns {object} - { name, config, source }
@@ -215,19 +220,13 @@ function getPackageManager(options = {}) {
}; };
} }
// 6. Use first available package manager // 6. Default to npm (always available with Node.js)
const available = getAvailablePackageManagers(); // NOTE: Previously this called getAvailablePackageManagers() which spawns
for (const pmName of fallbackOrder) { // child processes (where.exe/which) for each PM. This caused plugin freezes
if (available.includes(pmName)) { // on Windows (see #162) because session-start hooks run during Bun init,
return { // and the spawned processes exceed Bun's spawn limit.
name: pmName, // Steps 1-5 already cover all config-based and file-based detection.
config: PACKAGE_MANAGERS[pmName], // If none matched, npm is the safe default.
source: 'fallback'
};
}
}
// Default to npm (always available with Node.js)
return { return {
name: 'npm', name: 'npm',
config: PACKAGE_MANAGERS.npm, config: PACKAGE_MANAGERS.npm,
@@ -306,22 +305,18 @@ function getExecCommand(binary, args = '', options = {}) {
/** /**
* Interactive prompt for package manager selection * Interactive prompt for package manager selection
* Returns a message for Claude to show to user * Returns a message for Claude to show to user
*
* NOTE: Does NOT spawn child processes to check availability.
* Lists all supported PMs and shows how to configure preference.
*/ */
function getSelectionPrompt() { function getSelectionPrompt() {
const available = getAvailablePackageManagers(); let message = '[PackageManager] No package manager preference detected.\n';
const current = getPackageManager(); message += 'Supported package managers: ' + Object.keys(PACKAGE_MANAGERS).join(', ') + '\n';
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 += '\nTo set your preferred package manager:\n';
message += ' - Global: Set CLAUDE_PACKAGE_MANAGER environment variable\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 ~/.claude/package-manager.json: {"packageManager": "pnpm"}\n';
message += ' - Or add to package.json: {"packageManager": "pnpm@8"}\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; return message;
} }