( includeInvalid: boolean, )
| 689 | * the workspace directory does not match the cwd) |
| 690 | */ |
| 691 | export async function detectIDEs( |
| 692 | includeInvalid: boolean, |
| 693 | ): Promise<DetectedIDEInfo[]> { |
| 694 | const detectedIDEs: DetectedIDEInfo[] = [] |
| 695 | const singleLiveWorkspaceMismatchFallbacks: DetectedIDEInfo[] = [] |
| 696 | |
| 697 | try { |
| 698 | // Get the CLAUDE_CODE_SSE_PORT if set |
| 699 | const ssePort = process.env.CLAUDE_CODE_SSE_PORT |
| 700 | const envPort = ssePort ? parseInt(ssePort) : null |
| 701 | |
| 702 | // Get the current working directory, normalized to NFC for consistent |
| 703 | // comparison. macOS returns NFD paths (decomposed Unicode), while IDEs |
| 704 | // like VS Code report NFC paths (composed Unicode). Without normalization, |
| 705 | // paths containing accented/CJK characters fail to match. |
| 706 | const cwd = getOriginalCwd().normalize('NFC') |
| 707 | |
| 708 | // Get sorted lockfiles (full paths) and read them all in parallel. |
| 709 | // findAvailableIDE() polls this every 1s for up to 30s; serial I/O here was |
| 710 | // showing up as ~500ms self-time in CPU profiles. |
| 711 | const lockfiles = await getSortedIdeLockfiles() |
| 712 | const lockfileInfos = await Promise.all(lockfiles.map(readIdeLockfile)) |
| 713 | |
| 714 | // Ancestor PID walk shells out (ps in a loop, up to 10x). Make it lazy and |
| 715 | // single-shot per detectIDEs() call; with the workspace-check-first ordering |
| 716 | // below, this often never fires at all. |
| 717 | const getAncestors = makeAncestorPidLookup() |
| 718 | const needsAncestryCheck = getPlatform() !== 'wsl' && isSupportedTerminal() |
| 719 | |
| 720 | // Try to find a lockfile that contains our current working directory |
| 721 | for (const lockfileInfo of lockfileInfos) { |
| 722 | if (!lockfileInfo) continue |
| 723 | |
| 724 | let isValid = false |
| 725 | const skipValidityCheck = isEnvTruthy( |
| 726 | process.env.CLAUDE_CODE_IDE_SKIP_VALID_CHECK, |
| 727 | ) |
| 728 | if (isEnvTruthy(process.env.CLAUDE_CODE_IDE_SKIP_VALID_CHECK)) { |
| 729 | isValid = true |
| 730 | } else if (lockfileInfo.port === envPort) { |
| 731 | // If the port matches the environment variable, mark as valid regardless of directory |
| 732 | isValid = true |
| 733 | } else { |
| 734 | // Otherwise, check if the current working directory is within the workspace folders |
| 735 | isValid = lockfileInfo.workspaceFolders.some(idePath => { |
| 736 | if (!idePath) return false |
| 737 | |
| 738 | let localPath = idePath |
| 739 | |
| 740 | // Handle WSL-specific path conversion and distro matching |
| 741 | if ( |
| 742 | getPlatform() === 'wsl' && |
| 743 | lockfileInfo.runningInWindows && |
| 744 | process.env.WSL_DISTRO_NAME |
| 745 | ) { |
| 746 | // Check for WSL distro mismatch |
| 747 | if (!checkWSLDistroMatch(idePath, process.env.WSL_DISTRO_NAME)) { |
| 748 | return false |
no test coverage detected