πŸ“„ File detail

utils/jetbrains.ts

🧩 .tsπŸ“ 192 linesπŸ’Ύ 5,805 bytesπŸ“ text
← Back to All Files

🎯 Use case

This file lives under β€œutils/”, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, …). On the API surface it exposes isJetBrainsPluginInstalled, isJetBrainsPluginInstalledCached, and isJetBrainsPluginInstalledCachedSync β€” mainly functions, hooks, or classes. Dependencies touch Node OS/process metadata and Node path helpers. It composes internal code from utils and ide (relative imports).

Generated from folder role, exports, dependency roots, and inline comments β€” not hand-reviewed for every path.

🧠 Inline summary

import { homedir, platform } from 'os' import { join } from 'path' import { getFsImplementation } from '../utils/fsOperations.js' import type { IdeType } from './ide.js'

πŸ“€ Exports (heuristic)

  • isJetBrainsPluginInstalled
  • isJetBrainsPluginInstalledCached
  • isJetBrainsPluginInstalledCachedSync

πŸ“š External import roots

Package roots from from "…" (relative paths omitted).

  • os
  • path

πŸ–₯️ Source preview

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
}