electron-apps.ts
1 /** 2 * Electron app registry — maps site names to launch metadata. 3 * 4 * Builtin apps are defined here. User-defined apps are loaded 5 * from ~/.opencli/apps.yaml (additive only, does not override builtins). 6 */ 7 8 import * as fs from 'node:fs'; 9 import * as path from 'node:path'; 10 import * as os from 'node:os'; 11 import yaml from 'js-yaml'; 12 13 export interface ElectronAppEntry { 14 /** CDP debug port (unique per app) */ 15 port: number; 16 /** macOS process name for detection via pgrep */ 17 processName: string; 18 /** Candidate executable names inside Contents/MacOS/, tried in order */ 19 executableNames?: string[]; 20 /** macOS bundle ID for path discovery */ 21 bundleId?: string; 22 /** Human-readable name for prompts */ 23 displayName?: string; 24 /** Additional launch args beyond --remote-debugging-port */ 25 extraArgs?: string[]; 26 } 27 28 export const builtinApps: Record<string, ElectronAppEntry> = { 29 cursor: { port: 9226, processName: 'Cursor', bundleId: 'com.todesktop.runtime.Cursor', displayName: 'Cursor' }, 30 codex: { port: 9222, processName: 'Codex', bundleId: 'com.openai.codex', displayName: 'Codex' }, 31 chatwise: { port: 9228, processName: 'ChatWise', bundleId: 'com.chatwise.app', displayName: 'ChatWise' }, 32 notion: { port: 9230, processName: 'Notion', bundleId: 'notion.id', displayName: 'Notion' }, 33 'discord-app': { port: 9232, processName: 'Discord', bundleId: 'com.discord.app', displayName: 'Discord' }, 34 'doubao-app': { port: 9225, processName: 'Doubao', bundleId: 'com.volcengine.doubao', displayName: 'Doubao' }, 35 antigravity: { 36 port: 9234, 37 processName: 'Antigravity', 38 executableNames: ['Electron', 'Antigravity'], 39 bundleId: 'dev.antigravity.app', 40 displayName: 'Antigravity', 41 }, 42 'chatgpt-app': { port: 9236, processName: 'ChatGPT', bundleId: 'com.openai.chat', displayName: 'ChatGPT' }, 43 }; 44 45 /** Merge builtin + user-defined apps. User entries are additive only. */ 46 export function loadApps( 47 userApps?: Record<string, Omit<ElectronAppEntry, 'displayName'> & { displayName?: string }>, 48 ): Record<string, ElectronAppEntry> { 49 const merged = { ...builtinApps }; 50 if (userApps) { 51 for (const [name, entry] of Object.entries(userApps)) { 52 if (!(name in merged)) { 53 merged[name] = entry as ElectronAppEntry; 54 } 55 } 56 } 57 return merged; 58 } 59 60 let _apps: Record<string, ElectronAppEntry> | null = null; 61 62 function ensureLoaded(): Record<string, ElectronAppEntry> { 63 if (_apps) return _apps; 64 65 let userApps: Record<string, ElectronAppEntry> | undefined; 66 try { 67 const yamlPath = path.join(os.homedir(), '.opencli', 'apps.yaml'); 68 if (fs.existsSync(yamlPath)) { 69 const content = fs.readFileSync(yamlPath, 'utf-8'); 70 const parsed = yaml.load(content) as { apps?: Record<string, ElectronAppEntry> }; 71 userApps = parsed?.apps; 72 } 73 } catch { 74 // Silently ignore malformed user config 75 } 76 77 _apps = loadApps(userApps); 78 return _apps; 79 } 80 81 export function getElectronApp(site: string): ElectronAppEntry | undefined { 82 return ensureLoaded()[site]; 83 } 84 85 export function isElectronApp(site: string): boolean { 86 return site in ensureLoaded(); 87 } 88 89 /** Get all registered apps (builtin + user-defined). */ 90 export function getAllElectronApps(): Record<string, ElectronAppEntry> { 91 return ensureLoaded(); 92 } 93 94 /** Reset loaded apps (for testing). */ 95 export function _resetRegistry(): void { 96 _apps = null; 97 }