/ src / main.ts
main.ts
  1  #!/usr/bin/env node
  2  /**
  3   * opencli — Make any website your CLI. AI-powered.
  4   */
  5  
  6  // Ensure standard system paths are available for child processes.
  7  // Some environments (GUI apps, cron, IDE terminals) launch with a minimal PATH
  8  // that excludes /usr/local/bin, /usr/sbin, etc., causing external CLIs to fail.
  9  if (process.platform !== 'win32') {
 10    const std = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
 11    const cur = new Set((process.env.PATH ?? '').split(':').filter(Boolean));
 12    for (const p of std) cur.add(p);
 13    process.env.PATH = [...cur].join(':');
 14  }
 15  
 16  import * as fs from 'node:fs';
 17  import * as os from 'node:os';
 18  import * as path from 'node:path';
 19  import { fileURLToPath } from 'node:url';
 20  import { getCompletionsFromManifest, hasAllManifests, printCompletionScriptFast } from './completion-fast.js';
 21  import { findPackageRoot, getCliManifestPath } from './package-paths.js';
 22  import { PKG_VERSION } from './version.js';
 23  import { EXIT_CODES } from './errors.js';
 24  
 25  const __filename = fileURLToPath(import.meta.url);
 26  const __dirname = path.dirname(__filename);
 27  // Adapters are JS-first and live at <package-root>/clis/.
 28  // Use findPackageRoot so the path works both in dev (src/main.ts) and prod (dist/src/main.js).
 29  const BUILTIN_CLIS = path.join(findPackageRoot(__filename), 'clis');
 30  const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
 31  
 32  // ── Session lifecycle flags ──────────────────────────────────────────────
 33  // `--live` / `--focus` are top-level-ish toggles that tweak the automation
 34  // window's lifecycle. We strip them from argv before Commander runs so they
 35  // can be placed anywhere and work on any subcommand (adapter or browser).
 36  {
 37    const liveIdx = process.argv.indexOf('--live');
 38    if (liveIdx !== -1) {
 39      process.env.OPENCLI_LIVE = '1';
 40      process.argv.splice(liveIdx, 1);
 41    }
 42    const focusIdx = process.argv.indexOf('--focus');
 43    if (focusIdx !== -1) {
 44      process.env.OPENCLI_WINDOW_FOCUSED = '1';
 45      process.argv.splice(focusIdx, 1);
 46    }
 47  }
 48  
 49  // ── Ultra-fast path: lightweight commands bypass full discovery ──────────
 50  // These are high-frequency or trivial paths that must not pay the startup tax.
 51  const argv = process.argv.slice(2);
 52  
 53  // Fast path: --version (only when it's the top-level intent, not passed to a subcommand)
 54  // e.g. `opencli --version` or `opencli -V`, but NOT `opencli gh --version`
 55  if (argv[0] === '--version' || argv[0] === '-V') {
 56    process.stdout.write(PKG_VERSION + '\n');
 57    process.exit(EXIT_CODES.SUCCESS);
 58  }
 59  
 60  // Fast path: completion <shell> — print shell script without discovery
 61  if (argv[0] === 'completion' && argv.length >= 2) {
 62    if (printCompletionScriptFast(argv[1])) {
 63      process.exit(EXIT_CODES.SUCCESS);
 64    }
 65    // Unknown shell — fall through to full path for proper error handling
 66  }
 67  
 68  // Fast path: --get-completions — read from manifest, skip discovery
 69  const getCompIdx = process.argv.indexOf('--get-completions');
 70  if (getCompIdx !== -1) {
 71    // Only include manifests that actually exist on disk.
 72    // With sparse override, the user clis dir may exist but have no manifest.
 73    const manifestPaths = [getCliManifestPath(BUILTIN_CLIS)];
 74    const userManifest = getCliManifestPath(USER_CLIS);
 75    try { fs.accessSync(userManifest); manifestPaths.push(userManifest); } catch { /* no user manifest */ }
 76    if (hasAllManifests(manifestPaths)) {
 77      const rest = process.argv.slice(getCompIdx + 1);
 78      let cursor: number | undefined;
 79      const words: string[] = [];
 80      for (let i = 0; i < rest.length; i++) {
 81        if (rest[i] === '--cursor' && i + 1 < rest.length) {
 82          cursor = parseInt(rest[i + 1], 10);
 83          i++;
 84        } else {
 85          words.push(rest[i]);
 86        }
 87      }
 88      if (cursor === undefined) cursor = words.length;
 89      const candidates = getCompletionsFromManifest(words, cursor, manifestPaths);
 90      process.stdout.write(candidates.join('\n') + '\n');
 91      process.exit(EXIT_CODES.SUCCESS);
 92    }
 93    // No manifest — fall through to full discovery path below
 94  }
 95  
 96  // ── Full startup path ───────────────────────────────────────────────────
 97  // Dynamic imports: these are deferred so the fast path above never pays the cost.
 98  const { discoverClis, discoverPlugins, ensureUserCliCompatShims, ensureUserAdapters } = await import('./discovery.js');
 99  const { getCompletions } = await import('./completion.js');
100  const { runCli } = await import('./cli.js');
101  const { emitHook } = await import('./hooks.js');
102  const { installNodeNetwork } = await import('./node-network.js');
103  const { registerUpdateNoticeOnExit, checkForUpdateBackground } = await import('./update-check.js');
104  
105  installNodeNetwork();
106  
107  // Parallelise independent startup I/O:
108  //  - Built-in adapter discovery has no dependency on user-dir setup.
109  //  - ensureUserCliCompatShims and ensureUserAdapters operate on different paths
110  //    (~/.opencli/node_modules/ vs ~/.opencli/clis/ + adapter-manifest.json).
111  //  - registerCommand() overwrites on name collision (see registry.ts), so
112  //    user-CLI discovery MUST run after built-in discovery to preserve the
113  //    intended override order (user adapters override built-in ones).
114  //  - discoverPlugins runs last: plugins may override both built-in and user CLIs.
115  const [, ,] = await Promise.all([
116    ensureUserCliCompatShims(),
117    ensureUserAdapters(),
118    discoverClis(BUILTIN_CLIS),
119  ]);
120  await discoverClis(USER_CLIS);
121  await discoverPlugins();
122  
123  // Register exit hook: notice appears after command output (same as npm/gh/yarn)
124  registerUpdateNoticeOnExit();
125  // Kick off background fetch for next run (non-blocking)
126  checkForUpdateBackground();
127  
128  // ── Fallback completion: manifest unavailable, use full registry ─────────
129  if (getCompIdx !== -1) {
130    const rest = process.argv.slice(getCompIdx + 1);
131    let cursor: number | undefined;
132    const words: string[] = [];
133    for (let i = 0; i < rest.length; i++) {
134      if (rest[i] === '--cursor' && i + 1 < rest.length) {
135        cursor = parseInt(rest[i + 1], 10);
136        i++;
137      } else {
138        words.push(rest[i]);
139      }
140    }
141    if (cursor === undefined) cursor = words.length;
142    const candidates = getCompletions(words, cursor);
143    process.stdout.write(candidates.join('\n') + '\n');
144    process.exit(EXIT_CODES.SUCCESS);
145  }
146  
147  await emitHook('onStartup', { command: '__startup__', args: {} });
148  runCli(BUILTIN_CLIS, USER_CLIS);