/ src / electron-apps.ts
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  }