Skip to content

fix: broaden Claude CLI discovery paths#433

Open
WhiskyChoy wants to merge 2 commits intoop7418:mainfrom
WhiskyChoy:codex/fix-claude-cli-discovery
Open

fix: broaden Claude CLI discovery paths#433
WhiskyChoy wants to merge 2 commits intoop7418:mainfrom
WhiskyChoy:codex/fix-claude-cli-discovery

Conversation

@WhiskyChoy
Copy link
Copy Markdown

关联 Issue / Related Issue: closes #432

Summary / 概述

This PR broadens Claude CLI discovery so CodePilot can find installations that come from pnpm-style wrappers and other compatibility locations, instead of only a smaller hard-coded path set.

这个 PR 扩展了 Claude CLI 的查找逻辑,让 CodePilot 能识别 pnpm 风格包装路径和其他兼容安装位置,而不再只依赖较少的一组硬编码路径。

Root cause / 根因

CodePilot previously checked only a subset of native, bun, homebrew, and npm paths. That missed several directories that are now common in real user environments, especially on macOS.

之前 CodePilot 只检查了部分 native、bun、homebrew、npm 路径,因此遗漏了如今用户环境里常见的若干目录,尤其是 macOS。

Changes / 修改内容

  • expand shared PATH helper coverage to include ~/.claude/local, pnpm home directories, Yarn global bins, and Node manager shims

  • add a reusable getClaudeCandidatePathsFor() helper to make path generation easier to test across platforms

  • update Electron-side install detection to use the broader search directories and candidate executable list

  • classify pnpm/yarn/manager shim installs as the npm-family channel for UI reporting consistency

  • add focused unit tests covering macOS and Windows candidate generation

  • 扩展共享 PATH 辅助逻辑,纳入 ~/.claude/local、pnpm 目录、Yarn 全局 bin 与 Node 管理器 shim

  • 新增可复用的 getClaudeCandidatePathsFor(),便于跨平台测试候选路径生成

  • 更新 Electron 侧安装检测逻辑,使用更完整的搜索目录与候选可执行文件集合

  • 将 pnpm/yarn/管理器 shim 安装统一归类到 npm-family,保持 UI 展示一致

  • 增加针对 macOS 和 Windows 的聚焦单元测试

Test plan / 测试情况

  • npm run typecheck
  • npx tsx --test src/__tests__/unit/platform-claude-paths.test.ts
  • manual verification on a real macOS pnpm environment

Notes / 说明

  • I also checked npm run test:unit locally on Windows, but there are existing unrelated ERR_UNSUPPORTED_ESM_URL_SCHEME failures in other test files. This PR does not change that behavior.
  • 我也在 Windows 上额外检查了 npm run test:unit,但当前仓库里还有与本 PR 无关的 ERR_UNSUPPORTED_ESM_URL_SCHEME 旧失败,本 PR 未涉及该问题。

Copilot AI review requested due to automatic review settings April 5, 2026 04:45
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 5, 2026

@WhiskyChoy is attempting to deploy a commit to the op7418's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR expands Claude CLI discovery so CodePilot can locate installations exposed via pnpm-style wrappers, Yarn globals, and common Node manager shim directories, reducing false “not installed” results across platforms.

Changes:

  • Added platform-specific helpers (getExtraPathDirsFor, getClaudeCandidatePathsFor) to generate broader search directories and candidate executable paths.
  • Expanded install-type classification to treat pnpm/yarn/manager shim installs as the npm-family channel.
  • Added focused unit tests for macOS and Windows candidate generation.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
src/lib/platform.ts Broadens candidate directory/path generation and updates path classification for pnpm/yarn/shims.
src/__tests__/unit/platform-claude-paths.test.ts Adds unit tests validating new candidate dir/path generation for darwin/win32.
electron/main.ts Expands Electron-side PATH augmentation and Claude install detection directories/candidates.
Comments suppressed due to low confidence (1)

electron/main.ts:855

  • classifyPath() does not recognize the newly-added compatibility locations (~/.claude/local) or npm-family shims (pnpm/yarn/volta/fnm/asdf). As a result, installs discovered from those directories will be reported as unknown (or miss the intended npm-family classification), which conflicts with the PR goal of consistent UI channel reporting. Consider aligning this logic with src/lib/platform.ts:classifyClaudePath() (ideally by reusing a shared helper).
    function classifyPath(p: string): 'native' | 'homebrew' | 'npm' | 'bun' | 'unknown' {
      const n = p.replace(/\\/g, '/');
      if (n.includes('/.local/bin/') || n.includes('/.claude/bin/')) return 'native';
      if (n.includes('/.bun/bin/') || n.includes('/.bun/install/')) return 'bun';
      if (n.includes('/homebrew/') || n.includes('/Cellar/')) return 'homebrew';
      if (n.includes('/npm')) return 'npm';
      if (n === '/usr/local/bin/claude') {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +79 to +94
pathApi.join(env.APPDATA || pathApi.join(home, 'AppData', 'Roaming'), 'npm'),
pathApi.join(env.LOCALAPPDATA || pathApi.join(home, 'AppData', 'Local'), 'npm'),
pathApi.join(env.APPDATA || pathApi.join(home, 'AppData', 'Roaming'), 'pnpm'),
pathApi.join(env.LOCALAPPDATA || pathApi.join(home, 'AppData', 'Local'), 'pnpm'),
pnpmHome,
pathApi.join(home, '.npm-global', 'bin'),
pathApi.join(home, '.local', 'bin'),
pathApi.join(home, '.claude', 'bin'),
pathApi.join(home, '.claude', 'local'),
pathApi.join(home, '.bun', 'bin'),
pathApi.join(home, '.yarn', 'bin'),
pathApi.join(home, '.config', 'yarn', 'global', 'node_modules', '.bin'),
voltaHome,
pathApi.join(home, '.fnm', 'current', 'bin'),
pathApi.join(home, '.nvm', 'current', 'bin'),
pathApi.join(home, '.asdf', 'shims'),
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getClaudeCandidatePathsFor() reuses getExtraPathDirsFor() (which is intended for PATH expansion), but the Windows extras list currently starts with AppData npm/pnpm locations. Since findClaudeBinary() returns the first candidate that passes --version, this can incorrectly prefer npm/pnpm installs over the documented priority (native > bun/homebrew > npm). Consider building a separate, explicitly prioritized dir list for candidate paths (or reordering extras so native/bun come first).

Suggested change
pathApi.join(env.APPDATA || pathApi.join(home, 'AppData', 'Roaming'), 'npm'),
pathApi.join(env.LOCALAPPDATA || pathApi.join(home, 'AppData', 'Local'), 'npm'),
pathApi.join(env.APPDATA || pathApi.join(home, 'AppData', 'Roaming'), 'pnpm'),
pathApi.join(env.LOCALAPPDATA || pathApi.join(home, 'AppData', 'Local'), 'pnpm'),
pnpmHome,
pathApi.join(home, '.npm-global', 'bin'),
pathApi.join(home, '.local', 'bin'),
pathApi.join(home, '.claude', 'bin'),
pathApi.join(home, '.claude', 'local'),
pathApi.join(home, '.bun', 'bin'),
pathApi.join(home, '.yarn', 'bin'),
pathApi.join(home, '.config', 'yarn', 'global', 'node_modules', '.bin'),
voltaHome,
pathApi.join(home, '.fnm', 'current', 'bin'),
pathApi.join(home, '.nvm', 'current', 'bin'),
pathApi.join(home, '.asdf', 'shims'),
pathApi.join(home, '.claude', 'bin'),
pathApi.join(home, '.claude', 'local'),
pathApi.join(home, '.bun', 'bin'),
pathApi.join(home, '.local', 'bin'),
pathApi.join(home, '.yarn', 'bin'),
pathApi.join(home, '.config', 'yarn', 'global', 'node_modules', '.bin'),
voltaHome,
pathApi.join(home, '.fnm', 'current', 'bin'),
pathApi.join(home, '.nvm', 'current', 'bin'),
pathApi.join(home, '.asdf', 'shims'),
pathApi.join(env.APPDATA || pathApi.join(home, 'AppData', 'Roaming'), 'npm'),
pathApi.join(env.LOCALAPPDATA || pathApi.join(home, 'AppData', 'Local'), 'npm'),
pathApi.join(env.APPDATA || pathApi.join(home, 'AppData', 'Roaming'), 'pnpm'),
pathApi.join(env.LOCALAPPDATA || pathApi.join(home, 'AppData', 'Local'), 'pnpm'),
pnpmHome,
pathApi.join(home, '.npm-global', 'bin'),

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I split PATH expansion from Claude candidate ordering, so Windows candidate generation now uses an explicit priority list and keeps native/compatibility wrappers ahead of npm-family shims. Fixed in 1dfc6e7.

Comment on lines +100 to +115
pathApi.join(home, '.bun', 'bin'),
pathApi.join(home, '.npm-global', 'bin'),
pathApi.join(home, '.yarn', 'bin'),
pathApi.join(home, '.config', 'yarn', 'global', 'node_modules', '.bin'),
pathApi.join(home, '.volta', 'bin'),
pathApi.join(home, '.fnm', 'current', 'bin'),
pathApi.join(home, '.nvm', 'current', 'bin'),
pathApi.join(home, '.asdf', 'shims'),
pnpmHome,
pathApi.join(home, '.local', 'share', 'pnpm'),
...(platform === 'darwin' ? [pathApi.join(home, 'Library', 'pnpm')] : []),
'/usr/local/bin',
'/opt/homebrew/bin',
'/usr/bin',
'/bin',
];
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On non-Windows platforms, the candidate dir list used by getClaudeCandidatePathsFor() currently places npm-family locations (e.g. ~/.npm-global/bin, yarn/pnpm) before Homebrew paths (/opt/homebrew/bin, /usr/local/bin). Because findClaudeBinary() selects the first working candidate, this can flip the intended priority and pick an npm shim even when a Homebrew install is present. Reorder the candidate generation to keep native/bun/homebrew ahead of npm-family locations.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1dfc6e7. Non-Windows candidate generation now keeps native, bun, and Homebrew locations ahead of npm-family bins, and I added ordering assertions so we do not regress on this priority again.

electron/main.ts Outdated
Comment on lines +489 to +490
const pnpmHome = process.env.PNPM_HOME;
const voltaHome = process.env.VOLTA_HOME ? path.join(process.env.VOLTA_HOME, 'bin') : '';
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getExpandedShellPath() reads PNPM_HOME/VOLTA_HOME from process.env, but when Electron is launched from Dock/Finder process.env is often missing shell-initialized vars and userShellEnv is the authoritative source. This can prevent pnpm/volta paths from being added even though they exist in the resolved shell env. Consider deriving these from userShellEnv (or the merged execEnv) instead of process.env here.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Electron now derives PNPM_HOME, VOLTA_HOME, and related path expansion from the merged shell env (process.env + userShellEnv), so Dock/Finder launches can still pick up shell-initialized values. Fixed in 1dfc6e7.

Comment on lines +822 to 838
path.join(home, '.local', 'bin'),
path.join(home, '.claude', 'bin'),
path.join(home, '.claude', 'local'),
path.join(home, '.bun', 'bin'),
path.join(home, '.npm-global', 'bin'),
path.join(home, '.yarn', 'bin'),
path.join(home, '.config', 'yarn', 'global', 'node_modules', '.bin'),
path.join(home, '.volta', 'bin'),
path.join(home, '.fnm', 'current', 'bin'),
path.join(home, '.nvm', 'current', 'bin'),
path.join(home, '.asdf', 'shims'),
process.env.PNPM_HOME || '',
path.join(home, '.local', 'share', 'pnpm'),
...(process.platform === 'darwin' ? [path.join(home, 'Library', 'pnpm')] : []),
'/opt/homebrew/bin',
'/usr/local/bin',
];
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The claudeSearchDirs ordering for non-Windows currently places npm-family bins (e.g. ~/.npm-global/bin, yarn/pnpm) before Homebrew directories (/opt/homebrew/bin, /usr/local/bin). Since primary is derived from the first successful candidatePaths probe, this can prefer an npm shim even when a Homebrew install exists, contradicting the comment about priority. Reorder claudeSearchDirs so homebrew directories are checked before npm-family bins.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1dfc6e7. The Electron-side detection list now uses the same priority shape as the shared helper: native/compat wrappers first, then bun/Homebrew, then npm-family bins.

Comment on lines +6 to +16
describe('Claude path discovery helpers', () => {
it('darwin search dirs include pnpm, yarn, and claude local compatibility paths', () => {
const home = '/Users/tester';
const dirs = getExtraPathDirsFor('darwin', home, {
PNPM_HOME: '/Users/tester/Library/pnpm',
} as unknown as NodeJS.ProcessEnv);

assert.ok(dirs.includes('/Users/tester/Library/pnpm'));
assert.ok(dirs.includes('/Users/tester/.claude/local'));
assert.ok(dirs.includes('/Users/tester/.yarn/bin'));
});
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tests verify presence of pnpm/yarn/compatibility paths, but they don't cover the priority/ordering semantics that findClaudeBinary() depends on (first successful candidate wins). Adding an assertion about the relative ordering (e.g. native dirs/candidates appearing before npm-family ones) would help prevent regressions where discovery starts preferring the wrong installation channel.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I added explicit ordering assertions in the path helper tests so they now cover the first-match priority that findClaudeBinary() depends on. Included in 1dfc6e7.

@WhiskyChoy
Copy link
Copy Markdown
Author

Follow-up note: I also addressed Copilot's suppressed classifyPath suggestion while touching the same area. Electron-side classification now recognizes \.claude/local\ and the newly added npm-family shim locations, so install-channel reporting stays aligned with the shared platform helper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: broaden Claude CLI discovery paths for pnpm and compatibility wrappers

2 participants