-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
fix(windows): add Node.js path detection for claude.cmd validation #1277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 1 commit
7a606c6
05ca891
88b6ec6
0dc572a
d68110e
bb52151
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||
| import path from 'path'; | ||||||||||||||||||||||||||||||||||||||
| import { getAugmentedEnv, getAugmentedEnvAsync } from './env-utils'; | ||||||||||||||||||||||||||||||||||||||
| import { getToolPath, getToolPathAsync } from './cli-tool-manager'; | ||||||||||||||||||||||||||||||||||||||
| import { findNodeJsDirectories } from './platform'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export type ClaudeCliInvocation = { | ||||||||||||||||||||||||||||||||||||||
| command: string; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -25,13 +26,32 @@ function ensureCommandDirInPath(command: string, env: Record<string, string>): R | |||||||||||||||||||||||||||||||||||||
| .map((entry) => path.normalize(entry)) | ||||||||||||||||||||||||||||||||||||||
| .includes(normalizedCommandDir); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (hasCommandDir) { | ||||||||||||||||||||||||||||||||||||||
| // Collect directories to add | ||||||||||||||||||||||||||||||||||||||
| let dirsToAdd = hasCommandDir ? [] : [commandDir]; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // On Windows, if running claude.cmd, also add Node.js directories to PATH | ||||||||||||||||||||||||||||||||||||||
| // This is needed because claude.cmd requires node.exe to execute | ||||||||||||||||||||||||||||||||||||||
| if (process.platform === 'win32' && /\.cmd$/i.test(command)) { | ||||||||||||||||||||||||||||||||||||||
| const nodeDirs = findNodeJsDirectories(); | ||||||||||||||||||||||||||||||||||||||
| // Filter out directories already in PATH | ||||||||||||||||||||||||||||||||||||||
| for (const nodeDir of nodeDirs) { | ||||||||||||||||||||||||||||||||||||||
| const normalizedNodeDir = path.normalize(nodeDir); | ||||||||||||||||||||||||||||||||||||||
| const hasNodeDir = pathEntries | ||||||||||||||||||||||||||||||||||||||
| .map((entry) => path.normalize(entry).toLowerCase()) | ||||||||||||||||||||||||||||||||||||||
| .includes(normalizedNodeDir.toLowerCase()); | ||||||||||||||||||||||||||||||||||||||
| if (!hasNodeDir) { | ||||||||||||||||||||||||||||||||||||||
| dirsToAdd.push(nodeDir); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| for (const nodeDir of nodeDirs) { | |
| const normalizedNodeDir = path.normalize(nodeDir); | |
| const hasNodeDir = pathEntries | |
| .map((entry) => path.normalize(entry).toLowerCase()) | |
| .includes(normalizedNodeDir.toLowerCase()); | |
| if (!hasNodeDir) { | |
| dirsToAdd.push(nodeDir); | |
| } | |
| } | |
| const normalizedPathEntries = new Set( | |
| pathEntries.map((entry) => path.normalize(entry).toLowerCase()) | |
| ); | |
| for (const nodeDir of nodeDirs) { | |
| const normalizedNodeDir = path.normalize(nodeDir).toLowerCase(); | |
| if (!normalizedPathEntries.has(normalizedNodeDir)) { | |
| dirsToAdd.push(nodeDir); | |
| } | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,7 +27,7 @@ import os from 'os'; | |
| import { promisify } from 'util'; | ||
| import { app } from 'electron'; | ||
| import { findExecutable, findExecutableAsync, getAugmentedEnv, getAugmentedEnvAsync, shouldUseShell, existsAsync } from './env-utils'; | ||
| import { isWindows, isMacOS, isUnix, joinPaths, getExecutableExtension } from './platform'; | ||
| import { isWindows, isMacOS, isUnix, joinPaths, getExecutableExtension, findNodeJsDirectories } from './platform'; | ||
| import type { ToolDetectionResult } from '../shared/types'; | ||
| import { findHomebrewPython as findHomebrewPythonUtil } from './utils/homebrew-python'; | ||
|
|
||
|
|
@@ -942,7 +942,19 @@ class CLIToolManager { | |
|
|
||
| const needsShell = shouldUseShell(trimmedCmd); | ||
| const cmdDir = path.dirname(unquotedCmd); | ||
| const env = getAugmentedEnv(cmdDir && cmdDir !== '.' ? [cmdDir] : []); | ||
|
|
||
| // Prepare additional paths for environment augmentation | ||
| let additionalPaths = cmdDir && cmdDir !== '.' ? [cmdDir] : []; | ||
|
|
||
| // On Windows, if validating claude.cmd, also add Node.js directories to PATH | ||
| // This is needed because claude.cmd requires node.exe to execute | ||
| if (isWindows() && /\.cmd$/i.test(unquotedCmd)) { | ||
| const nodeDirs = findNodeJsDirectories(); | ||
| additionalPaths = [...additionalPaths, ...nodeDirs]; | ||
| console.warn('[Claude CLI] Adding Node.js directories to PATH for .cmd validation:', nodeDirs); | ||
| } | ||
|
||
|
|
||
| const env = getAugmentedEnv(additionalPaths); | ||
|
|
||
| let version: string; | ||
|
|
||
|
|
@@ -1081,7 +1093,19 @@ class CLIToolManager { | |
|
|
||
| const needsShell = shouldUseShell(trimmedCmd); | ||
| const cmdDir = path.dirname(unquotedCmd); | ||
| const env = await getAugmentedEnvAsync(cmdDir && cmdDir !== '.' ? [cmdDir] : []); | ||
|
|
||
| // Prepare additional paths for environment augmentation | ||
| let additionalPaths = cmdDir && cmdDir !== '.' ? [cmdDir] : []; | ||
|
|
||
| // On Windows, if validating claude.cmd, also add Node.js directories to PATH | ||
| // This is needed because claude.cmd requires node.exe to execute | ||
| if (isWindows() && /\.cmd$/i.test(unquotedCmd)) { | ||
| const nodeDirs = findNodeJsDirectories(); | ||
| additionalPaths = [...additionalPaths, ...nodeDirs]; | ||
| console.warn('[Claude CLI] Adding Node.js directories to PATH for .cmd validation:', nodeDirs); | ||
| } | ||
|
|
||
| const env = await getAugmentedEnvAsync(additionalPaths); | ||
|
|
||
| let stdout: string; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -19,6 +19,7 @@ import type { ClaudeCodeVersionInfo, ClaudeInstallationList, ClaudeInstallationI | |||||
| import { getToolInfo, configureTools, sortNvmVersionDirs, getClaudeDetectionPaths, type ExecFileAsyncOptionsWithVerbatim } from '../cli-tool-manager'; | ||||||
| import { readSettingsFile, writeSettingsFile } from '../settings-utils'; | ||||||
| import { isSecurePath } from '../utils/windows-paths'; | ||||||
| import { findNodeJsDirectories } from '../platform'; | ||||||
| import semver from 'semver'; | ||||||
|
|
||||||
| const execFileAsync = promisify(execFile); | ||||||
|
|
@@ -45,9 +46,19 @@ async function validateClaudeCliAsync(cliPath: string): Promise<[boolean, string | |||||
|
|
||||||
| // Augment PATH with the CLI directory for proper resolution | ||||||
| const cliDir = path.dirname(cliPath); | ||||||
| let pathEntries = [cliDir]; | ||||||
|
|
||||||
| // On Windows, if validating claude.cmd, also add Node.js directories to PATH | ||||||
| // This is needed because claude.cmd requires node.exe to execute | ||||||
| if (isWindows && /\.cmd$/i.test(cliPath)) { | ||||||
| const nodeDirs = findNodeJsDirectories(); | ||||||
| pathEntries = [...pathEntries, ...nodeDirs]; | ||||||
| console.log('[Claude CLI] Adding Node.js directories to PATH for .cmd validation:', nodeDirs); | ||||||
| } | ||||||
|
|
||||||
| const env = { | ||||||
| ...process.env, | ||||||
| PATH: cliDir ? `${cliDir}${path.delimiter}${process.env.PATH || ''}` : process.env.PATH, | ||||||
| PATH: pathEntries.filter(Boolean).concat(process.env.PATH || '').join(path.delimiter), | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The construction of the
Suggested change
|
||||||
| }; | ||||||
|
|
||||||
| let stdout: string; | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -163,6 +163,75 @@ export function getNpmExecutablePath(): string { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 'npm'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Find Node.js installation directories on Windows | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns array of potential Node.js bin directories where node.exe might be installed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * This is needed because claude.cmd (npm global binary) requires node.exe to be in PATH. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Search priority: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 1. Program Files\nodejs (standard installer location) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 2. %APPDATA%\npm (user-level npm globals - may contain node.exe with nvm-windows) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 3. NVM for Windows installation directories | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 4. Scoop, Chocolatey installations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns Array of existing Node.js bin directories | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function findNodeJsDirectories(): string[] { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isWindows()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const homeDir = os.homedir(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const candidates: string[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Standard Node.js installer location | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| joinPaths('C:\\Program Files', 'nodejs'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| joinPaths('C:\\Program Files (x86)', 'nodejs'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // User-level npm global directory (may contain node.exe with nvm-windows) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| joinPaths(homeDir, 'AppData', 'Roaming', 'npm'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // NVM for Windows default location | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| joinPaths(homeDir, 'AppData', 'Roaming', 'nvm'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| joinPaths('C:\\Program Files', 'nvm'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Scoop installation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| joinPaths(homeDir, 'scoop', 'apps', 'nodejs', 'current'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Chocolatey installation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| joinPaths('C:\\ProgramData', 'chocolatey', 'bin'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| joinPaths('C:\\Program Files', 'nodejs'), | |
| joinPaths('C:\\Program Files (x86)', 'nodejs'), | |
| // User-level npm global directory (may contain node.exe with nvm-windows) | |
| joinPaths(homeDir, 'AppData', 'Roaming', 'npm'), | |
| // NVM for Windows default location | |
| joinPaths(homeDir, 'AppData', 'Roaming', 'nvm'), | |
| joinPaths('C:\\Program Files', 'nvm'), | |
| // Scoop installation | |
| joinPaths(homeDir, 'scoop', 'apps', 'nodejs', 'current'), | |
| // Chocolatey installation | |
| joinPaths('C:\\ProgramData', 'chocolatey', 'bin'), | |
| joinPaths(process.env.ProgramFiles || 'C:\\Program Files', 'nodejs'), | |
| joinPaths(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'nodejs'), | |
| // User-level npm global directory (may contain node.exe with nvm-windows) | |
| joinPaths(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), 'npm'), | |
| // NVM for Windows default location | |
| joinPaths(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), 'nvm'), | |
| joinPaths(process.env.ProgramFiles || 'C:\\Program Files', 'nvm'), | |
| // Scoop installation | |
| joinPaths(homeDir, 'scoop', 'apps', 'nodejs', 'current'), | |
| // Chocolatey installation | |
| joinPaths(process.env.ProgramData || 'C:\\ProgramData', 'chocolatey', 'bin'), |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This custom version sorting logic is also present in sortNvmVersionDirs in cli-tool-manager.ts. This code duplication can lead to inconsistencies. Furthermore, the semver package is already a dependency in this project and is more robust for version comparisons, as it correctly handles a wider range of version formats.
I suggest importing semver and using semver.rcompare to simplify and unify the version sorting. You will need to add import semver from 'semver'; to the top of the file.
.sort((a, b) => semver.rcompare(a.name, b.name))
Uh oh!
There was an error while loading. Please reload this page.