source dump of claude code
at main 191 lines 5.8 kB view raw
1import { homedir, platform } from 'os' 2import { join } from 'path' 3import { getFsImplementation } from '../utils/fsOperations.js' 4import type { IdeType } from './ide.js' 5 6const PLUGIN_PREFIX = 'claude-code-jetbrains-plugin' 7 8// Map of IDE names to their directory patterns 9const ideNameToDirMap: { [key: string]: string[] } = { 10 pycharm: ['PyCharm'], 11 intellij: ['IntelliJIdea', 'IdeaIC'], 12 webstorm: ['WebStorm'], 13 phpstorm: ['PhpStorm'], 14 rubymine: ['RubyMine'], 15 clion: ['CLion'], 16 goland: ['GoLand'], 17 rider: ['Rider'], 18 datagrip: ['DataGrip'], 19 appcode: ['AppCode'], 20 dataspell: ['DataSpell'], 21 aqua: ['Aqua'], 22 gateway: ['Gateway'], 23 fleet: ['Fleet'], 24 androidstudio: ['AndroidStudio'], 25} 26 27// Build plugin directory paths 28// https://www.jetbrains.com/help/pycharm/directories-used-by-the-ide-to-store-settings-caches-plugins-and-logs.html#plugins-directory 29function buildCommonPluginDirectoryPaths(ideName: string): string[] { 30 const homeDir = homedir() 31 const directories: string[] = [] 32 const idePatterns = ideNameToDirMap[ideName.toLowerCase()] 33 if (!idePatterns) { 34 return directories 35 } 36 37 const appData = process.env.APPDATA || join(homeDir, 'AppData', 'Roaming') 38 const localAppData = 39 process.env.LOCALAPPDATA || join(homeDir, 'AppData', 'Local') 40 41 switch (platform()) { 42 case 'darwin': 43 directories.push( 44 join(homeDir, 'Library', 'Application Support', 'JetBrains'), 45 join(homeDir, 'Library', 'Application Support'), 46 ) 47 if (ideName.toLowerCase() === 'androidstudio') { 48 directories.push( 49 join(homeDir, 'Library', 'Application Support', 'Google'), 50 ) 51 } 52 break 53 54 case 'win32': 55 directories.push( 56 join(appData, 'JetBrains'), 57 join(localAppData, 'JetBrains'), 58 join(appData), 59 ) 60 if (ideName.toLowerCase() === 'androidstudio') { 61 directories.push(join(localAppData, 'Google')) 62 } 63 break 64 65 case 'linux': 66 directories.push( 67 join(homeDir, '.config', 'JetBrains'), 68 join(homeDir, '.local', 'share', 'JetBrains'), 69 ) 70 for (const pattern of idePatterns) { 71 directories.push(join(homeDir, '.' + pattern)) 72 } 73 if (ideName.toLowerCase() === 'androidstudio') { 74 directories.push(join(homeDir, '.config', 'Google')) 75 } 76 break 77 default: 78 break 79 } 80 81 return directories 82} 83 84// Find all actual plugin directories that exist 85async function detectPluginDirectories(ideName: string): Promise<string[]> { 86 const foundDirectories: string[] = [] 87 const fs = getFsImplementation() 88 89 const pluginDirPaths = buildCommonPluginDirectoryPaths(ideName) 90 const idePatterns = ideNameToDirMap[ideName.toLowerCase()] 91 if (!idePatterns) { 92 return foundDirectories 93 } 94 95 // Precompile once — idePatterns is invariant across baseDirs 96 const regexes = idePatterns.map(p => new RegExp('^' + p)) 97 98 for (const baseDir of pluginDirPaths) { 99 try { 100 const entries = await fs.readdir(baseDir) 101 for (const regex of regexes) { 102 for (const entry of entries) { 103 if (!regex.test(entry.name)) continue 104 // Accept symlinks too — dirent.isDirectory() is false for symlinks, 105 // but GNU stow users symlink their JetBrains config dirs. Downstream 106 // fs.stat() calls will filter out symlinks that don't point to dirs. 107 if (!entry.isDirectory() && !entry.isSymbolicLink()) continue 108 const dir = join(baseDir, entry.name) 109 // Linux is the only OS to not have a plugins directory 110 if (platform() === 'linux') { 111 foundDirectories.push(dir) 112 continue 113 } 114 const pluginDir = join(dir, 'plugins') 115 try { 116 await fs.stat(pluginDir) 117 foundDirectories.push(pluginDir) 118 } catch { 119 // Plugin directory doesn't exist, skip 120 } 121 } 122 } 123 } catch { 124 // Ignore errors from stale IDE directories (ENOENT, EACCES, etc.) 125 continue 126 } 127 } 128 129 return foundDirectories.filter( 130 (dir, index) => foundDirectories.indexOf(dir) === index, 131 ) 132} 133 134export async function isJetBrainsPluginInstalled( 135 ideType: IdeType, 136): Promise<boolean> { 137 const pluginDirs = await detectPluginDirectories(ideType) 138 for (const dir of pluginDirs) { 139 const pluginPath = join(dir, PLUGIN_PREFIX) 140 try { 141 await getFsImplementation().stat(pluginPath) 142 return true 143 } catch { 144 // Plugin not found in this directory, continue 145 } 146 } 147 return false 148} 149 150const pluginInstalledCache = new Map<IdeType, boolean>() 151const pluginInstalledPromiseCache = new Map<IdeType, Promise<boolean>>() 152 153async function isJetBrainsPluginInstalledMemoized( 154 ideType: IdeType, 155 forceRefresh = false, 156): Promise<boolean> { 157 if (!forceRefresh) { 158 const existing = pluginInstalledPromiseCache.get(ideType) 159 if (existing) { 160 return existing 161 } 162 } 163 const promise = isJetBrainsPluginInstalled(ideType).then(result => { 164 pluginInstalledCache.set(ideType, result) 165 return result 166 }) 167 pluginInstalledPromiseCache.set(ideType, promise) 168 return promise 169} 170 171export async function isJetBrainsPluginInstalledCached( 172 ideType: IdeType, 173 forceRefresh = false, 174): Promise<boolean> { 175 if (forceRefresh) { 176 pluginInstalledCache.delete(ideType) 177 pluginInstalledPromiseCache.delete(ideType) 178 } 179 return isJetBrainsPluginInstalledMemoized(ideType, forceRefresh) 180} 181 182/** 183 * Returns the cached result of isJetBrainsPluginInstalled synchronously. 184 * Returns false if the result hasn't been resolved yet. 185 * Use this only in sync contexts (e.g., status notice isActive checks). 186 */ 187export function isJetBrainsPluginInstalledCachedSync( 188 ideType: IdeType, 189): boolean { 190 return pluginInstalledCache.get(ideType) ?? false 191}