| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import os from 'os'; |
| import path from 'path'; |
| import fsSync from 'fs'; |
| import fs from 'fs/promises'; |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| function getNvmWindowsCliPaths(cliName: string): string[] { |
| const nvmSymlink = process.env.NVM_SYMLINK; |
| if (!nvmSymlink) return []; |
| return [path.join(nvmSymlink, `${cliName}.cmd`), path.join(nvmSymlink, cliName)]; |
| } |
|
|
| |
| |
| |
| export function getGitHubCliPaths(): string[] { |
| const isWindows = process.platform === 'win32'; |
|
|
| if (isWindows) { |
| return [ |
| path.join(process.env.LOCALAPPDATA || '', 'Programs', 'gh', 'bin', 'gh.exe'), |
| path.join(process.env.ProgramFiles || '', 'GitHub CLI', 'gh.exe'), |
| ].filter(Boolean); |
| } |
|
|
| return [ |
| '/opt/homebrew/bin/gh', |
| '/usr/local/bin/gh', |
| '/usr/bin/gh', |
| path.join(os.homedir(), '.local', 'bin', 'gh'), |
| '/home/linuxbrew/.linuxbrew/bin/gh', |
| ]; |
| } |
|
|
| |
| |
| |
| export function getClaudeCliPaths(): string[] { |
| const isWindows = process.platform === 'win32'; |
|
|
| if (isWindows) { |
| const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'); |
| return [ |
| path.join(os.homedir(), '.local', 'bin', 'claude.exe'), |
| path.join(appData, 'npm', 'claude.cmd'), |
| path.join(appData, 'npm', 'claude'), |
| path.join(appData, '.npm-global', 'bin', 'claude.cmd'), |
| path.join(appData, '.npm-global', 'bin', 'claude'), |
| ...getNvmWindowsCliPaths('claude'), |
| ]; |
| } |
|
|
| return [ |
| path.join(os.homedir(), '.local', 'bin', 'claude'), |
| path.join(os.homedir(), '.claude', 'local', 'claude'), |
| '/usr/local/bin/claude', |
| path.join(os.homedir(), '.npm-global', 'bin', 'claude'), |
| ]; |
| } |
|
|
| |
| |
| |
| function getNvmBinPaths(): string[] { |
| const nvmDir = process.env.NVM_DIR || path.join(os.homedir(), '.nvm'); |
| const versionsDir = path.join(nvmDir, 'versions', 'node'); |
|
|
| try { |
| if (!fsSync.existsSync(versionsDir)) { |
| return []; |
| } |
| const versions = fsSync.readdirSync(versionsDir); |
| return versions.map((version) => path.join(versionsDir, version, 'bin')); |
| } catch { |
| return []; |
| } |
| } |
|
|
| |
| |
| |
| function getFnmBinPaths(): string[] { |
| const homeDir = os.homedir(); |
| const possibleFnmDirs = [ |
| path.join(homeDir, '.local', 'share', 'fnm', 'node-versions'), |
| path.join(homeDir, '.fnm', 'node-versions'), |
| |
| path.join(homeDir, 'Library', 'Application Support', 'fnm', 'node-versions'), |
| ]; |
|
|
| const binPaths: string[] = []; |
|
|
| for (const fnmDir of possibleFnmDirs) { |
| try { |
| if (!fsSync.existsSync(fnmDir)) { |
| continue; |
| } |
| const versions = fsSync.readdirSync(fnmDir); |
| for (const version of versions) { |
| binPaths.push(path.join(fnmDir, version, 'installation', 'bin')); |
| } |
| } catch { |
| |
| } |
| } |
|
|
| return binPaths; |
| } |
|
|
| |
| |
| |
| export function getCodexCliPaths(): string[] { |
| const isWindows = process.platform === 'win32'; |
| const homeDir = os.homedir(); |
|
|
| if (isWindows) { |
| const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'); |
| const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'); |
| return [ |
| path.join(homeDir, '.local', 'bin', 'codex.exe'), |
| path.join(appData, 'npm', 'codex.cmd'), |
| path.join(appData, 'npm', 'codex'), |
| path.join(appData, '.npm-global', 'bin', 'codex.cmd'), |
| path.join(appData, '.npm-global', 'bin', 'codex'), |
| |
| path.join(homeDir, '.volta', 'bin', 'codex.exe'), |
| |
| path.join(localAppData, 'pnpm', 'codex.cmd'), |
| path.join(localAppData, 'pnpm', 'codex'), |
| ...getNvmWindowsCliPaths('codex'), |
| ]; |
| } |
|
|
| |
| const nvmBinPaths = getNvmBinPaths().map((binPath) => path.join(binPath, 'codex')); |
|
|
| |
| const fnmBinPaths = getFnmBinPaths().map((binPath) => path.join(binPath, 'codex')); |
|
|
| |
| const pnpmHome = process.env.PNPM_HOME || path.join(homeDir, '.local', 'share', 'pnpm'); |
|
|
| return [ |
| |
| path.join(homeDir, '.local', 'bin', 'codex'), |
| '/opt/homebrew/bin/codex', |
| '/usr/local/bin/codex', |
| '/usr/bin/codex', |
| path.join(homeDir, '.npm-global', 'bin', 'codex'), |
| |
| '/home/linuxbrew/.linuxbrew/bin/codex', |
| |
| path.join(homeDir, '.volta', 'bin', 'codex'), |
| |
| path.join(pnpmHome, 'codex'), |
| |
| path.join(homeDir, '.yarn', 'bin', 'codex'), |
| path.join(homeDir, '.config', 'yarn', 'global', 'node_modules', '.bin', 'codex'), |
| |
| '/snap/bin/codex', |
| |
| ...nvmBinPaths, |
| |
| ...fnmBinPaths, |
| ]; |
| } |
|
|
| const CODEX_CONFIG_DIR_NAME = '.codex'; |
| const CODEX_AUTH_FILENAME = 'auth.json'; |
| const CODEX_TOKENS_KEY = 'tokens'; |
|
|
| |
| |
| |
| export function getCodexConfigDir(): string { |
| return path.join(os.homedir(), CODEX_CONFIG_DIR_NAME); |
| } |
|
|
| |
| |
| |
| export function getCodexAuthPath(): string { |
| return path.join(getCodexConfigDir(), CODEX_AUTH_FILENAME); |
| } |
|
|
| |
| |
| |
| export function getClaudeConfigDir(): string { |
| return path.join(os.homedir(), '.claude'); |
| } |
|
|
| |
| |
| |
| export function getClaudeCredentialPaths(): string[] { |
| const claudeDir = getClaudeConfigDir(); |
| return [path.join(claudeDir, '.credentials.json'), path.join(claudeDir, 'credentials.json')]; |
| } |
|
|
| |
| |
| |
| export function getClaudeSettingsPath(): string { |
| return path.join(getClaudeConfigDir(), 'settings.json'); |
| } |
|
|
| |
| |
| |
| export function getClaudeStatsCachePath(): string { |
| return path.join(getClaudeConfigDir(), 'stats-cache.json'); |
| } |
|
|
| |
| |
| |
| export function getClaudeProjectsDir(): string { |
| return path.join(getClaudeConfigDir(), 'projects'); |
| } |
|
|
| |
| |
| |
| |
| function enumerateMatchingPaths( |
| parentDir: string, |
| prefix: string, |
| ...subPathParts: string[] |
| ): string[] { |
| try { |
| if (!fsSync.existsSync(parentDir)) { |
| return []; |
| } |
| const entries = fsSync.readdirSync(parentDir); |
| const matching = entries.filter((entry) => entry.startsWith(prefix)); |
| return matching.map((entry) => path.join(parentDir, entry, ...subPathParts)); |
| } catch { |
| return []; |
| } |
| } |
|
|
| |
| |
| |
| |
| export function getGitBashPaths(): string[] { |
| if (process.platform !== 'win32') { |
| return []; |
| } |
|
|
| const homeDir = os.homedir(); |
| const localAppData = process.env.LOCALAPPDATA || ''; |
|
|
| |
| |
| const wingetGitPaths = localAppData |
| ? enumerateMatchingPaths( |
| path.join(localAppData, 'Microsoft', 'WinGet', 'Packages'), |
| 'Git.Git_', |
| 'bin', |
| 'bash.exe' |
| ) |
| : []; |
|
|
| |
| const githubDesktopPaths = localAppData |
| ? enumerateMatchingPaths( |
| path.join(localAppData, 'GitHubDesktop'), |
| 'app-', |
| 'resources', |
| 'app', |
| 'git', |
| 'cmd', |
| 'bash.exe' |
| ) |
| : []; |
|
|
| return [ |
| |
| 'C:\\Program Files\\Git\\bin\\bash.exe', |
| 'C:\\Program Files (x86)\\Git\\bin\\bash.exe', |
| |
| path.join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'), |
| |
| path.join(homeDir, 'scoop', 'apps', 'git', 'current', 'bin', 'bash.exe'), |
| |
| path.join( |
| process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey', |
| 'lib', |
| 'git', |
| 'tools', |
| 'bin', |
| 'bash.exe' |
| ), |
| |
| ...wingetGitPaths, |
| |
| ...githubDesktopPaths, |
| ].filter(Boolean); |
| } |
|
|
| |
| |
| |
| |
| export function getShellPaths(): string[] { |
| if (process.platform === 'win32') { |
| return [ |
| |
| 'C:\\Program Files\\PowerShell\\7\\pwsh.exe', |
| 'C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe', |
| 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', |
| |
| process.env.COMSPEC || 'C:\\Windows\\System32\\cmd.exe', |
| |
| 'pwsh.exe', |
| 'pwsh', |
| 'powershell.exe', |
| 'powershell', |
| 'cmd.exe', |
| 'cmd', |
| ]; |
| } |
|
|
| |
| return [ |
| |
| '/bin/zsh', |
| '/bin/bash', |
| '/bin/sh', |
| '/usr/bin/zsh', |
| '/usr/bin/bash', |
| '/usr/bin/sh', |
| '/usr/local/bin/zsh', |
| '/usr/local/bin/bash', |
| '/opt/homebrew/bin/zsh', |
| '/opt/homebrew/bin/bash', |
| |
| 'zsh', |
| 'bash', |
| 'sh', |
| ]; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| export function getNvmPaths(): string[] { |
| const homeDir = os.homedir(); |
|
|
| if (process.platform === 'win32') { |
| const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'); |
| return [path.join(appData, 'nvm')]; |
| } |
|
|
| return [path.join(homeDir, '.nvm', 'versions', 'node')]; |
| } |
|
|
| |
| |
| |
| export function getFnmPaths(): string[] { |
| const homeDir = os.homedir(); |
|
|
| if (process.platform === 'win32') { |
| const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'); |
| return [ |
| path.join(homeDir, '.fnm', 'node-versions'), |
| path.join(localAppData, 'fnm', 'node-versions'), |
| ]; |
| } |
|
|
| if (process.platform === 'darwin') { |
| return [ |
| path.join(homeDir, '.local', 'share', 'fnm', 'node-versions'), |
| path.join(homeDir, 'Library', 'Application Support', 'fnm', 'node-versions'), |
| ]; |
| } |
|
|
| return [ |
| path.join(homeDir, '.local', 'share', 'fnm', 'node-versions'), |
| path.join(homeDir, '.fnm', 'node-versions'), |
| ]; |
| } |
|
|
| |
| |
| |
| export function getNodeSystemPaths(): string[] { |
| if (process.platform === 'win32') { |
| return [ |
| path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'nodejs', 'node.exe'), |
| path.join( |
| process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', |
| 'nodejs', |
| 'node.exe' |
| ), |
| ]; |
| } |
|
|
| if (process.platform === 'darwin') { |
| return ['/opt/homebrew/bin/node', '/usr/local/bin/node', '/usr/bin/node']; |
| } |
|
|
| |
| return ['/usr/bin/node', '/usr/local/bin/node', '/snap/bin/node']; |
| } |
|
|
| |
| |
| |
| export function getScoopNodePath(): string { |
| return path.join(os.homedir(), 'scoop', 'apps', 'nodejs', 'current', 'node.exe'); |
| } |
|
|
| |
| |
| |
| export function getChocolateyNodePath(): string { |
| return path.join( |
| process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey', |
| 'bin', |
| 'node.exe' |
| ); |
| } |
|
|
| |
| |
| |
| export function getWslVersionPath(): string { |
| return '/proc/version'; |
| } |
|
|
| |
| |
| |
| export function getExtendedPath(): string { |
| const paths = [ |
| process.env.PATH, |
| '/opt/homebrew/bin', |
| '/usr/local/bin', |
| '/home/linuxbrew/.linuxbrew/bin', |
| `${process.env.HOME}/.local/bin`, |
| ]; |
|
|
| return paths.filter(Boolean).join(process.platform === 'win32' ? ';' : ':'); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| export function systemPathExists(filePath: string): boolean { |
| if (!isAllowedSystemPath(filePath)) { |
| throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`); |
| } |
| return fsSync.existsSync(filePath); |
| } |
|
|
| |
| |
| |
| |
| |
| export async function systemPathAccess(filePath: string): Promise<boolean> { |
| if (!isAllowedSystemPath(filePath)) { |
| throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`); |
| } |
| try { |
| await fs.access(filePath); |
| return true; |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| export function systemPathIsExecutable(filePath: string): boolean { |
| if (!isAllowedSystemPath(filePath)) { |
| throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`); |
| } |
| try { |
| if (process.platform === 'win32') { |
| fsSync.accessSync(filePath, fsSync.constants.F_OK); |
| } else { |
| fsSync.accessSync(filePath, fsSync.constants.X_OK); |
| } |
| return true; |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export async function systemPathReadFile( |
| filePath: string, |
| encoding: BufferEncoding = 'utf-8' |
| ): Promise<string> { |
| if (!isAllowedSystemPath(filePath)) { |
| throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`); |
| } |
| return fs.readFile(filePath, encoding); |
| } |
|
|
| |
| |
| |
| export function systemPathReadFileSync( |
| filePath: string, |
| encoding: BufferEncoding = 'utf-8' |
| ): string { |
| if (!isAllowedSystemPath(filePath)) { |
| throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`); |
| } |
| return fsSync.readFileSync(filePath, encoding); |
| } |
|
|
| |
| |
| |
| export function systemPathWriteFileSync( |
| filePath: string, |
| data: string, |
| options?: { encoding?: BufferEncoding; mode?: number } |
| ): void { |
| if (!isAllowedSystemPath(filePath)) { |
| throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`); |
| } |
| fsSync.writeFileSync(filePath, data, options); |
| } |
|
|
| |
| |
| |
| |
| export async function systemPathReaddir(dirPath: string): Promise<string[]> { |
| if (!isAllowedSystemPath(dirPath)) { |
| throw new Error(`[SystemPaths] Access denied: ${dirPath} is not an allowed system path`); |
| } |
| return fs.readdir(dirPath); |
| } |
|
|
| |
| |
| |
| export function systemPathReaddirSync(dirPath: string): string[] { |
| if (!isAllowedSystemPath(dirPath)) { |
| throw new Error(`[SystemPaths] Access denied: ${dirPath} is not an allowed system path`); |
| } |
| return fsSync.readdirSync(dirPath); |
| } |
|
|
| |
| |
| |
| export function systemPathStatSync(filePath: string): fsSync.Stats { |
| if (!isAllowedSystemPath(filePath)) { |
| throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`); |
| } |
| return fsSync.statSync(filePath); |
| } |
|
|
| |
| |
| |
| export async function systemPathStat(filePath: string): Promise<fsSync.Stats> { |
| if (!isAllowedSystemPath(filePath)) { |
| throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`); |
| } |
| return fs.stat(filePath); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| function getAllAllowedSystemPaths(): string[] { |
| return [ |
| |
| ...getGitHubCliPaths(), |
| |
| ...getClaudeCliPaths(), |
| |
| getClaudeConfigDir(), |
| ...getClaudeCredentialPaths(), |
| getClaudeSettingsPath(), |
| getClaudeStatsCachePath(), |
| getClaudeProjectsDir(), |
| |
| ...getCodexCliPaths(), |
| |
| getCodexConfigDir(), |
| getCodexAuthPath(), |
| |
| ...getOpenCodeCliPaths(), |
| |
| getOpenCodeConfigDir(), |
| getOpenCodeAuthPath(), |
| |
| ...getShellPaths(), |
| |
| ...getGitBashPaths(), |
| |
| ...getNodeSystemPaths(), |
| getScoopNodePath(), |
| getChocolateyNodePath(), |
| |
| getWslVersionPath(), |
| ]; |
| } |
|
|
| |
| |
| |
| function getAllAllowedSystemDirs(): string[] { |
| return [ |
| |
| getClaudeConfigDir(), |
| getClaudeProjectsDir(), |
| |
| getCodexConfigDir(), |
| |
| getOpenCodeConfigDir(), |
| |
| ...getNvmPaths(), |
| ...getFnmPaths(), |
| ]; |
| } |
|
|
| |
| |
| |
| |
| export function isAllowedSystemPath(filePath: string): boolean { |
| const normalizedPath = path.resolve(filePath); |
| const allowedPaths = getAllAllowedSystemPaths(); |
|
|
| |
| if (allowedPaths.includes(normalizedPath)) { |
| return true; |
| } |
|
|
| |
| const allowedDirs = getAllAllowedSystemDirs(); |
|
|
| for (const allowedDir of allowedDirs) { |
| const normalizedAllowedDir = path.resolve(allowedDir); |
| |
| if ( |
| normalizedPath === normalizedAllowedDir || |
| normalizedPath.startsWith(normalizedAllowedDir + path.sep) |
| ) { |
| return true; |
| } |
| } |
|
|
| return false; |
| } |
|
|
| |
| |
| |
|
|
| |
| let electronUserDataPath: string | null = null; |
|
|
| |
| |
| |
| export function setElectronUserDataPath(userDataPath: string): void { |
| electronUserDataPath = userDataPath; |
| } |
|
|
| |
| |
| |
| export function getElectronUserDataPath(): string | null { |
| return electronUserDataPath; |
| } |
|
|
| |
| |
| |
| export function isElectronUserDataPath(filePath: string): boolean { |
| if (!electronUserDataPath) return false; |
| const normalizedPath = path.resolve(filePath); |
| const normalizedUserData = path.resolve(electronUserDataPath); |
| return ( |
| normalizedPath === normalizedUserData || |
| normalizedPath.startsWith(normalizedUserData + path.sep) |
| ); |
| } |
|
|
| |
| |
| |
| export function electronUserDataReadFileSync( |
| relativePath: string, |
| encoding: BufferEncoding = 'utf-8' |
| ): string { |
| if (!electronUserDataPath) { |
| throw new Error('[SystemPaths] Electron userData path not initialized'); |
| } |
| const fullPath = path.join(electronUserDataPath, relativePath); |
| return fsSync.readFileSync(fullPath, encoding); |
| } |
|
|
| |
| |
| |
| export function electronUserDataWriteFileSync( |
| relativePath: string, |
| data: string, |
| options?: { encoding?: BufferEncoding; mode?: number } |
| ): void { |
| if (!electronUserDataPath) { |
| throw new Error('[SystemPaths] Electron userData path not initialized'); |
| } |
| const fullPath = path.join(electronUserDataPath, relativePath); |
| |
| const dir = path.dirname(fullPath); |
| fsSync.mkdirSync(dir, { recursive: true }); |
| fsSync.writeFileSync(fullPath, data, options); |
| } |
|
|
| |
| |
| |
| export function electronUserDataExists(relativePath: string): boolean { |
| if (!electronUserDataPath) return false; |
| const fullPath = path.join(electronUserDataPath, relativePath); |
| return fsSync.existsSync(fullPath); |
| } |
|
|
| |
| |
| |
|
|
| |
| let scriptBaseDir: string | null = null; |
|
|
| |
| |
| |
| export function setScriptBaseDir(baseDir: string): void { |
| scriptBaseDir = baseDir; |
| } |
|
|
| |
| |
| |
| export function getScriptBaseDir(): string | null { |
| return scriptBaseDir; |
| } |
|
|
| |
| |
| |
| export function scriptDirExists(relativePath: string): boolean { |
| if (!scriptBaseDir) { |
| throw new Error('[SystemPaths] Script base directory not initialized'); |
| } |
| const fullPath = path.join(scriptBaseDir, relativePath); |
| return fsSync.existsSync(fullPath); |
| } |
|
|
| |
| |
| |
| export function scriptDirMkdirSync(relativePath: string, options?: { recursive?: boolean }): void { |
| if (!scriptBaseDir) { |
| throw new Error('[SystemPaths] Script base directory not initialized'); |
| } |
| const fullPath = path.join(scriptBaseDir, relativePath); |
| fsSync.mkdirSync(fullPath, options); |
| } |
|
|
| |
| |
| |
| export function scriptDirCreateWriteStream(relativePath: string): fsSync.WriteStream { |
| if (!scriptBaseDir) { |
| throw new Error('[SystemPaths] Script base directory not initialized'); |
| } |
| const fullPath = path.join(scriptBaseDir, relativePath); |
| return fsSync.createWriteStream(fullPath); |
| } |
|
|
| |
| |
| |
|
|
| |
| let electronAppDirs: string[] = []; |
| let electronResourcesPath: string | null = null; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function setElectronAppPaths(appDirOrDirs: string | string[], resourcesPath?: string): void { |
| electronAppDirs = Array.isArray(appDirOrDirs) ? appDirOrDirs : [appDirOrDirs]; |
| electronResourcesPath = resourcesPath || null; |
| } |
|
|
| |
| |
| |
| function isElectronAppPath(filePath: string): boolean { |
| const normalizedPath = path.resolve(filePath); |
|
|
| |
| for (const appDir of electronAppDirs) { |
| const normalizedAppDir = path.resolve(appDir); |
| if ( |
| normalizedPath === normalizedAppDir || |
| normalizedPath.startsWith(normalizedAppDir + path.sep) |
| ) { |
| return true; |
| } |
| } |
|
|
| |
| if (electronResourcesPath) { |
| const normalizedResources = path.resolve(electronResourcesPath); |
| if ( |
| normalizedPath === normalizedResources || |
| normalizedPath.startsWith(normalizedResources + path.sep) |
| ) { |
| return true; |
| } |
| } |
|
|
| return false; |
| } |
|
|
| |
| |
| |
| export function electronAppExists(filePath: string): boolean { |
| if (!isElectronAppPath(filePath)) { |
| throw new Error(`[SystemPaths] Access denied: ${filePath} is not within Electron app bundle`); |
| } |
| return fsSync.existsSync(filePath); |
| } |
|
|
| |
| |
| |
| export function electronAppReadFileSync(filePath: string): Buffer { |
| if (!isElectronAppPath(filePath)) { |
| throw new Error(`[SystemPaths] Access denied: ${filePath} is not within Electron app bundle`); |
| } |
| return fsSync.readFileSync(filePath); |
| } |
|
|
| |
| |
| |
| export function electronAppStatSync(filePath: string): fsSync.Stats { |
| if (!isElectronAppPath(filePath)) { |
| throw new Error(`[SystemPaths] Access denied: ${filePath} is not within Electron app bundle`); |
| } |
| return fsSync.statSync(filePath); |
| } |
|
|
| |
| |
| |
| export function electronAppStat( |
| filePath: string, |
| callback: (err: NodeJS.ErrnoException | null, stats: fsSync.Stats | undefined) => void |
| ): void { |
| if (!isElectronAppPath(filePath)) { |
| callback( |
| new Error(`[SystemPaths] Access denied: ${filePath} is not within Electron app bundle`), |
| undefined |
| ); |
| return; |
| } |
| fsSync.stat(filePath, callback); |
| } |
|
|
| |
| |
| |
| export function electronAppReadFile( |
| filePath: string, |
| callback: (err: NodeJS.ErrnoException | null, data: Buffer | undefined) => void |
| ): void { |
| if (!isElectronAppPath(filePath)) { |
| callback( |
| new Error(`[SystemPaths] Access denied: ${filePath} is not within Electron app bundle`), |
| undefined |
| ); |
| return; |
| } |
| fsSync.readFile(filePath, callback); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| export async function findFirstExistingPath(paths: string[]): Promise<string | null> { |
| for (const p of paths) { |
| if (await systemPathAccess(p)) { |
| return p; |
| } |
| } |
| return null; |
| } |
|
|
| |
| |
| |
| export async function findGitHubCliPath(): Promise<string | null> { |
| return findFirstExistingPath(getGitHubCliPaths()); |
| } |
|
|
| |
| |
| |
| export async function findClaudeCliPath(): Promise<string | null> { |
| return findFirstExistingPath(getClaudeCliPaths()); |
| } |
|
|
| export async function findCodexCliPath(): Promise<string | null> { |
| return findFirstExistingPath(getCodexCliPaths()); |
| } |
|
|
| |
| |
| |
| export async function findGitBashPath(): Promise<string | null> { |
| return findFirstExistingPath(getGitBashPaths()); |
| } |
|
|
| |
| |
| |
| export interface FileCheckResult { |
| path: string; |
| exists: boolean; |
| readable: boolean; |
| error?: string; |
| } |
|
|
| |
| |
| |
| export interface DirectoryCheckResult { |
| path: string; |
| exists: boolean; |
| readable: boolean; |
| entryCount: number; |
| error?: string; |
| } |
|
|
| |
| |
| |
| export interface ClaudeAuthIndicators { |
| hasCredentialsFile: boolean; |
| hasSettingsFile: boolean; |
| hasStatsCacheWithActivity: boolean; |
| hasProjectsSessions: boolean; |
| credentials: { |
| hasOAuthToken: boolean; |
| hasApiKey: boolean; |
| } | null; |
| |
| checks: { |
| settingsFile: FileCheckResult; |
| statsCache: FileCheckResult & { hasDailyActivity?: boolean }; |
| projectsDir: DirectoryCheckResult; |
| credentialFiles: FileCheckResult[]; |
| }; |
| } |
|
|
| export async function getClaudeAuthIndicators(): Promise<ClaudeAuthIndicators> { |
| const settingsPath = getClaudeSettingsPath(); |
| const statsCachePath = getClaudeStatsCachePath(); |
| const projectsDir = getClaudeProjectsDir(); |
| const credentialPaths = getClaudeCredentialPaths(); |
|
|
| |
| const settingsFileCheck: FileCheckResult = { |
| path: settingsPath, |
| exists: false, |
| readable: false, |
| }; |
|
|
| const statsCacheCheck: FileCheckResult & { hasDailyActivity?: boolean } = { |
| path: statsCachePath, |
| exists: false, |
| readable: false, |
| }; |
|
|
| const projectsDirCheck: DirectoryCheckResult = { |
| path: projectsDir, |
| exists: false, |
| readable: false, |
| entryCount: 0, |
| }; |
|
|
| const credentialFileChecks: FileCheckResult[] = credentialPaths.map((p) => ({ |
| path: p, |
| exists: false, |
| readable: false, |
| })); |
|
|
| const result: ClaudeAuthIndicators = { |
| hasCredentialsFile: false, |
| hasSettingsFile: false, |
| hasStatsCacheWithActivity: false, |
| hasProjectsSessions: false, |
| credentials: null, |
| checks: { |
| settingsFile: settingsFileCheck, |
| statsCache: statsCacheCheck, |
| projectsDir: projectsDirCheck, |
| credentialFiles: credentialFileChecks, |
| }, |
| }; |
|
|
| |
| |
| try { |
| if (await systemPathAccess(settingsPath)) { |
| settingsFileCheck.exists = true; |
| |
| try { |
| await systemPathReadFile(settingsPath); |
| settingsFileCheck.readable = true; |
| result.hasSettingsFile = true; |
| } catch (readErr) { |
| |
| settingsFileCheck.readable = false; |
| settingsFileCheck.error = `Cannot read: ${readErr instanceof Error ? readErr.message : String(readErr)}`; |
| } |
| } |
| } catch (err) { |
| settingsFileCheck.error = err instanceof Error ? err.message : String(err); |
| } |
|
|
| |
| try { |
| const statsContent = await systemPathReadFile(statsCachePath); |
| statsCacheCheck.exists = true; |
| statsCacheCheck.readable = true; |
| try { |
| const stats = JSON.parse(statsContent); |
| if (stats.dailyActivity && stats.dailyActivity.length > 0) { |
| statsCacheCheck.hasDailyActivity = true; |
| result.hasStatsCacheWithActivity = true; |
| } else { |
| statsCacheCheck.hasDailyActivity = false; |
| } |
| } catch (parseErr) { |
| statsCacheCheck.error = `JSON parse error: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`; |
| } |
| } catch (err) { |
| if ((err as NodeJS.ErrnoException).code === 'ENOENT') { |
| statsCacheCheck.exists = false; |
| } else { |
| statsCacheCheck.error = err instanceof Error ? err.message : String(err); |
| } |
| } |
|
|
| |
| try { |
| const sessions = await systemPathReaddir(projectsDir); |
| projectsDirCheck.exists = true; |
| projectsDirCheck.readable = true; |
| projectsDirCheck.entryCount = sessions.length; |
| if (sessions.length > 0) { |
| result.hasProjectsSessions = true; |
| } |
| } catch (err) { |
| if ((err as NodeJS.ErrnoException).code === 'ENOENT') { |
| projectsDirCheck.exists = false; |
| } else { |
| projectsDirCheck.error = err instanceof Error ? err.message : String(err); |
| } |
| } |
|
|
| |
| |
| |
| |
| for (let i = 0; i < credentialPaths.length; i++) { |
| const credPath = credentialPaths[i]; |
| const credCheck = credentialFileChecks[i]; |
| try { |
| const content = await systemPathReadFile(credPath); |
| credCheck.exists = true; |
| credCheck.readable = true; |
| try { |
| const credentials = JSON.parse(content); |
| |
| |
| |
| |
| const hasClaudeOauth = !!credentials.claudeAiOauth?.accessToken; |
| const hasLegacyOauth = !!(credentials.oauth_token || credentials.access_token); |
| const hasOAuthToken = hasClaudeOauth || hasLegacyOauth; |
| const hasApiKey = !!credentials.api_key; |
|
|
| |
| |
| |
| if (hasOAuthToken || hasApiKey) { |
| result.hasCredentialsFile = true; |
| result.credentials = { |
| hasOAuthToken, |
| hasApiKey, |
| }; |
| break; |
| } |
| |
| } catch (parseErr) { |
| credCheck.error = `JSON parse error: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`; |
| } |
| } catch (err) { |
| if ((err as NodeJS.ErrnoException).code === 'ENOENT') { |
| credCheck.exists = false; |
| } else { |
| credCheck.error = err instanceof Error ? err.message : String(err); |
| } |
| } |
| } |
|
|
| return result; |
| } |
|
|
| export interface CodexAuthIndicators { |
| hasAuthFile: boolean; |
| hasOAuthToken: boolean; |
| hasApiKey: boolean; |
| } |
|
|
| const CODEX_OAUTH_KEYS = ['access_token', 'oauth_token'] as const; |
| const CODEX_API_KEY_KEYS = ['api_key', 'OPENAI_API_KEY'] as const; |
|
|
| function hasNonEmptyStringField(record: Record<string, unknown>, keys: readonly string[]): boolean { |
| return keys.some((key) => typeof record[key] === 'string' && record[key]); |
| } |
|
|
| function getNestedTokens(record: Record<string, unknown>): Record<string, unknown> | null { |
| const tokens = record[CODEX_TOKENS_KEY]; |
| if (tokens && typeof tokens === 'object' && !Array.isArray(tokens)) { |
| return tokens as Record<string, unknown>; |
| } |
| return null; |
| } |
|
|
| export async function getCodexAuthIndicators(): Promise<CodexAuthIndicators> { |
| const result: CodexAuthIndicators = { |
| hasAuthFile: false, |
| hasOAuthToken: false, |
| hasApiKey: false, |
| }; |
|
|
| try { |
| const authContent = await systemPathReadFile(getCodexAuthPath()); |
| result.hasAuthFile = true; |
|
|
| try { |
| const authJson = JSON.parse(authContent) as Record<string, unknown>; |
| result.hasOAuthToken = hasNonEmptyStringField(authJson, CODEX_OAUTH_KEYS); |
| result.hasApiKey = hasNonEmptyStringField(authJson, CODEX_API_KEY_KEYS); |
| const nestedTokens = getNestedTokens(authJson); |
| if (nestedTokens) { |
| result.hasOAuthToken = |
| result.hasOAuthToken || hasNonEmptyStringField(nestedTokens, CODEX_OAUTH_KEYS); |
| result.hasApiKey = |
| result.hasApiKey || hasNonEmptyStringField(nestedTokens, CODEX_API_KEY_KEYS); |
| } |
| } catch { |
| |
| } |
| } catch { |
| |
| } |
|
|
| return result; |
| } |
|
|
| |
| |
| |
|
|
| const OPENCODE_DATA_DIR = '.local/share/opencode'; |
| const OPENCODE_AUTH_FILENAME = 'auth.json'; |
| const OPENCODE_TOKENS_KEY = 'tokens'; |
|
|
| |
| |
| |
| export function getOpenCodeCliPaths(): string[] { |
| const isWindows = process.platform === 'win32'; |
| const homeDir = os.homedir(); |
|
|
| if (isWindows) { |
| const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'); |
| const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'); |
| return [ |
| |
| path.join(homeDir, '.opencode', 'bin', 'opencode.exe'), |
| path.join(homeDir, '.local', 'bin', 'opencode.exe'), |
| path.join(appData, 'npm', 'opencode.cmd'), |
| path.join(appData, 'npm', 'opencode'), |
| path.join(appData, '.npm-global', 'bin', 'opencode.cmd'), |
| path.join(appData, '.npm-global', 'bin', 'opencode'), |
| |
| path.join(homeDir, '.volta', 'bin', 'opencode.exe'), |
| |
| path.join(localAppData, 'pnpm', 'opencode.cmd'), |
| path.join(localAppData, 'pnpm', 'opencode'), |
| |
| path.join(homeDir, 'go', 'bin', 'opencode.exe'), |
| path.join(process.env.GOPATH || path.join(homeDir, 'go'), 'bin', 'opencode.exe'), |
| ...getNvmWindowsCliPaths('opencode'), |
| ]; |
| } |
|
|
| |
| const nvmBinPaths = getNvmBinPaths().map((binPath) => path.join(binPath, 'opencode')); |
|
|
| |
| const fnmBinPaths = getFnmBinPaths().map((binPath) => path.join(binPath, 'opencode')); |
|
|
| |
| const pnpmHome = process.env.PNPM_HOME || path.join(homeDir, '.local', 'share', 'pnpm'); |
|
|
| return [ |
| |
| path.join(homeDir, '.opencode', 'bin', 'opencode'), |
| |
| path.join(homeDir, '.local', 'bin', 'opencode'), |
| '/opt/homebrew/bin/opencode', |
| '/usr/local/bin/opencode', |
| '/usr/bin/opencode', |
| path.join(homeDir, '.npm-global', 'bin', 'opencode'), |
| |
| '/home/linuxbrew/.linuxbrew/bin/opencode', |
| |
| path.join(homeDir, '.volta', 'bin', 'opencode'), |
| |
| path.join(pnpmHome, 'opencode'), |
| |
| path.join(homeDir, '.yarn', 'bin', 'opencode'), |
| path.join(homeDir, '.config', 'yarn', 'global', 'node_modules', '.bin', 'opencode'), |
| |
| path.join(homeDir, 'go', 'bin', 'opencode'), |
| path.join(process.env.GOPATH || path.join(homeDir, 'go'), 'bin', 'opencode'), |
| |
| '/snap/bin/opencode', |
| |
| ...nvmBinPaths, |
| |
| ...fnmBinPaths, |
| ]; |
| } |
|
|
| |
| |
| |
| |
| |
| export function getOpenCodeConfigDir(): string { |
| return path.join(os.homedir(), OPENCODE_DATA_DIR); |
| } |
|
|
| |
| |
| |
| export function getOpenCodeAuthPath(): string { |
| return path.join(getOpenCodeConfigDir(), OPENCODE_AUTH_FILENAME); |
| } |
|
|
| |
| |
| |
| export async function findOpenCodeCliPath(): Promise<string | null> { |
| return findFirstExistingPath(getOpenCodeCliPaths()); |
| } |
|
|
| export interface OpenCodeAuthIndicators { |
| hasAuthFile: boolean; |
| hasOAuthToken: boolean; |
| hasApiKey: boolean; |
| } |
|
|
| const OPENCODE_OAUTH_KEYS = ['access_token', 'oauth_token'] as const; |
| const OPENCODE_API_KEY_KEYS = ['api_key', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY'] as const; |
|
|
| |
| |
| const OPENCODE_PROVIDERS = [ |
| 'anthropic', |
| 'openai', |
| 'google', |
| 'bedrock', |
| 'amazon-bedrock', |
| 'github-copilot', |
| 'copilot', |
| ] as const; |
|
|
| function getOpenCodeNestedTokens(record: Record<string, unknown>): Record<string, unknown> | null { |
| const tokens = record[OPENCODE_TOKENS_KEY]; |
| if (tokens && typeof tokens === 'object' && !Array.isArray(tokens)) { |
| return tokens as Record<string, unknown>; |
| } |
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| function hasProviderOAuth(authJson: Record<string, unknown>): boolean { |
| for (const provider of OPENCODE_PROVIDERS) { |
| const providerAuth = authJson[provider]; |
| if (providerAuth && typeof providerAuth === 'object' && !Array.isArray(providerAuth)) { |
| const auth = providerAuth as Record<string, unknown>; |
| |
| if (auth.type === 'oauth') { |
| if ( |
| (typeof auth.access === 'string' && auth.access) || |
| (typeof auth.refresh === 'string' && auth.refresh) |
| ) { |
| return true; |
| } |
| } |
| |
| if (typeof auth.access_token === 'string' && auth.access_token) { |
| return true; |
| } |
| |
| if (typeof auth.refresh_token === 'string' && auth.refresh_token) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
|
|
| |
| |
| |
| function hasProviderApiKey(authJson: Record<string, unknown>): boolean { |
| for (const provider of OPENCODE_PROVIDERS) { |
| const providerAuth = authJson[provider]; |
| if (providerAuth && typeof providerAuth === 'object' && !Array.isArray(providerAuth)) { |
| const auth = providerAuth as Record<string, unknown>; |
| |
| if (auth.type === 'api_key' && typeof auth.key === 'string' && auth.key) { |
| return true; |
| } |
| |
| if (typeof auth.api_key === 'string' && auth.api_key) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
|
|
| |
| |
| |
| export async function getOpenCodeAuthIndicators(): Promise<OpenCodeAuthIndicators> { |
| const result: OpenCodeAuthIndicators = { |
| hasAuthFile: false, |
| hasOAuthToken: false, |
| hasApiKey: false, |
| }; |
|
|
| try { |
| const authContent = await systemPathReadFile(getOpenCodeAuthPath()); |
| result.hasAuthFile = true; |
|
|
| try { |
| const authJson = JSON.parse(authContent) as Record<string, unknown>; |
|
|
| |
| result.hasOAuthToken = hasNonEmptyStringField(authJson, OPENCODE_OAUTH_KEYS); |
| result.hasApiKey = hasNonEmptyStringField(authJson, OPENCODE_API_KEY_KEYS); |
|
|
| |
| const nestedTokens = getOpenCodeNestedTokens(authJson); |
| if (nestedTokens) { |
| result.hasOAuthToken = |
| result.hasOAuthToken || hasNonEmptyStringField(nestedTokens, OPENCODE_OAUTH_KEYS); |
| result.hasApiKey = |
| result.hasApiKey || hasNonEmptyStringField(nestedTokens, OPENCODE_API_KEY_KEYS); |
| } |
|
|
| |
| |
| result.hasOAuthToken = result.hasOAuthToken || hasProviderOAuth(authJson); |
| result.hasApiKey = result.hasApiKey || hasProviderApiKey(authJson); |
| } catch { |
| |
| } |
| } catch { |
| |
| } |
|
|
| return result; |
| } |
|
|