234 lines
6.6 KiB
TypeScript
234 lines
6.6 KiB
TypeScript
import { readdir } from 'fs/promises'
|
|
import { homedir } from 'os'
|
|
import { join } from 'path'
|
|
import { isFsInaccessible } from '../errors.js'
|
|
|
|
export const CHROME_EXTENSION_URL = 'https://claude.ai/chrome'
|
|
|
|
// Production extension ID
|
|
const PROD_EXTENSION_ID = 'fcoeoabgfenejglbffodgkkbkcdhcgfn'
|
|
// Dev extension IDs (for internal use)
|
|
const DEV_EXTENSION_ID = 'dihbgbndebgnbjfmelmegjepbnkhlgni'
|
|
const ANT_EXTENSION_ID = 'dngcpimnedloihjnnfngkgjoidhnaolf'
|
|
|
|
function getExtensionIds(): string[] {
|
|
return process.env.USER_TYPE === 'ant'
|
|
? [PROD_EXTENSION_ID, DEV_EXTENSION_ID, ANT_EXTENSION_ID]
|
|
: [PROD_EXTENSION_ID]
|
|
}
|
|
|
|
// Must match ChromiumBrowser from common.ts
|
|
export type ChromiumBrowser =
|
|
| 'chrome'
|
|
| 'brave'
|
|
| 'arc'
|
|
| 'chromium'
|
|
| 'edge'
|
|
| 'vivaldi'
|
|
| 'opera'
|
|
|
|
export type BrowserPath = {
|
|
browser: ChromiumBrowser
|
|
path: string
|
|
}
|
|
|
|
type Logger = (message: string) => void
|
|
|
|
// Browser detection order - must match BROWSER_DETECTION_ORDER from common.ts
|
|
const BROWSER_DETECTION_ORDER: ChromiumBrowser[] = [
|
|
'chrome',
|
|
'brave',
|
|
'arc',
|
|
'edge',
|
|
'chromium',
|
|
'vivaldi',
|
|
'opera',
|
|
]
|
|
|
|
type BrowserDataConfig = {
|
|
macos: string[]
|
|
linux: string[]
|
|
windows: { path: string[]; useRoaming?: boolean }
|
|
}
|
|
|
|
// Must match CHROMIUM_BROWSERS dataPath from common.ts
|
|
const CHROMIUM_BROWSERS: Record<ChromiumBrowser, BrowserDataConfig> = {
|
|
chrome: {
|
|
macos: ['Library', 'Application Support', 'Google', 'Chrome'],
|
|
linux: ['.config', 'google-chrome'],
|
|
windows: { path: ['Google', 'Chrome', 'User Data'] },
|
|
},
|
|
brave: {
|
|
macos: ['Library', 'Application Support', 'BraveSoftware', 'Brave-Browser'],
|
|
linux: ['.config', 'BraveSoftware', 'Brave-Browser'],
|
|
windows: { path: ['BraveSoftware', 'Brave-Browser', 'User Data'] },
|
|
},
|
|
arc: {
|
|
macos: ['Library', 'Application Support', 'Arc', 'User Data'],
|
|
linux: [],
|
|
windows: { path: ['Arc', 'User Data'] },
|
|
},
|
|
chromium: {
|
|
macos: ['Library', 'Application Support', 'Chromium'],
|
|
linux: ['.config', 'chromium'],
|
|
windows: { path: ['Chromium', 'User Data'] },
|
|
},
|
|
edge: {
|
|
macos: ['Library', 'Application Support', 'Microsoft Edge'],
|
|
linux: ['.config', 'microsoft-edge'],
|
|
windows: { path: ['Microsoft', 'Edge', 'User Data'] },
|
|
},
|
|
vivaldi: {
|
|
macos: ['Library', 'Application Support', 'Vivaldi'],
|
|
linux: ['.config', 'vivaldi'],
|
|
windows: { path: ['Vivaldi', 'User Data'] },
|
|
},
|
|
opera: {
|
|
macos: ['Library', 'Application Support', 'com.operasoftware.Opera'],
|
|
linux: ['.config', 'opera'],
|
|
windows: { path: ['Opera Software', 'Opera Stable'], useRoaming: true },
|
|
},
|
|
}
|
|
|
|
/**
|
|
* Get all browser data paths to check for extension installation.
|
|
* Portable version that uses process.platform directly.
|
|
*/
|
|
export function getAllBrowserDataPathsPortable(): BrowserPath[] {
|
|
const home = homedir()
|
|
const paths: BrowserPath[] = []
|
|
|
|
for (const browserId of BROWSER_DETECTION_ORDER) {
|
|
const config = CHROMIUM_BROWSERS[browserId]
|
|
let dataPath: string[] | undefined
|
|
|
|
switch (process.platform) {
|
|
case 'darwin':
|
|
dataPath = config.macos
|
|
break
|
|
case 'linux':
|
|
dataPath = config.linux
|
|
break
|
|
case 'win32': {
|
|
if (config.windows.path.length > 0) {
|
|
const appDataBase = config.windows.useRoaming
|
|
? join(home, 'AppData', 'Roaming')
|
|
: join(home, 'AppData', 'Local')
|
|
paths.push({
|
|
browser: browserId,
|
|
path: join(appDataBase, ...config.windows.path),
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
if (dataPath && dataPath.length > 0) {
|
|
paths.push({
|
|
browser: browserId,
|
|
path: join(home, ...dataPath),
|
|
})
|
|
}
|
|
}
|
|
|
|
return paths
|
|
}
|
|
|
|
/**
|
|
* Detects if the Claude in Chrome extension is installed by checking the Extensions
|
|
* directory across all supported Chromium-based browsers and their profiles.
|
|
*
|
|
* This is a portable version that can be used by both TUI and VS Code extension.
|
|
*
|
|
* @param browserPaths - Array of browser data paths to check (from getAllBrowserDataPaths)
|
|
* @param log - Optional logging callback for debug messages
|
|
* @returns Object with isInstalled boolean and the browser where the extension was found
|
|
*/
|
|
export async function detectExtensionInstallationPortable(
|
|
browserPaths: BrowserPath[],
|
|
log?: Logger,
|
|
): Promise<{
|
|
isInstalled: boolean
|
|
browser: ChromiumBrowser | null
|
|
}> {
|
|
if (browserPaths.length === 0) {
|
|
log?.(`[Claude in Chrome] No browser paths to check`)
|
|
return { isInstalled: false, browser: null }
|
|
}
|
|
|
|
const extensionIds = getExtensionIds()
|
|
|
|
// Check each browser for the extension
|
|
for (const { browser, path: browserBasePath } of browserPaths) {
|
|
let browserProfileEntries = []
|
|
|
|
try {
|
|
browserProfileEntries = await readdir(browserBasePath, {
|
|
withFileTypes: true,
|
|
})
|
|
} catch (e) {
|
|
// Browser not installed or path doesn't exist, continue to next browser
|
|
if (isFsInaccessible(e)) continue
|
|
throw e
|
|
}
|
|
|
|
const profileDirs = browserProfileEntries
|
|
.filter(entry => entry.isDirectory())
|
|
.filter(
|
|
entry => entry.name === 'Default' || entry.name.startsWith('Profile '),
|
|
)
|
|
.map(entry => entry.name)
|
|
|
|
if (profileDirs.length > 0) {
|
|
log?.(
|
|
`[Claude in Chrome] Found ${browser} profiles: ${profileDirs.join(', ')}`,
|
|
)
|
|
}
|
|
|
|
// Check each profile for any of the extension IDs
|
|
for (const profile of profileDirs) {
|
|
for (const extensionId of extensionIds) {
|
|
const extensionPath = join(
|
|
browserBasePath,
|
|
profile,
|
|
'Extensions',
|
|
extensionId,
|
|
)
|
|
|
|
try {
|
|
await readdir(extensionPath)
|
|
log?.(
|
|
`[Claude in Chrome] Extension ${extensionId} found in ${browser} ${profile}`,
|
|
)
|
|
return { isInstalled: true, browser }
|
|
} catch {
|
|
// Extension not found in this profile, continue checking
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
log?.(`[Claude in Chrome] Extension not found in any browser`)
|
|
return { isInstalled: false, browser: null }
|
|
}
|
|
|
|
/**
|
|
* Simple wrapper that returns just the boolean result
|
|
*/
|
|
export async function isChromeExtensionInstalledPortable(
|
|
browserPaths: BrowserPath[],
|
|
log?: Logger,
|
|
): Promise<boolean> {
|
|
const result = await detectExtensionInstallationPortable(browserPaths, log)
|
|
return result.isInstalled
|
|
}
|
|
|
|
/**
|
|
* Convenience function that gets browser paths automatically.
|
|
* Use this when you don't need to provide custom browser paths.
|
|
*/
|
|
export function isChromeExtensionInstalled(log?: Logger): Promise<boolean> {
|
|
const browserPaths = getAllBrowserDataPathsPortable()
|
|
return isChromeExtensionInstalledPortable(browserPaths, log)
|
|
}
|