@openagents-org/agent-launcher 0.2.152 → 0.2.153

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.152",
3
+ "version": "0.2.153",
4
4
  "description": "OpenAgents Launcher \u2014 install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/installer.js CHANGED
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const { execSync, exec } = require('child_process');
7
- const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache, aiderBinDirs } = require('./paths');
7
+ const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache, aiderBinDirs, resolveBinaryInKnownDirs } = require('./paths');
8
8
  const { EnvManager } = require('./env');
9
9
  const { readinessReason, REASON } = require('./adapters/health-status');
10
10
 
@@ -394,6 +394,44 @@ class Installer {
394
394
  return found ? { path: found } : null;
395
395
  }
396
396
 
397
+ /**
398
+ * Confirm a real cursor-agent binary landed, for verify-before-mark.
399
+ *
400
+ * Cursor's Windows one-liner (`irm 'https://cursor.com/install?win32=true' |
401
+ * iex`) downloads the CLI with Invoke-WebRequest. When that download fails —
402
+ * an "unexpected EOF or 0 bytes" IOException is what users behind a filtering
403
+ * network get — PowerShell treats it as NON-terminating: the script carries
404
+ * on, prints "Start using Cursor Agent" and "Happy coding!", and exits 0. All
405
+ * that's left on disk is an empty %LOCALAPPDATA%\cursor-agent\versions\.
406
+ *
407
+ * Same defect hermes and amp already guard against here. Without this the
408
+ * marker gets written, the launcher reports Cursor installed, offers a
409
+ * sign-in, and the only way the user finds out is a terminal saying
410
+ * "'cursor-agent' is not recognized".
411
+ */
412
+ _verifyCursorBinary() {
413
+ try { clearBinaryLookupCache(); } catch {}
414
+ const resolved = this._whichBinary('cursor');
415
+ if (resolved && fs.existsSync(resolved)) return { path: resolved };
416
+ return null;
417
+ }
418
+
419
+ _cursorBinaryNotFoundMessage() {
420
+ const localAppData =
421
+ process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
422
+ const isWin = process.platform === 'win32';
423
+ const expected = isWin
424
+ ? path.join(localAppData, 'cursor-agent')
425
+ : path.join(os.homedir(), '.local', 'bin', 'cursor-agent');
426
+ return (
427
+ 'Cursor install command completed, but the cursor-agent binary could not be found\n' +
428
+ "(its installer prints \"Happy coding!\" and exits 0 even when the download failed —\n" +
429
+ 'look for "unexpected EOF" or a network error in the log above).\n\n' +
430
+ `Expected under:\n${expected}\n\n` +
431
+ 'Retry the install, or check that cursor.com is reachable from this network.'
432
+ );
433
+ }
434
+
397
435
  _hermesBinaryNotFoundMessage() {
398
436
  const isWin = process.platform === 'win32';
399
437
  const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
@@ -1066,6 +1104,18 @@ class Installer {
1066
1104
  }
1067
1105
  if (onData) onData(`\nHermes CLI resolved: ${hermes.path}\n`);
1068
1106
  }
1107
+ // Cursor-only: same failure shape as hermes above — its installer
1108
+ // exits 0 after a failed download. See _verifyCursorBinary.
1109
+ if (agentType === 'cursor') {
1110
+ const cursor = this._verifyCursorBinary();
1111
+ if (!cursor) {
1112
+ const msg = this._cursorBinaryNotFoundMessage();
1113
+ if (onData) onData(`\n${msg}\n`);
1114
+ reject(new Error(msg));
1115
+ return;
1116
+ }
1117
+ if (onData) onData(`\nCursor CLI resolved: ${cursor.path}\n`);
1118
+ }
1069
1119
  this._markInstalled(agentType);
1070
1120
  if (onData) onData(`\nDone! ${agentType} is now installed.\n`);
1071
1121
  resolve({ success: true, command: displayCmd });
@@ -1296,7 +1346,14 @@ class Installer {
1296
1346
  // runnable. Generic and backward-compatible — only runs after PATH misses.
1297
1347
  const pkgBin = this._resolvePackageBin(agentType, entry, binary);
1298
1348
  if (pkgBin) return pkgBin;
1299
- return null;
1349
+ // Last: check the known install dirs on disk. A PATH lookup misses a CLI
1350
+ // whose installer only edited the *registry* PATH (Cursor, Amp, Hermes on
1351
+ // Windows) until the process restarts, and the launcher — which resolves
1352
+ // through here — then reports a working install as missing and offers a
1353
+ // terminal running a bare command that cannot resolve either. Each adapter
1354
+ // already does this search; this puts it in the one place the launcher
1355
+ // actually calls.
1356
+ return resolveBinaryInKnownDirs([binary, ...aliases], agentType);
1300
1357
  }
1301
1358
 
1302
1359
  /**
package/src/paths.js CHANGED
@@ -517,12 +517,69 @@ function aiderBinDirs() {
517
517
  return dirs;
518
518
  }
519
519
 
520
+ /** Executable suffixes to try for a bare binary name, per platform. */
521
+ const BIN_EXTS = IS_WINDOWS ? ['.cmd', '.exe', '.bat', ''] : [''];
522
+
523
+ /**
524
+ * Last-resort binary lookup: walk the directories we already know agent CLIs
525
+ * land in and check the filesystem directly, instead of asking the shell.
526
+ *
527
+ * `where`/`which` is not enough on Windows. The Cursor, Amp and Hermes
528
+ * installers edit the *registry* PATH, which an already-running process never
529
+ * inherits — so a perfectly good install is invisible to a PATH lookup until
530
+ * the machine (or at least the app) is restarted. Every adapter grew its own
531
+ * tiered search to cope; the launcher had none, so it alone reported "can't
532
+ * find it" and then opened a terminal running a bare command that could only
533
+ * fail with "'cursor-agent' is not recognized".
534
+ *
535
+ * Deliberately reuses getExtraBinDirs() rather than a second hardcoded list:
536
+ * that list already IS the curated set, and a copy of it is exactly how the
537
+ * launcher and the adapters drifted apart in the first place.
538
+ *
539
+ * Runs only after a PATH lookup has already missed, so it can add resolutions
540
+ * but never change one that already works.
541
+ *
542
+ * @param {string|string[]} names binary name(s), e.g. ['cursor-agent','agent']
543
+ * @param {string} [agentType] adds that agent's isolated runtime bin dir
544
+ * @returns {string|null} absolute path, or null
545
+ */
546
+ function resolveBinaryInKnownDirs(names, agentType) {
547
+ const list = (Array.isArray(names) ? names : [names]).filter(Boolean);
548
+ if (!list.length) return null;
549
+
550
+ const dirs = [];
551
+ const push = (d) => { if (d && !dirs.includes(d)) dirs.push(d); };
552
+
553
+ // The agent's own isolated runtime first — it is the copy this launcher
554
+ // installed, and should win over anything the user happens to have globally.
555
+ if (agentType) push(path.join(getRuntimePrefix(agentType), 'node_modules', '.bin'));
556
+ push(path.join(HOME, '.openagents', 'nodejs', 'node_modules', '.bin'));
557
+ try { push(path.dirname(process.execPath)); } catch {}
558
+ for (const d of getExtraBinDirs()) push(d);
559
+
560
+ for (const dir of dirs) {
561
+ for (const name of list) {
562
+ for (const ext of BIN_EXTS) {
563
+ const candidate = path.join(dir, `${name}${ext}`);
564
+ try {
565
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
566
+ } catch {
567
+ /* unreadable path — keep looking */
568
+ }
569
+ }
570
+ }
571
+ }
572
+ return null;
573
+ }
574
+
575
+
520
576
  module.exports = {
521
577
  getExtraBinDirs,
522
578
  getEnhancedPATH,
523
579
  getEnhancedEnv,
524
580
  whichBinary,
525
581
  whereBinary,
582
+ resolveBinaryInKnownDirs,
526
583
  clearBinaryLookupCache,
527
584
  getRuntimePrefix,
528
585
  getCorePrefix,