/ utils / claudeInChrome / setupPortable.ts
setupPortable.ts
  1  import { readdir } from 'fs/promises'
  2  import { homedir } from 'os'
  3  import { join } from 'path'
  4  import { isFsInaccessible } from '../errors.js'
  5  
  6  export const CHROME_EXTENSION_URL = 'https://claude.ai/chrome'
  7  
  8  // Production extension ID
  9  const PROD_EXTENSION_ID = 'fcoeoabgfenejglbffodgkkbkcdhcgfn'
 10  // Dev extension IDs (for internal use)
 11  const DEV_EXTENSION_ID = 'dihbgbndebgnbjfmelmegjepbnkhlgni'
 12  const ANT_EXTENSION_ID = 'dngcpimnedloihjnnfngkgjoidhnaolf'
 13  
 14  function getExtensionIds(): string[] {
 15    return process.env.USER_TYPE === 'ant'
 16      ? [PROD_EXTENSION_ID, DEV_EXTENSION_ID, ANT_EXTENSION_ID]
 17      : [PROD_EXTENSION_ID]
 18  }
 19  
 20  // Must match ChromiumBrowser from common.ts
 21  export type ChromiumBrowser =
 22    | 'chrome'
 23    | 'brave'
 24    | 'arc'
 25    | 'chromium'
 26    | 'edge'
 27    | 'vivaldi'
 28    | 'opera'
 29  
 30  export type BrowserPath = {
 31    browser: ChromiumBrowser
 32    path: string
 33  }
 34  
 35  type Logger = (message: string) => void
 36  
 37  // Browser detection order - must match BROWSER_DETECTION_ORDER from common.ts
 38  const BROWSER_DETECTION_ORDER: ChromiumBrowser[] = [
 39    'chrome',
 40    'brave',
 41    'arc',
 42    'edge',
 43    'chromium',
 44    'vivaldi',
 45    'opera',
 46  ]
 47  
 48  type BrowserDataConfig = {
 49    macos: string[]
 50    linux: string[]
 51    windows: { path: string[]; useRoaming?: boolean }
 52  }
 53  
 54  // Must match CHROMIUM_BROWSERS dataPath from common.ts
 55  const CHROMIUM_BROWSERS: Record<ChromiumBrowser, BrowserDataConfig> = {
 56    chrome: {
 57      macos: ['Library', 'Application Support', 'Google', 'Chrome'],
 58      linux: ['.config', 'google-chrome'],
 59      windows: { path: ['Google', 'Chrome', 'User Data'] },
 60    },
 61    brave: {
 62      macos: ['Library', 'Application Support', 'BraveSoftware', 'Brave-Browser'],
 63      linux: ['.config', 'BraveSoftware', 'Brave-Browser'],
 64      windows: { path: ['BraveSoftware', 'Brave-Browser', 'User Data'] },
 65    },
 66    arc: {
 67      macos: ['Library', 'Application Support', 'Arc', 'User Data'],
 68      linux: [],
 69      windows: { path: ['Arc', 'User Data'] },
 70    },
 71    chromium: {
 72      macos: ['Library', 'Application Support', 'Chromium'],
 73      linux: ['.config', 'chromium'],
 74      windows: { path: ['Chromium', 'User Data'] },
 75    },
 76    edge: {
 77      macos: ['Library', 'Application Support', 'Microsoft Edge'],
 78      linux: ['.config', 'microsoft-edge'],
 79      windows: { path: ['Microsoft', 'Edge', 'User Data'] },
 80    },
 81    vivaldi: {
 82      macos: ['Library', 'Application Support', 'Vivaldi'],
 83      linux: ['.config', 'vivaldi'],
 84      windows: { path: ['Vivaldi', 'User Data'] },
 85    },
 86    opera: {
 87      macos: ['Library', 'Application Support', 'com.operasoftware.Opera'],
 88      linux: ['.config', 'opera'],
 89      windows: { path: ['Opera Software', 'Opera Stable'], useRoaming: true },
 90    },
 91  }
 92  
 93  /**
 94   * Get all browser data paths to check for extension installation.
 95   * Portable version that uses process.platform directly.
 96   */
 97  export function getAllBrowserDataPathsPortable(): BrowserPath[] {
 98    const home = homedir()
 99    const paths: BrowserPath[] = []
100  
101    for (const browserId of BROWSER_DETECTION_ORDER) {
102      const config = CHROMIUM_BROWSERS[browserId]
103      let dataPath: string[] | undefined
104  
105      switch (process.platform) {
106        case 'darwin':
107          dataPath = config.macos
108          break
109        case 'linux':
110          dataPath = config.linux
111          break
112        case 'win32': {
113          if (config.windows.path.length > 0) {
114            const appDataBase = config.windows.useRoaming
115              ? join(home, 'AppData', 'Roaming')
116              : join(home, 'AppData', 'Local')
117            paths.push({
118              browser: browserId,
119              path: join(appDataBase, ...config.windows.path),
120            })
121          }
122          continue
123        }
124      }
125  
126      if (dataPath && dataPath.length > 0) {
127        paths.push({
128          browser: browserId,
129          path: join(home, ...dataPath),
130        })
131      }
132    }
133  
134    return paths
135  }
136  
137  /**
138   * Detects if the Claude in Chrome extension is installed by checking the Extensions
139   * directory across all supported Chromium-based browsers and their profiles.
140   *
141   * This is a portable version that can be used by both TUI and VS Code extension.
142   *
143   * @param browserPaths - Array of browser data paths to check (from getAllBrowserDataPaths)
144   * @param log - Optional logging callback for debug messages
145   * @returns Object with isInstalled boolean and the browser where the extension was found
146   */
147  export async function detectExtensionInstallationPortable(
148    browserPaths: BrowserPath[],
149    log?: Logger,
150  ): Promise<{
151    isInstalled: boolean
152    browser: ChromiumBrowser | null
153  }> {
154    if (browserPaths.length === 0) {
155      log?.(`[Claude in Chrome] No browser paths to check`)
156      return { isInstalled: false, browser: null }
157    }
158  
159    const extensionIds = getExtensionIds()
160  
161    // Check each browser for the extension
162    for (const { browser, path: browserBasePath } of browserPaths) {
163      let browserProfileEntries = []
164  
165      try {
166        browserProfileEntries = await readdir(browserBasePath, {
167          withFileTypes: true,
168        })
169      } catch (e) {
170        // Browser not installed or path doesn't exist, continue to next browser
171        if (isFsInaccessible(e)) continue
172        throw e
173      }
174  
175      const profileDirs = browserProfileEntries
176        .filter(entry => entry.isDirectory())
177        .filter(
178          entry => entry.name === 'Default' || entry.name.startsWith('Profile '),
179        )
180        .map(entry => entry.name)
181  
182      if (profileDirs.length > 0) {
183        log?.(
184          `[Claude in Chrome] Found ${browser} profiles: ${profileDirs.join(', ')}`,
185        )
186      }
187  
188      // Check each profile for any of the extension IDs
189      for (const profile of profileDirs) {
190        for (const extensionId of extensionIds) {
191          const extensionPath = join(
192            browserBasePath,
193            profile,
194            'Extensions',
195            extensionId,
196          )
197  
198          try {
199            await readdir(extensionPath)
200            log?.(
201              `[Claude in Chrome] Extension ${extensionId} found in ${browser} ${profile}`,
202            )
203            return { isInstalled: true, browser }
204          } catch {
205            // Extension not found in this profile, continue checking
206          }
207        }
208      }
209    }
210  
211    log?.(`[Claude in Chrome] Extension not found in any browser`)
212    return { isInstalled: false, browser: null }
213  }
214  
215  /**
216   * Simple wrapper that returns just the boolean result
217   */
218  export async function isChromeExtensionInstalledPortable(
219    browserPaths: BrowserPath[],
220    log?: Logger,
221  ): Promise<boolean> {
222    const result = await detectExtensionInstallationPortable(browserPaths, log)
223    return result.isInstalled
224  }
225  
226  /**
227   * Convenience function that gets browser paths automatically.
228   * Use this when you don't need to provide custom browser paths.
229   */
230  export function isChromeExtensionInstalled(log?: Logger): Promise<boolean> {
231    const browserPaths = getAllBrowserDataPathsPortable()
232    return isChromeExtensionInstalledPortable(browserPaths, log)
233  }