@openagents-org/agent-launcher 0.2.148 → 0.2.150

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.148",
3
+ "version": "0.2.150",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/installer.js CHANGED
@@ -471,7 +471,7 @@ class Installer {
471
471
  // Prefer the resolved absolute binary path (quoted) over the bare name:
472
472
  // it's unambiguous and lets `--version` succeed for binaries resolved via
473
473
  // the package-bin fallback (which are not on PATH).
474
- const versionCmd = checkCmd || `"${binary}" --version`;
474
+ const versionCmd = checkCmd || this._versionProbeCommand(binary);
475
475
 
476
476
  const version = this._detectVersion(binary, versionCmd);
477
477
  const readiness = this._evaluateReadiness(agentType, entry, binary);
@@ -504,6 +504,56 @@ class Installer {
504
504
  };
505
505
  }
506
506
 
507
+ /**
508
+ * Build the `<binary> --version` probe command for a resolved binary path.
509
+ *
510
+ * When the resolved binary is a JS entry (`.mjs`/`.cjs`/`.js` — e.g. OpenClaw,
511
+ * whose package `bin` is `openclaw.mjs` and is what `_resolvePackageBin`
512
+ * returns when the `.cmd` shim isn't on PATH), it MUST be run through node.
513
+ * execSync goes through a shell; on Windows `cmd.exe "C:\…\openclaw.mjs"
514
+ * --version` finds no PATHEXT association for `.mjs`, so it ShellExecutes the
515
+ * file and the OS pops the "choose an app to open this .mjs file" dialog. The
516
+ * launcher polls health/version on a timer, so that dialog reappears over and
517
+ * over — even at idle, and stacked (one per poll). Prefixing node makes the
518
+ * probe run headless. Harmless on Unix (node runs a .mjs regardless of any
519
+ * shebang), so it's applied on every platform for consistency.
520
+ */
521
+ _versionProbeCommand(binary) {
522
+ if (/\.(mjs|cjs|js)$/i.test(binary)) {
523
+ return `"${this._nodeBinary()}" "${binary}" --version`;
524
+ }
525
+ return `"${binary}" --version`;
526
+ }
527
+
528
+ /**
529
+ * Resolve a node executable to run JS-entry CLIs with. Prefers the launcher's
530
+ * portable node, then a PATH lookup, then the bare name. Deliberately does NOT
531
+ * use process.execPath: when the core runs inside the Electron main process
532
+ * that path is the Electron binary, and `electron foo.mjs` would launch the
533
+ * app instead of running the script.
534
+ */
535
+ _nodeBinary() {
536
+ const isWin = process.platform === 'win32';
537
+ const nodeName = isWin ? 'node.exe' : 'node';
538
+ const portableDir = path.join(os.homedir(), '.openagents', 'nodejs');
539
+ for (const candidate of [
540
+ path.join(portableDir, nodeName),
541
+ path.join(portableDir, 'bin', nodeName),
542
+ ]) {
543
+ if (fs.existsSync(candidate)) return candidate;
544
+ }
545
+ try {
546
+ const found = execSync(isWin ? 'where node.exe' : 'which node', {
547
+ encoding: 'utf-8',
548
+ timeout: 5000,
549
+ stdio: ['pipe', 'pipe', 'pipe'],
550
+ env: getEnhancedEnv(),
551
+ }).split(/\r?\n/)[0].trim();
552
+ if (found) return found;
553
+ } catch {}
554
+ return 'node';
555
+ }
556
+
507
557
  /**
508
558
  * Detect a binary's version string, cached briefly. Returns null on failure.
509
559
  */
@@ -115,6 +115,44 @@ function npmPrefixFromBin(npmBin, platform = process.platform) {
115
115
  return platform === 'win32' ? binDir : p.dirname(binDir);
116
116
  }
117
117
 
118
+ /**
119
+ * Detect when the running launcher lives in an *isolated runtime* — the local
120
+ * npm project install.sh creates at `~/.openagents/nodejs`, where the package
121
+ * is at `<dir>/node_modules/@openagents-org/agent-launcher` and `<dir>` has its
122
+ * own `package.json`.
123
+ *
124
+ * This matters because a `-g` install (what a normal global/nvm install needs)
125
+ * lands in `<dir>/lib/node_modules`, but the isolated runtime's daemon loads
126
+ * from `<dir>/node_modules` — so `agn update` would report success while the
127
+ * running code never changed (the acen "update no-op" incident). For an
128
+ * isolated runtime we must do a *local* install into `<dir>` instead.
129
+ *
130
+ * Returns the project dir (`<dir>`) for an isolated runtime, else null (caller
131
+ * falls back to the global `-g` path). Global/nvm layouts are excluded: their
132
+ * package sits under `.../lib/node_modules` (basename 'lib') and the prefix
133
+ * root has no package.json.
134
+ */
135
+ function isolatedRuntimeDir(opts = {}) {
136
+ const dir = opts.dir || __dirname; // <pkgRoot>/src
137
+ const exists = opts.exists || fs.existsSync;
138
+ // Path impl is injectable so tests can force POSIX/Windows semantics
139
+ // regardless of the host OS (mirrors npmPrefixFromBin's `platform`).
140
+ const p = opts.path || path;
141
+ try {
142
+ const pkgRoot = p.resolve(dir, '..'); // .../agent-launcher
143
+ const nodeModules = p.resolve(pkgRoot, '..', '..'); // .../node_modules
144
+ if (p.basename(nodeModules) !== 'node_modules') return null;
145
+ const projectDir = p.resolve(nodeModules, '..'); // dir containing node_modules
146
+ // Global npm layout is <prefix>/lib/node_modules — not an isolated project.
147
+ if (p.basename(projectDir) === 'lib') return null;
148
+ // A real local project has its own package.json; a global prefix does not.
149
+ if (!exists(p.join(projectDir, 'package.json'))) return null;
150
+ return projectDir;
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
155
+
118
156
  /**
119
157
  * Locate the npm executable next to the running node binary, falling back to
120
158
  * a PATH lookup. On Windows the runnable file is `npm.cmd`; the extensionless
@@ -150,15 +188,28 @@ function runUpdate(opts = {}) {
150
188
  const platform = opts.platform || process.platform;
151
189
  const spawn = opts.spawn || spawnSync;
152
190
  const npmBin = opts.npmBin || findNpmBin({ platform });
153
- const prefix = opts.prefix !== undefined ? opts.prefix : npmPrefixFromBin(npmBin, platform);
154
- // Use a GLOBAL install (-g). A LOCAL install (`npm install <pkg> --prefix
155
- // <dir>`) treats <dir> as a project root and prunes every node_modules entry
156
- // not reachable from its package.json. When the prefix is the portable node
157
- // dir, that deletes node's own bundled npm/corepack (and other agent runtimes),
158
- // breaking every subsequent `agn` command. A global install only adds/updates
159
- // the named package into <prefix>/node_modules and never prunes its siblings.
160
- const args = ['install', '-g', `${PKG_NAME}@latest`];
161
- if (prefix) args.push('--prefix', prefix);
191
+ const isoDir = opts.isolatedDir !== undefined ? opts.isolatedDir : isolatedRuntimeDir();
192
+
193
+ let args;
194
+ if (isoDir) {
195
+ // Isolated runtime (install.sh's ~/.openagents/nodejs): a LOCAL npm project
196
+ // whose package lives in <dir>/node_modules — NOT <dir>/lib/node_modules.
197
+ // A `-g` install lands in lib/node_modules and silently no-ops the running
198
+ // launcher/daemon (which loads from node_modules). Install locally into the
199
+ // project dir instead. `--no-save` leaves package.json untouched (install.sh
200
+ // owns it), and because <dir>'s package.json already lists our deps npm has
201
+ // nothing extraneous to prune.
202
+ args = ['install', '--no-save', `${PKG_NAME}@latest`, '--prefix', isoDir];
203
+ } else {
204
+ // Global / nvm install. A LOCAL install (`npm install <pkg> --prefix <dir>`)
205
+ // here would treat <dir> as a project root and prune every node_modules
206
+ // entry not reachable from its package.json — deleting node's own bundled
207
+ // npm/corepack. A global install only adds/updates the named package into
208
+ // <prefix>/node_modules and never prunes its siblings.
209
+ const prefix = opts.prefix !== undefined ? opts.prefix : npmPrefixFromBin(npmBin, platform);
210
+ args = ['install', '-g', `${PKG_NAME}@latest`];
211
+ if (prefix) args.push('--prefix', prefix);
212
+ }
162
213
 
163
214
  // On Windows, npm.cmd is a batch file and must be invoked through cmd.exe.
164
215
  // Passing the args via cmd.exe with shell:false lets Node quote paths that
@@ -240,5 +291,6 @@ module.exports = {
240
291
  runUpdate,
241
292
  findNpmBin,
242
293
  npmPrefixFromBin,
294
+ isolatedRuntimeDir,
243
295
  currentVersion,
244
296
  };