| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- import { homedir, platform } from 'os'
- import { join } from 'path'
- import { getFsImplementation } from '../utils/fsOperations.js'
- import type { IdeType } from './ide.js'
- const PLUGIN_PREFIX = 'claude-code-jetbrains-plugin'
- // Map of IDE names to their directory patterns
- const ideNameToDirMap: { [key: string]: string[] } = {
- pycharm: ['PyCharm'],
- intellij: ['IntelliJIdea', 'IdeaIC'],
- webstorm: ['WebStorm'],
- phpstorm: ['PhpStorm'],
- rubymine: ['RubyMine'],
- clion: ['CLion'],
- goland: ['GoLand'],
- rider: ['Rider'],
- datagrip: ['DataGrip'],
- appcode: ['AppCode'],
- dataspell: ['DataSpell'],
- aqua: ['Aqua'],
- gateway: ['Gateway'],
- fleet: ['Fleet'],
- androidstudio: ['AndroidStudio'],
- }
- // Build plugin directory paths
- // https://www.jetbrains.com/help/pycharm/directories-used-by-the-ide-to-store-settings-caches-plugins-and-logs.html#plugins-directory
- function buildCommonPluginDirectoryPaths(ideName: string): string[] {
- const homeDir = homedir()
- const directories: string[] = []
- const idePatterns = ideNameToDirMap[ideName.toLowerCase()]
- if (!idePatterns) {
- return directories
- }
- const appData = process.env.APPDATA || join(homeDir, 'AppData', 'Roaming')
- const localAppData =
- process.env.LOCALAPPDATA || join(homeDir, 'AppData', 'Local')
- switch (platform()) {
- case 'darwin':
- directories.push(
- join(homeDir, 'Library', 'Application Support', 'JetBrains'),
- join(homeDir, 'Library', 'Application Support'),
- )
- if (ideName.toLowerCase() === 'androidstudio') {
- directories.push(
- join(homeDir, 'Library', 'Application Support', 'Google'),
- )
- }
- break
- case 'win32':
- directories.push(
- join(appData, 'JetBrains'),
- join(localAppData, 'JetBrains'),
- join(appData),
- )
- if (ideName.toLowerCase() === 'androidstudio') {
- directories.push(join(localAppData, 'Google'))
- }
- break
- case 'linux':
- directories.push(
- join(homeDir, '.config', 'JetBrains'),
- join(homeDir, '.local', 'share', 'JetBrains'),
- )
- for (const pattern of idePatterns) {
- directories.push(join(homeDir, '.' + pattern))
- }
- if (ideName.toLowerCase() === 'androidstudio') {
- directories.push(join(homeDir, '.config', 'Google'))
- }
- break
- default:
- break
- }
- return directories
- }
- // Find all actual plugin directories that exist
- async function detectPluginDirectories(ideName: string): Promise<string[]> {
- const foundDirectories: string[] = []
- const fs = getFsImplementation()
- const pluginDirPaths = buildCommonPluginDirectoryPaths(ideName)
- const idePatterns = ideNameToDirMap[ideName.toLowerCase()]
- if (!idePatterns) {
- return foundDirectories
- }
- // Precompile once — idePatterns is invariant across baseDirs
- const regexes = idePatterns.map(p => new RegExp('^' + p))
- for (const baseDir of pluginDirPaths) {
- try {
- const entries = await fs.readdir(baseDir)
- for (const regex of regexes) {
- for (const entry of entries) {
- if (!regex.test(entry.name)) continue
- // Accept symlinks too — dirent.isDirectory() is false for symlinks,
- // but GNU stow users symlink their JetBrains config dirs. Downstream
- // fs.stat() calls will filter out symlinks that don't point to dirs.
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
- const dir = join(baseDir, entry.name)
- // Linux is the only OS to not have a plugins directory
- if (platform() === 'linux') {
- foundDirectories.push(dir)
- continue
- }
- const pluginDir = join(dir, 'plugins')
- try {
- await fs.stat(pluginDir)
- foundDirectories.push(pluginDir)
- } catch {
- // Plugin directory doesn't exist, skip
- }
- }
- }
- } catch {
- // Ignore errors from stale IDE directories (ENOENT, EACCES, etc.)
- continue
- }
- }
- return foundDirectories.filter(
- (dir, index) => foundDirectories.indexOf(dir) === index,
- )
- }
- export async function isJetBrainsPluginInstalled(
- ideType: IdeType,
- ): Promise<boolean> {
- const pluginDirs = await detectPluginDirectories(ideType)
- for (const dir of pluginDirs) {
- const pluginPath = join(dir, PLUGIN_PREFIX)
- try {
- await getFsImplementation().stat(pluginPath)
- return true
- } catch {
- // Plugin not found in this directory, continue
- }
- }
- return false
- }
- const pluginInstalledCache = new Map<IdeType, boolean>()
- const pluginInstalledPromiseCache = new Map<IdeType, Promise<boolean>>()
- async function isJetBrainsPluginInstalledMemoized(
- ideType: IdeType,
- forceRefresh = false,
- ): Promise<boolean> {
- if (!forceRefresh) {
- const existing = pluginInstalledPromiseCache.get(ideType)
- if (existing) {
- return existing
- }
- }
- const promise = isJetBrainsPluginInstalled(ideType).then(result => {
- pluginInstalledCache.set(ideType, result)
- return result
- })
- pluginInstalledPromiseCache.set(ideType, promise)
- return promise
- }
- export async function isJetBrainsPluginInstalledCached(
- ideType: IdeType,
- forceRefresh = false,
- ): Promise<boolean> {
- if (forceRefresh) {
- pluginInstalledCache.delete(ideType)
- pluginInstalledPromiseCache.delete(ideType)
- }
- return isJetBrainsPluginInstalledMemoized(ideType, forceRefresh)
- }
- /**
- * Returns the cached result of isJetBrainsPluginInstalled synchronously.
- * Returns false if the result hasn't been resolved yet.
- * Use this only in sync contexts (e.g., status notice isActive checks).
- */
- export function isJetBrainsPluginInstalledCachedSync(
- ideType: IdeType,
- ): boolean {
- return pluginInstalledCache.get(ideType) ?? false
- }
|