@openagents-org/agent-launcher 0.2.183 → 0.2.185

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/src/wsl.js CHANGED
@@ -27,20 +27,54 @@
27
27
  * The mirror direction needs no marker: a path ending in .exe/.cmd/.bat while
28
28
  * we are running inside a distro is unambiguously a Windows binary.
29
29
  *
30
- * Cost: resolution never spawns wsl.exe per binary. One probe per process
31
- * (30s TTL) reads the distro's login PATH, and every later lookup is a
32
- * filesystem check through the \\wsl.localhost UNC mount — which Windows serves
33
- * without starting a distro session.
30
+ * Background resolution checks whether the default distro is already running
31
+ * before probing its login PATH or UNC mount. A stopped distro is left stopped;
32
+ * after the user starts it, the next scan can detect its CLIs. One PATH probe
33
+ * (5 min TTL) then serves later lookups through the \\wsl.localhost UNC mount.
34
+ *
35
+ * Inside the desktop app's main process none of that may block: starting
36
+ * wsl.exe can mean booting the WSL VM, and the first touch of the UNC mount
37
+ * waits on it too — seconds during which the window is frozen. There every
38
+ * probe answers from its cache and refreshes in the background (probe-mode.js).
34
39
  */
35
40
 
36
41
  'use strict';
37
42
 
38
43
  const fs = require('fs');
39
- const { spawn: nodeSpawn, execFileSync } = require('child_process');
44
+ const { spawn: nodeSpawn, execFile, execFileSync } = require('child_process');
45
+ const { canBlock } = require('./probe-mode');
40
46
 
41
47
  const IS_WINDOWS = process.platform === 'win32';
42
48
  const PROBE_TIMEOUT_MS = 10000;
49
+ /** Which binaries were found. Short: an install inside the distro should show up. */
43
50
  const CACHE_TTL_MS = 30 * 1000;
51
+ /**
52
+ * The distro list, its UNC root and its login PATH describe the machine, not
53
+ * any one install, and each costs a wsl.exe start — re-asking every 30s kept
54
+ * the VM (and, in the desktop app, the window) busy for nothing.
55
+ */
56
+ const MACHINE_TTL_MS = 5 * 60 * 1000;
57
+ // A stopped distro must never be woken by background detection. Keep this
58
+ // short so a distro the user starts becomes visible on the next scan.
59
+ const RUNNING_TTL_MS = 2000;
60
+
61
+ function _fresh(cache, ttl) {
62
+ return !!cache.at && Date.now() - cache.at < ttl;
63
+ }
64
+
65
+ /** execFile as a promise of stdout, or null on any failure. stdin is closed. */
66
+ function _execFileOut(file, argv, opts) {
67
+ return new Promise((resolve) => {
68
+ try {
69
+ const child = execFile(file, argv, { windowsHide: true, timeout: PROBE_TIMEOUT_MS, ...opts }, (err, stdout) => {
70
+ resolve(err ? null : stdout);
71
+ });
72
+ if (child.stdin) child.stdin.end();
73
+ } catch {
74
+ resolve(null);
75
+ }
76
+ });
77
+ }
44
78
 
45
79
  /** Marks a resolved path as living inside the distro: `wsl:/home/u/bin/claude`. */
46
80
  const WSL_PREFIX = 'wsl:';
@@ -95,6 +129,38 @@ function isRunningInWsl() {
95
129
  // ---------------------------------------------------------------------------
96
130
 
97
131
  let distrosCache = { value: null, at: 0 };
132
+ let distrosRefresh = null;
133
+ let runningDistrosCache = { value: null, at: 0 };
134
+ let runningDistrosRefresh = null;
135
+
136
+ function _parseDistros(buf) {
137
+ return Buffer.from(buf)
138
+ .toString('utf16le')
139
+ .split(/\r?\n/)
140
+ .map((l) => l.replace(/\0/g, '').trim())
141
+ .filter(Boolean);
142
+ }
143
+
144
+ /** Record the distro list; a changed one invalidates everything derived from it. */
145
+ function _setDistros(names) {
146
+ const prev = distrosCache.value;
147
+ distrosCache = { value: names, at: Date.now() };
148
+ if (prev && prev.join('\0') !== names.join('\0')) {
149
+ uncRootCache = { value: null, at: 0 };
150
+ shellProbeCache = { value: null, at: 0 };
151
+ wslBinaryCache.clear();
152
+ }
153
+ }
154
+
155
+ function _refreshDistros() {
156
+ if (distrosCache.value && _fresh(distrosCache, MACHINE_TTL_MS)) return Promise.resolve();
157
+ if (!distrosRefresh) {
158
+ distrosRefresh = _execFileOut('wsl.exe', ['-l', '-q'], { encoding: 'buffer', maxBuffer: 1024 * 1024 })
159
+ .then((out) => _setDistros(out ? _parseDistros(out) : []))
160
+ .finally(() => { distrosRefresh = null; });
161
+ }
162
+ return distrosRefresh;
163
+ }
98
164
 
99
165
  /**
100
166
  * Registered, non-broken distro names, default first.
@@ -110,9 +176,13 @@ let distrosCache = { value: null, at: 0 };
110
176
  */
111
177
  function wslDistros() {
112
178
  if (!IS_WINDOWS) return [];
113
- if (distrosCache.value && Date.now() - distrosCache.at < CACHE_TTL_MS) {
179
+ if (distrosCache.value && _fresh(distrosCache, MACHINE_TTL_MS)) {
114
180
  return [...distrosCache.value];
115
181
  }
182
+ if (!canBlock()) {
183
+ void _refreshDistros();
184
+ return [...(distrosCache.value || [])];
185
+ }
116
186
  let names = [];
117
187
  try {
118
188
  const buf = execFileSync('wsl.exe', ['-l', '-q'], {
@@ -121,15 +191,11 @@ function wslDistros() {
121
191
  stdio: ['ignore', 'pipe', 'ignore'],
122
192
  maxBuffer: 1024 * 1024,
123
193
  });
124
- names = buf
125
- .toString('utf16le')
126
- .split(/\r?\n/)
127
- .map((l) => l.replace(/\0/g, '').trim())
128
- .filter(Boolean);
194
+ names = _parseDistros(buf);
129
195
  } catch {
130
196
  names = [];
131
197
  }
132
- distrosCache = { value: names, at: Date.now() };
198
+ _setDistros(names);
133
199
  return [...names];
134
200
  }
135
201
 
@@ -143,24 +209,100 @@ function defaultDistro() {
143
209
  return wslDistros()[0] || null;
144
210
  }
145
211
 
212
+ function _refreshRunningDistros() {
213
+ if (_fresh(runningDistrosCache, RUNNING_TTL_MS)) return Promise.resolve();
214
+ if (!runningDistrosRefresh) {
215
+ runningDistrosRefresh = _execFileOut('wsl.exe', ['-l', '--running', '-q'], {
216
+ encoding: 'buffer', maxBuffer: 1024 * 1024,
217
+ })
218
+ .then((out) => { runningDistrosCache = { value: out ? _parseDistros(out) : [], at: Date.now() }; })
219
+ .finally(() => { runningDistrosRefresh = null; });
220
+ }
221
+ return runningDistrosRefresh;
222
+ }
223
+
224
+ /** Query WSL's management state without executing a command inside a distro. */
225
+ function _defaultDistroRunning() {
226
+ const distro = defaultDistro();
227
+ if (!distro) return false;
228
+ if (!_fresh(runningDistrosCache, RUNNING_TTL_MS)) {
229
+ if (!canBlock()) {
230
+ void _refreshRunningDistros();
231
+ } else {
232
+ let names = [];
233
+ try {
234
+ names = _parseDistros(execFileSync('wsl.exe', ['-l', '--running', '-q'], {
235
+ timeout: PROBE_TIMEOUT_MS,
236
+ windowsHide: true,
237
+ stdio: ['ignore', 'pipe', 'ignore'],
238
+ maxBuffer: 1024 * 1024,
239
+ }));
240
+ } catch { /* WSL unavailable: do not wake it */ }
241
+ runningDistrosCache = { value: names, at: Date.now() };
242
+ }
243
+ }
244
+ return (runningDistrosCache.value || []).includes(distro);
245
+ }
246
+
247
+ async function _defaultDistroRunningAsync() {
248
+ await _refreshDistros();
249
+ const distro = (distrosCache.value || [])[0];
250
+ if (!distro) return false;
251
+ await _refreshRunningDistros();
252
+ return (runningDistrosCache.value || []).includes(distro);
253
+ }
254
+
146
255
  // ---------------------------------------------------------------------------
147
256
  // Windows side: reaching into the distro's filesystem without starting one
148
257
  // ---------------------------------------------------------------------------
149
258
 
150
259
  let uncRootCache = { value: null, at: 0 };
260
+ let uncRootRefresh = null;
261
+
262
+ const UNC_PREFIXES = ['\\\\wsl.localhost\\', '\\\\wsl$\\'];
263
+
264
+ function _refreshUncRoot() {
265
+ if (_fresh(uncRootCache, MACHINE_TTL_MS)) return Promise.resolve();
266
+ if (!uncRootRefresh) {
267
+ uncRootRefresh = (async () => {
268
+ if (!(await _defaultDistroRunningAsync())) return;
269
+ const distro = (distrosCache.value || [])[0];
270
+ if (!distro) return;
271
+ let root = null;
272
+ for (const prefix of UNC_PREFIXES) {
273
+ try {
274
+ await fs.promises.access(prefix + distro);
275
+ root = prefix + distro;
276
+ break;
277
+ } catch {
278
+ /* unreachable share — try the next spelling */
279
+ }
280
+ }
281
+ uncRootCache = { value: root, at: Date.now() };
282
+ })()
283
+ .catch(() => {})
284
+ .finally(() => { uncRootRefresh = null; });
285
+ }
286
+ return uncRootRefresh;
287
+ }
151
288
 
152
289
  /**
153
290
  * The UNC prefix that maps the default distro's root into Windows, or null.
154
291
  * WSL2 serves \\wsl.localhost\<distro>; WSL1 and older builds only \\wsl$\.
155
292
  */
156
293
  function _uncRoot() {
157
- if (uncRootCache.at && Date.now() - uncRootCache.at < CACHE_TTL_MS) {
294
+ if (!_defaultDistroRunning()) return null;
295
+ if (_fresh(uncRootCache, MACHINE_TTL_MS)) {
296
+ return uncRootCache.value;
297
+ }
298
+ if (!canBlock()) {
299
+ void _refreshUncRoot();
158
300
  return uncRootCache.value;
159
301
  }
160
302
  const distro = defaultDistro();
161
303
  if (!distro) return null;
162
304
  let root = null;
163
- for (const prefix of ['\\\\wsl.localhost\\', '\\\\wsl$\\']) {
305
+ for (const prefix of UNC_PREFIXES) {
164
306
  const candidate = prefix + distro;
165
307
  try {
166
308
  if (fs.existsSync(candidate)) { root = candidate; break; }
@@ -198,9 +340,69 @@ function toUncPath(linuxPath) {
198
340
 
199
341
  const PROBE_DELIM = '__OPENAGENTS_WSL__';
200
342
  let shellProbeCache = { value: null, at: 0 };
343
+ let shellProbeRefresh = null;
344
+
345
+ const SHELL_PROBE_SCRIPT =
346
+ 'echo ' + PROBE_DELIM + '; echo "$PATH"; echo "$HOME"; ' +
347
+ "wslpath -u 'C:\\' 2>/dev/null || echo /mnt/c/";
348
+ const SHELL_PROBE_ARGVS = [
349
+ ['-e', 'bash', '-ilc', SHELL_PROBE_SCRIPT],
350
+ ['-e', 'sh', '-lc', SHELL_PROBE_SCRIPT],
351
+ ];
352
+
353
+ function _emptyShellProbe() {
354
+ return { dirs: [], home: null, automount: '/mnt/' };
355
+ }
356
+
357
+ /** The probe's delimited output as {dirs, home, automount}, or null if unusable. */
358
+ function _parseShellProbe(raw) {
359
+ const idx = raw.indexOf(PROBE_DELIM);
360
+ if (idx === -1) return null;
361
+ const lines = raw.slice(idx + PROBE_DELIM.length).split(/\r?\n/).map((l) => l.trim());
362
+ const pathLine = lines[1];
363
+ const homeLine = lines[2];
364
+ const cRoot = lines[3];
365
+ if (!pathLine) return null;
366
+ const dirs = [];
367
+ for (const d of pathLine.split(':')) {
368
+ const dir = d.trim();
369
+ // /mnt/<drive> entries are the Windows PATH bounced back by interop.
370
+ // They hold Windows binaries, never the in-distro install we are after.
371
+ if (dir && dir.startsWith('/') && !/^\/mnt\/[a-z]\//i.test(dir) && !dirs.includes(dir)) {
372
+ dirs.push(dir);
373
+ }
374
+ }
375
+ // `wslpath -u 'C:\'` answers /mnt/c/ by default, but /etc/wsl.conf can
376
+ // move the automount root — derive it rather than hardcoding /mnt.
377
+ let automount = '/mnt/';
378
+ const m = /^(\/.*\/)c\/?$/i.exec(cRoot || '');
379
+ if (m) automount = m[1];
380
+ return { dirs, home: homeLine && homeLine.startsWith('/') ? homeLine : null, automount };
381
+ }
382
+
383
+ function _refreshShellProbe() {
384
+ if (_fresh(shellProbeCache, MACHINE_TTL_MS)) return Promise.resolve();
385
+ if (!shellProbeRefresh) {
386
+ shellProbeRefresh = (async () => {
387
+ if (!(await _defaultDistroRunningAsync())) return;
388
+ let out = null;
389
+ for (const argv of SHELL_PROBE_ARGVS) {
390
+ const raw = await _execFileOut('wsl.exe', argv, { encoding: 'utf-8', maxBuffer: 4 * 1024 * 1024 });
391
+ out = raw == null ? null : _parseShellProbe(raw);
392
+ if (out) break;
393
+ }
394
+ shellProbeCache = { value: out || _emptyShellProbe(), at: Date.now() };
395
+ // Lookups answered before the PATH was known searched too few dirs.
396
+ if (out) wslBinaryCache.clear();
397
+ })()
398
+ .catch(() => {})
399
+ .finally(() => { shellProbeRefresh = null; });
400
+ }
401
+ return shellProbeRefresh;
402
+ }
201
403
 
202
404
  /**
203
- * One wsl.exe call per process (30s TTL) that answers everything later lookups
405
+ * One wsl.exe call (5 min TTL) that answers everything later lookups
204
406
  * need: the login PATH, $HOME, and the real automount root.
205
407
  *
206
408
  * `bash -ilc` — INTERACTIVE as well as login — because the version managers
@@ -211,18 +413,19 @@ let shellProbeCache = { value: null, at: 0 };
211
413
  * stderr is dropped: an interactive shell with no tty warns about job control.
212
414
  */
213
415
  function _shellProbe() {
214
- const empty = { dirs: [], home: null, automount: '/mnt/' };
215
- if (shellProbeCache.at && Date.now() - shellProbeCache.at < CACHE_TTL_MS) {
416
+ const empty = _emptyShellProbe();
417
+ if (!_defaultDistroRunning()) return empty;
418
+ if (_fresh(shellProbeCache, MACHINE_TTL_MS)) {
419
+ return shellProbeCache.value || empty;
420
+ }
421
+ if (!canBlock()) {
422
+ void _refreshShellProbe();
216
423
  return shellProbeCache.value || empty;
217
424
  }
218
425
  if (!isWslAvailable()) return empty;
219
426
 
220
- const script =
221
- 'echo ' + PROBE_DELIM + '; echo "$PATH"; echo "$HOME"; ' +
222
- "wslpath -u 'C:\\' 2>/dev/null || echo /mnt/c/";
223
-
224
427
  let out = null;
225
- for (const argv of [['-e', 'bash', '-ilc', script], ['-e', 'sh', '-lc', script]]) {
428
+ for (const argv of SHELL_PROBE_ARGVS) {
226
429
  try {
227
430
  const raw = execFileSync('wsl.exe', argv, {
228
431
  encoding: 'utf-8',
@@ -231,29 +434,8 @@ function _shellProbe() {
231
434
  stdio: ['ignore', 'pipe', 'ignore'],
232
435
  maxBuffer: 4 * 1024 * 1024,
233
436
  });
234
- const idx = raw.indexOf(PROBE_DELIM);
235
- if (idx === -1) continue;
236
- const lines = raw.slice(idx + PROBE_DELIM.length).split(/\r?\n/).map((l) => l.trim());
237
- const pathLine = lines[1];
238
- const homeLine = lines[2];
239
- const cRoot = lines[3];
240
- if (!pathLine) continue;
241
- const dirs = [];
242
- for (const d of pathLine.split(':')) {
243
- const dir = d.trim();
244
- // /mnt/<drive> entries are the Windows PATH bounced back by interop.
245
- // They hold Windows binaries, never the in-distro install we are after.
246
- if (dir && dir.startsWith('/') && !/^\/mnt\/[a-z]\//i.test(dir) && !dirs.includes(dir)) {
247
- dirs.push(dir);
248
- }
249
- }
250
- // `wslpath -u 'C:\'` answers /mnt/c/ by default, but /etc/wsl.conf can
251
- // move the automount root — derive it rather than hardcoding /mnt.
252
- let automount = '/mnt/';
253
- const m = /^(\/.*\/)c\/?$/i.exec(cRoot || '');
254
- if (m) automount = m[1];
255
- out = { dirs, home: homeLine && homeLine.startsWith('/') ? homeLine : null, automount };
256
- break;
437
+ out = _parseShellProbe(raw);
438
+ if (out) break;
257
439
  } catch {
258
440
  /* try the next shell */
259
441
  }
@@ -290,6 +472,42 @@ function wslBinDirs() {
290
472
  }
291
473
 
292
474
  const wslBinaryCache = new Map();
475
+ const wslBinaryRefresh = new Map();
476
+
477
+ /** resolveWslBinary's search, off the calling thread. See probe-mode.js. */
478
+ function _refreshWslBinary(key, list) {
479
+ if (wslBinaryRefresh.has(key)) return wslBinaryRefresh.get(key);
480
+ const job = (async () => {
481
+ if (!(await _defaultDistroRunningAsync())) return;
482
+ let value = null;
483
+ if ((distrosCache.value || []).length) {
484
+ await Promise.all([_refreshUncRoot(), _refreshShellProbe()]);
485
+ if (uncRootCache.value) {
486
+ search:
487
+ for (const dir of wslBinDirs()) {
488
+ for (const name of list) {
489
+ const linuxPath = dir.replace(/\/+$/, '') + '/' + name;
490
+ const unc = toUncPath(linuxPath);
491
+ if (!unc) continue;
492
+ try {
493
+ if ((await fs.promises.stat(unc)).isFile()) {
494
+ value = WSL_PREFIX + linuxPath;
495
+ break search;
496
+ }
497
+ } catch {
498
+ /* unreadable path — keep looking */
499
+ }
500
+ }
501
+ }
502
+ }
503
+ }
504
+ wslBinaryCache.set(key, { value, at: Date.now() });
505
+ })()
506
+ .catch(() => {})
507
+ .finally(() => wslBinaryRefresh.delete(key));
508
+ wslBinaryRefresh.set(key, job);
509
+ return job;
510
+ }
293
511
 
294
512
  /**
295
513
  * Resolve an agent binary inside the default distro, or null.
@@ -297,18 +515,23 @@ const wslBinaryCache = new Map();
297
515
  * Returns the path as the DISTRO sees it, behind the `wsl:` marker
298
516
  * (`wsl:/home/u/.local/bin/claude`) — the inner path is what `wsl.exe -e` must
299
517
  * be handed. Existence is proved through the UNC mount, so a machine with WSL
300
- * installed but no distro pays nothing beyond the single cached distro listing.
518
+ * installed but no running default distro pays only for WSL management queries.
301
519
  *
302
520
  * @param {string|string[]} names binary name(s), e.g. 'claude' or ['cursor-agent','agent']
303
521
  * @returns {string|null} marked in-distro path
304
522
  */
305
523
  function resolveWslBinary(names) {
306
524
  if (!IS_WINDOWS) return null;
525
+ if (!_defaultDistroRunning()) return null;
307
526
  const list = (Array.isArray(names) ? names : [names]).filter(Boolean);
308
527
  if (!list.length) return null;
309
528
  const key = list.join('\0');
310
529
  const cached = wslBinaryCache.get(key);
311
530
  if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.value;
531
+ if (!canBlock()) {
532
+ void _refreshWslBinary(key, list);
533
+ return cached ? cached.value : null;
534
+ }
312
535
 
313
536
  let value = null;
314
537
  if (isWslAvailable() && _uncRoot()) {
@@ -348,6 +571,7 @@ function clearWslBinaryCache() {
348
571
  /** Full reset, including the per-process probes. For tests and rare rescans. */
349
572
  function clearWslCache() {
350
573
  distrosCache = { value: null, at: 0 };
574
+ runningDistrosCache = { value: null, at: 0 };
351
575
  uncRootCache = { value: null, at: 0 };
352
576
  shellProbeCache = { value: null, at: 0 };
353
577
  wslBinaryCache.clear();