@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/icons/codearts.svg +9 -0
- package/package.json +1 -1
- package/registry.json +68 -0
- package/src/adapters/aider.js +2 -9
- package/src/adapters/amp.js +3 -15
- package/src/adapters/antigravity.js +3 -20
- package/src/adapters/base.js +307 -19
- package/src/adapters/claude.js +63 -47
- package/src/adapters/cline.js +2 -13
- package/src/adapters/codearts.js +316 -0
- package/src/adapters/codebuddy.js +3 -21
- package/src/adapters/codex.js +3 -13
- package/src/adapters/commandcode.js +3 -21
- package/src/adapters/copilot.js +2 -26
- package/src/adapters/cursor.js +8 -13
- package/src/adapters/deepseek.js +33 -68
- package/src/adapters/gemini.js +3 -20
- package/src/adapters/goose.js +4 -14
- package/src/adapters/hermes.js +2 -10
- package/src/adapters/index.js +4 -1
- package/src/adapters/kimi-stream.js +44 -2
- package/src/adapters/kimi.js +118 -34
- package/src/adapters/llm-direct.js +17 -6
- package/src/adapters/mini.js +3 -14
- package/src/adapters/nanoclaw.js +25 -13
- package/src/adapters/openclaw.js +41 -0
- package/src/adapters/opencode.js +66 -48
- package/src/adapters/openworker.js +21 -14
- package/src/adapters/pi.js +22 -29
- package/src/daemon.js +76 -5
- package/src/installer.js +48 -16
- package/src/paths.js +187 -92
- package/src/probe-mode.js +36 -0
- package/src/wsl.js +270 -46
package/src/installer.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const { execSync, exec } = require('child_process');
|
|
7
7
|
const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache, aiderBinDirs, resolveBinaryInKnownDirs } = require('./paths');
|
|
8
8
|
const { isWslBinary, bridgedCommandString, wslHomeUnc } = require('./wsl');
|
|
9
|
+
const { canBlock } = require('./probe-mode');
|
|
9
10
|
const { EnvManager } = require('./env');
|
|
10
11
|
const { nodeDistUrls, installRegistry } = require('./mirrors');
|
|
11
12
|
const { readinessReason, REASON } = require('./adapters/health-status');
|
|
@@ -39,6 +40,19 @@ const STALL_POLL_MS = Math.max(50, Math.min(15000, Math.floor(INSTALL_STALL_MS /
|
|
|
39
40
|
// results are cached briefly and invalidated on install/uninstall.
|
|
40
41
|
const VERSION_CACHE_TTL_MS = 60000;
|
|
41
42
|
const versionCache = new Map();
|
|
43
|
+
/** Version probes running in the background, by cache key. See probe-mode.js. */
|
|
44
|
+
const versionRefresh = new Set();
|
|
45
|
+
|
|
46
|
+
/** The version token in a `--version` output, or its first line. */
|
|
47
|
+
function parseVersionOutput(raw) {
|
|
48
|
+
// The optional `-prerelease` tail is NOT decoration. An agent pinned to a
|
|
49
|
+
// preview release (`@deepseek-ai/dsh@0.1.0-rc.6`) is gated on the exact
|
|
50
|
+
// version, and dropping the suffix reported a correctly installed rc.6 as
|
|
51
|
+
// plain `0.1.0` — which then failed its own gate. Agents without a
|
|
52
|
+
// prerelease segment match exactly as before.
|
|
53
|
+
const match = raw.match(/(\d+[\d.]+\d+(?:-[0-9A-Za-z.-]+)?)/);
|
|
54
|
+
return match ? match[1] : (raw.split('\n')[0] || null);
|
|
55
|
+
}
|
|
42
56
|
|
|
43
57
|
/**
|
|
44
58
|
* Compare two dotted version strings numerically, ignoring any pre-release /
|
|
@@ -616,15 +630,8 @@ class Installer {
|
|
|
616
630
|
]) {
|
|
617
631
|
if (fs.existsSync(candidate)) return candidate;
|
|
618
632
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
encoding: 'utf-8',
|
|
622
|
-
timeout: 5000,
|
|
623
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
624
|
-
env: getEnhancedEnv(),
|
|
625
|
-
}).split(/\r?\n/)[0].trim();
|
|
626
|
-
if (found) return found;
|
|
627
|
-
} catch {}
|
|
633
|
+
const found = whichBinary(nodeName, { allowWsl: false });
|
|
634
|
+
if (found) return found;
|
|
628
635
|
return 'node';
|
|
629
636
|
}
|
|
630
637
|
|
|
@@ -636,6 +643,14 @@ class Installer {
|
|
|
636
643
|
const cached = versionCache.get(key);
|
|
637
644
|
if (cached && (Date.now() - cached.ts) < VERSION_CACHE_TTL_MS) return cached.version;
|
|
638
645
|
|
|
646
|
+
if (!canBlock()) {
|
|
647
|
+
// A node CLI's `--version` takes 0.5s+ (seconds for one inside WSL).
|
|
648
|
+
// Report the last known version — or unknown, which healthCheck already
|
|
649
|
+
// handles — and let the next check pick up the answer.
|
|
650
|
+
this._refreshVersion(key, versionCmd);
|
|
651
|
+
return cached ? cached.version : null;
|
|
652
|
+
}
|
|
653
|
+
|
|
639
654
|
let version = null;
|
|
640
655
|
try {
|
|
641
656
|
const raw = execSync(versionCmd, {
|
|
@@ -644,19 +659,36 @@ class Installer {
|
|
|
644
659
|
env: getEnhancedEnv(),
|
|
645
660
|
timeout: 10000,
|
|
646
661
|
}).trim();
|
|
647
|
-
|
|
648
|
-
// preview release (`@deepseek-ai/dsh@0.1.0-rc.6`) is gated on the exact
|
|
649
|
-
// version, and dropping the suffix reported a correctly installed rc.6 as
|
|
650
|
-
// plain `0.1.0` — which then failed its own gate. Agents without a
|
|
651
|
-
// prerelease segment match exactly as before.
|
|
652
|
-
const match = raw.match(/(\d+[\d.]+\d+(?:-[0-9A-Za-z.-]+)?)/);
|
|
653
|
-
version = match ? match[1] : (raw.split('\n')[0] || null);
|
|
662
|
+
version = parseVersionOutput(raw);
|
|
654
663
|
} catch {}
|
|
655
664
|
|
|
656
665
|
versionCache.set(key, { version, ts: Date.now() });
|
|
657
666
|
return version;
|
|
658
667
|
}
|
|
659
668
|
|
|
669
|
+
/** _detectVersion's probe, off the calling thread. */
|
|
670
|
+
_refreshVersion(key, versionCmd) {
|
|
671
|
+
if (versionRefresh.has(key)) return;
|
|
672
|
+
versionRefresh.add(key);
|
|
673
|
+
try {
|
|
674
|
+
const child = exec(versionCmd, {
|
|
675
|
+
encoding: 'utf-8',
|
|
676
|
+
env: getEnhancedEnv(),
|
|
677
|
+
timeout: 10000,
|
|
678
|
+
windowsHide: true,
|
|
679
|
+
}, (err, stdout) => {
|
|
680
|
+
versionRefresh.delete(key);
|
|
681
|
+
versionCache.set(key, {
|
|
682
|
+
version: err ? null : parseVersionOutput(String(stdout).trim()),
|
|
683
|
+
ts: Date.now(),
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
if (child.stdin) child.stdin.end();
|
|
687
|
+
} catch {
|
|
688
|
+
versionRefresh.delete(key);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
660
692
|
/**
|
|
661
693
|
* Evaluate a registry version gate against a detected version. Generic for any
|
|
662
694
|
* agent. Returns { compatible: true|false|null, minVersion, message }.
|
package/src/paths.js
CHANGED
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const os = require('os');
|
|
14
|
-
const { execSync, execFileSync } = require('child_process');
|
|
14
|
+
const { exec, execSync, execFile, execFileSync } = require('child_process');
|
|
15
15
|
const {
|
|
16
16
|
isRunningInWsl,
|
|
17
17
|
resolveWslBinary,
|
|
18
18
|
clearWslBinaryCache,
|
|
19
19
|
wslBinDirs,
|
|
20
20
|
} = require('./wsl');
|
|
21
|
+
const { canBlock } = require('./probe-mode');
|
|
21
22
|
|
|
22
23
|
const IS_WINDOWS = process.platform === 'win32';
|
|
23
24
|
const IS_MACOS = process.platform === 'darwin';
|
|
@@ -254,43 +255,107 @@ function _whichNative(name) {
|
|
|
254
255
|
return value;
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
/** The value of an env var, matching the key case-insensitively on Windows. */
|
|
259
|
+
function _envValue(env, name) {
|
|
260
|
+
if (!IS_WINDOWS) return env[name];
|
|
261
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === name);
|
|
262
|
+
return key ? env[key] : undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
257
265
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* zh-CN), so `where` prints a path whose non-ASCII bytes don't match the utf-8
|
|
262
|
-
* decoding execSync does — a Chinese username comes back mangled (e.g.
|
|
263
|
-
* `C:\Users\??.?[\…`) and yields ENOENT downstream. We can't reliably re-encode
|
|
264
|
-
* it (and forcing `chcp` is unsafe under windowsHide's console-less cmd), so we
|
|
265
|
-
* existence-check every hit and only return ones that are real; a mangled path
|
|
266
|
-
* fails and the caller falls through to its Node-derived tiers (built from
|
|
267
|
-
* process.env / os.homedir, which the OS hands us as correct UTF-16).
|
|
266
|
+
* Directory listings for _searchPath, keyed by dir. One sweep over every agent
|
|
267
|
+
* type asks for ~20 names × every PATHEXT spelling × every PATH dir — thousands
|
|
268
|
+
* of stats — where reading each dir once answers all of them.
|
|
268
269
|
*/
|
|
269
|
-
|
|
270
|
-
|
|
270
|
+
const DIR_INDEX_TTL_MS = 5000;
|
|
271
|
+
const dirIndexCache = new Map();
|
|
272
|
+
|
|
273
|
+
/** name (lower-cased on Windows) → name on disk; null when unlistable. */
|
|
274
|
+
function _dirIndex(dir) {
|
|
275
|
+
const hit = dirIndexCache.get(dir);
|
|
276
|
+
if (hit && Date.now() - hit.at < DIR_INDEX_TTL_MS) return hit.names;
|
|
277
|
+
let names = new Map();
|
|
271
278
|
try {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
for (const line of result.split(/\r?\n/)) {
|
|
287
|
-
const hit = line.trim();
|
|
288
|
-
if (hit && fs.existsSync(hit)) hits.push(hit);
|
|
289
|
-
}
|
|
290
|
-
return hits;
|
|
279
|
+
for (const n of fs.readdirSync(dir)) names.set(IS_WINDOWS ? n.toLowerCase() : n, n);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
// A dir that isn't there holds nothing. One we may not list can still hold
|
|
282
|
+
// files we may run (an execute-only dir), so that one is stat'ed instead.
|
|
283
|
+
if (e && e.code !== 'ENOENT' && e.code !== 'ENOTDIR') names = null;
|
|
284
|
+
}
|
|
285
|
+
dirIndexCache.set(dir, { names, at: Date.now() });
|
|
286
|
+
return names;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Whether `p` is a file `where`/`which` would report. */
|
|
290
|
+
function _isCommandFile(p) {
|
|
291
|
+
try {
|
|
292
|
+
if (!fs.statSync(p).isFile()) return false;
|
|
291
293
|
} catch {
|
|
292
|
-
|
|
294
|
+
// App Execution Aliases (…\WindowsApps\*.exe) are reparse points stat
|
|
295
|
+
// cannot follow, yet Windows runs them and `where` lists them.
|
|
296
|
+
if (!IS_WINDOWS) return false;
|
|
297
|
+
try { fs.lstatSync(p); return true; } catch { return false; }
|
|
298
|
+
}
|
|
299
|
+
if (IS_WINDOWS) return true;
|
|
300
|
+
try { fs.accessSync(p, fs.constants.X_OK); return true; } catch { return false; }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* What `where name` (Windows: every match, PATH order, PATHEXT applied to a
|
|
305
|
+
* bare name) or `which name` (Unix: the first executable) would report.
|
|
306
|
+
*
|
|
307
|
+
* Walked in-process rather than by spawning either command. Detection asks
|
|
308
|
+
* this for every agent type every time a lookup cache expires, and each spawn
|
|
309
|
+
* is a cmd.exe start — ~60-300ms apiece on Windows, far more under an
|
|
310
|
+
* antivirus scan — which in the desktop app froze the window for seconds on
|
|
311
|
+
* every refresh. It also removes the codepage hazard the spawn had: `where`
|
|
312
|
+
* printed paths in the OEM codepage (936 on zh-CN), mangling any non-ASCII
|
|
313
|
+
* user name, whereas these paths never leave UTF-16.
|
|
314
|
+
*/
|
|
315
|
+
function _searchPath(name, env) {
|
|
316
|
+
const hits = [];
|
|
317
|
+
if (!name) return hits;
|
|
318
|
+
const source = env || process.env;
|
|
319
|
+
let names = [name];
|
|
320
|
+
if (IS_WINDOWS) {
|
|
321
|
+
const exts = String(_envValue(source, 'PATHEXT') || '.COM;.EXE;.BAT;.CMD')
|
|
322
|
+
.split(';')
|
|
323
|
+
.map((e) => e.trim().toLowerCase())
|
|
324
|
+
.filter((e) => e.startsWith('.'));
|
|
325
|
+
const ext = path.extname(name).toLowerCase();
|
|
326
|
+
if (!ext || !exts.includes(ext)) names = [name, ...exts.map((e) => name + e)];
|
|
327
|
+
}
|
|
328
|
+
const seen = new Set();
|
|
329
|
+
for (const raw of String(_envValue(source, 'PATH') || '').split(SEP)) {
|
|
330
|
+
const dir = raw.trim().replace(/^"(.*)"$/, '$1');
|
|
331
|
+
if (!dir) continue;
|
|
332
|
+
const index = _dirIndex(dir);
|
|
333
|
+
for (const n of names) {
|
|
334
|
+
let full;
|
|
335
|
+
if (index) {
|
|
336
|
+
const onDisk = index.get(IS_WINDOWS ? n.toLowerCase() : n);
|
|
337
|
+
if (!onDisk) continue;
|
|
338
|
+
full = path.join(dir, onDisk);
|
|
339
|
+
} else {
|
|
340
|
+
full = path.join(dir, n);
|
|
341
|
+
}
|
|
342
|
+
const key = IS_WINDOWS ? full.toLowerCase() : full;
|
|
343
|
+
if (seen.has(key)) continue;
|
|
344
|
+
seen.add(key);
|
|
345
|
+
if (!_isCommandFile(full)) continue;
|
|
346
|
+
hits.push(full);
|
|
347
|
+
if (!IS_WINDOWS) return hits;
|
|
348
|
+
}
|
|
293
349
|
}
|
|
350
|
+
return hits;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Existing paths `where`/`which` reports for a name, in its own order. */
|
|
354
|
+
function _runWhich(name) {
|
|
355
|
+
// getEnhancedEnv(), never `{...process.env, PATH}`: spreading process.env on
|
|
356
|
+
// Windows yields a "Path" key, and a second PATH key makes which one counts
|
|
357
|
+
// undefined.
|
|
358
|
+
return _searchPath(name, getEnhancedEnv());
|
|
294
359
|
}
|
|
295
360
|
|
|
296
361
|
/**
|
|
@@ -303,18 +368,12 @@ function _isWindowsMount(p) {
|
|
|
303
368
|
}
|
|
304
369
|
|
|
305
370
|
/**
|
|
306
|
-
*
|
|
371
|
+
* `where`/`which` lookup, shared by every CLI adapter.
|
|
307
372
|
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
* no `chcp` (which is unreliable under windowsHide's console-less cmd):
|
|
313
|
-
* 1. (Windows) check the npm-global default `%APPDATA%\npm\<name>.cmd` FIRST.
|
|
314
|
-
* APPDATA comes from the OS as UTF-16 via Node, so this path is always
|
|
315
|
-
* correctly encoded — and it's where the npm-installed CLIs actually live.
|
|
316
|
-
* 2. Fall back to `where`/`which`, but existence-check every hit so a mangled
|
|
317
|
-
* path is skipped and the caller drops to its own Node-derived tiers.
|
|
373
|
+
* 1. (Windows) the npm-global default `%APPDATA%\npm\<name>.cmd` FIRST —
|
|
374
|
+
* it's where the npm-installed CLIs actually live.
|
|
375
|
+
* 2. Then PATH, as `where <name>.cmd || where <name>.exe || where <name>`
|
|
376
|
+
* (Unix: `which <name>`) would search it — see _searchPath.
|
|
318
377
|
*
|
|
319
378
|
* @param {string|string[]} names base name(s), e.g. 'claude' or ['cursor-agent','agent'].
|
|
320
379
|
* On Windows each is tried as <name>.cmd, <name>.exe, <name>.
|
|
@@ -333,33 +392,27 @@ function whereBinary(names, env) {
|
|
|
333
392
|
}
|
|
334
393
|
}
|
|
335
394
|
|
|
336
|
-
// 2. PATH lookup
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
395
|
+
// 2. PATH lookup.
|
|
396
|
+
const lookupEnv = env || getEnhancedEnv();
|
|
397
|
+
for (const b of bases) {
|
|
398
|
+
for (const form of IS_WINDOWS ? [`${b}.cmd`, `${b}.exe`, b] : [b]) {
|
|
399
|
+
const hit = _searchPath(form, lookupEnv)[0];
|
|
400
|
+
if (hit) return hit;
|
|
342
401
|
}
|
|
343
|
-
cmd = parts.join(' || ');
|
|
344
|
-
} else {
|
|
345
|
-
cmd = bases.map((b) => `which ${b}`).join(' || ');
|
|
346
402
|
}
|
|
347
|
-
try {
|
|
348
|
-
const out = execSync(cmd, {
|
|
349
|
-
encoding: 'utf-8',
|
|
350
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
351
|
-
timeout: 5000,
|
|
352
|
-
windowsHide: true,
|
|
353
|
-
env: env || getEnhancedEnv(),
|
|
354
|
-
});
|
|
355
|
-
for (const line of out.split(/\r?\n/)) {
|
|
356
|
-
const hit = line.trim();
|
|
357
|
-
if (hit && fs.existsSync(hit)) return hit;
|
|
358
|
-
}
|
|
359
|
-
} catch {}
|
|
360
403
|
return null;
|
|
361
404
|
}
|
|
362
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Drop the bin-dir snapshots and every lookup answered from them — for when a
|
|
408
|
+
* background probe has just learned of directories they did not include.
|
|
409
|
+
*/
|
|
410
|
+
function _invalidateBinDirCaches() {
|
|
411
|
+
knownBinDirsCache = { value: null, at: 0 };
|
|
412
|
+
extraBinDirsCache = { value: null, at: 0, path: '' };
|
|
413
|
+
whichBinaryCache.clear();
|
|
414
|
+
}
|
|
415
|
+
|
|
363
416
|
/**
|
|
364
417
|
* Global bin directories for every JS package manager we might find an agent
|
|
365
418
|
* CLI in — plus the npm prefix the user has actually configured.
|
|
@@ -451,6 +504,13 @@ function _addAgentInstallerPaths(dirs) {
|
|
|
451
504
|
// these; the installer that decides whether it EXISTS had not.
|
|
452
505
|
_push(dirs, path.join(HOME, '.codebuddy', 'bin'));
|
|
453
506
|
|
|
507
|
+
// codearts — Huawei's install.sh / install.ps1 unpack into
|
|
508
|
+
// ~/.codeartsdoer/installers and put that dir on PATH through a shell rc
|
|
509
|
+
// edit or the user `Path` registry value. The `codearts` there is a wrapper
|
|
510
|
+
// script; the CLI itself sits in its bin/, which the installer deliberately
|
|
511
|
+
// keeps off PATH.
|
|
512
|
+
_push(dirs, path.join(HOME, '.codeartsdoer', 'installers'));
|
|
513
|
+
|
|
454
514
|
// claude — `claude install` (the native, non-npm build) relocates the CLI to
|
|
455
515
|
// ~/.claude/local and reaches it through a shell alias, which a GUI process
|
|
456
516
|
// inherits no more than it inherits an rc-file PATH edit.
|
|
@@ -602,14 +662,28 @@ function _npmPrefix() {
|
|
|
602
662
|
}
|
|
603
663
|
} catch {}
|
|
604
664
|
|
|
665
|
+
const opts = {
|
|
666
|
+
encoding: 'utf-8',
|
|
667
|
+
timeout: 8000,
|
|
668
|
+
windowsHide: true,
|
|
669
|
+
// The user's PATH first: it is their npm we are asking about.
|
|
670
|
+
env: { ...process.env, PATH: _basePATH() },
|
|
671
|
+
};
|
|
672
|
+
if (!canBlock()) {
|
|
673
|
+
// npm takes a second or more to start on Windows. Answer "no custom
|
|
674
|
+
// prefix" for now; the bin dirs are rebuilt once npm has said otherwise.
|
|
675
|
+
try {
|
|
676
|
+
const child = exec('npm config get prefix', opts, (err, stdout) => {
|
|
677
|
+
if (!err && accept(String(stdout).trim())) _invalidateBinDirCaches();
|
|
678
|
+
});
|
|
679
|
+
if (child.stdin) child.stdin.end();
|
|
680
|
+
} catch {}
|
|
681
|
+
return npmPrefixCache;
|
|
682
|
+
}
|
|
605
683
|
try {
|
|
606
684
|
const out = execSync('npm config get prefix', {
|
|
607
|
-
|
|
608
|
-
timeout: 8000,
|
|
609
|
-
windowsHide: true,
|
|
685
|
+
...opts,
|
|
610
686
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
611
|
-
// The user's PATH first: it is their npm we are asking about.
|
|
612
|
-
env: { ...process.env, PATH: _basePATH() },
|
|
613
687
|
}).trim();
|
|
614
688
|
accept(out);
|
|
615
689
|
} catch {}
|
|
@@ -663,34 +737,54 @@ function loginShellDirs() {
|
|
|
663
737
|
if (IS_WINDOWS) return loginShellDirsCache;
|
|
664
738
|
if (process.env.OPENAGENTS_SKIP_SHELL_PATH === '1') return loginShellDirsCache;
|
|
665
739
|
const shell = process.env.SHELL || '/bin/zsh';
|
|
740
|
+
const argv = ['-ilc', `echo ${SHELL_ENV_DELIM}; command env; echo ${SHELL_ENV_DELIM}`];
|
|
741
|
+
const opts = {
|
|
742
|
+
encoding: 'utf-8',
|
|
743
|
+
timeout: SHELL_PROBE_TIMEOUT_MS,
|
|
744
|
+
killSignal: 'SIGKILL',
|
|
745
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
746
|
+
};
|
|
666
747
|
try {
|
|
667
748
|
if (!fs.existsSync(shell)) return loginShellDirsCache;
|
|
668
|
-
|
|
669
|
-
shell
|
|
670
|
-
|
|
671
|
-
{
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
);
|
|
681
|
-
const parts = String(out).split(SHELL_ENV_DELIM);
|
|
682
|
-
if (parts.length < 3) return loginShellDirsCache;
|
|
683
|
-
const line = parts[1].split(/\r?\n/).find((l) => l.startsWith('PATH='));
|
|
684
|
-
if (!line) return loginShellDirsCache;
|
|
685
|
-
const seen = new Set();
|
|
686
|
-
for (const d of line.slice('PATH='.length).split(SEP)) {
|
|
687
|
-
const dir = d.trim();
|
|
688
|
-
if (dir && !seen.has(dir)) { seen.add(dir); loginShellDirsCache.push(dir); }
|
|
749
|
+
if (!canBlock()) {
|
|
750
|
+
// A login shell with nvm/oh-my-zsh in its rc files takes seconds. Search
|
|
751
|
+
// the curated dirs meanwhile and add the shell's once it has answered.
|
|
752
|
+
const child = execFile(shell, argv, opts, (err, stdout) => {
|
|
753
|
+
const dirs = err ? [] : _parseLoginShellPath(stdout);
|
|
754
|
+
if (dirs.length) {
|
|
755
|
+
loginShellDirsCache = dirs;
|
|
756
|
+
_invalidateBinDirCaches();
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
if (child.stdin) child.stdin.end();
|
|
760
|
+
return loginShellDirsCache;
|
|
689
761
|
}
|
|
762
|
+
const out = execFileSync(shell, argv, {
|
|
763
|
+
...opts,
|
|
764
|
+
// stdin closed, stderr dropped: an interactive shell with no tty writes
|
|
765
|
+
// job-control warnings there and we do not want them in the output.
|
|
766
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
767
|
+
});
|
|
768
|
+
loginShellDirsCache = _parseLoginShellPath(out);
|
|
690
769
|
} catch {}
|
|
691
770
|
return loginShellDirsCache;
|
|
692
771
|
}
|
|
693
772
|
|
|
773
|
+
/** The PATH entries in a delimited `env` dump, in order and deduplicated. */
|
|
774
|
+
function _parseLoginShellPath(out) {
|
|
775
|
+
const parts = String(out).split(SHELL_ENV_DELIM);
|
|
776
|
+
if (parts.length < 3) return [];
|
|
777
|
+
const line = parts[1].split(/\r?\n/).find((l) => l.startsWith('PATH='));
|
|
778
|
+
if (!line) return [];
|
|
779
|
+
const dirs = [];
|
|
780
|
+
const seen = new Set();
|
|
781
|
+
for (const d of line.slice('PATH='.length).split(SEP)) {
|
|
782
|
+
const dir = d.trim();
|
|
783
|
+
if (dir && !seen.has(dir)) { seen.add(dir); dirs.push(dir); }
|
|
784
|
+
}
|
|
785
|
+
return dirs;
|
|
786
|
+
}
|
|
787
|
+
|
|
694
788
|
// ---- Windows paths ----
|
|
695
789
|
|
|
696
790
|
function _addWindowsPaths(dirs) {
|
|
@@ -970,6 +1064,7 @@ function clearBinaryLookupCache() {
|
|
|
970
1064
|
knownBinDirsCache = { value: null, at: 0 };
|
|
971
1065
|
extraBinDirsCache = { value: null, at: 0, path: '' };
|
|
972
1066
|
whichBinaryCache.clear();
|
|
1067
|
+
dirIndexCache.clear();
|
|
973
1068
|
clearWslBinaryCache();
|
|
974
1069
|
// The login-shell / npm-prefix probes are deliberately NOT cleared: they each
|
|
975
1070
|
// cost a process spawn, they answer a question about the machine rather than
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this process may wait on a probe.
|
|
3
|
+
*
|
|
4
|
+
* The core runs in two kinds of process. The daemon and the CLI are plain Node
|
|
5
|
+
* processes: a synchronous `wsl.exe`, `npm config get prefix` or `<cli>
|
|
6
|
+
* --version` only delays the caller. The desktop app loads the same modules
|
|
7
|
+
* into Electron's MAIN process, whose thread also pumps the window's messages —
|
|
8
|
+
* while it waits on a probe the window cannot paint or take input, and Windows
|
|
9
|
+
* marks it "Not Responding". The app polls agent detection every few seconds,
|
|
10
|
+
* so those waits add up to a window that is frozen more often than not.
|
|
11
|
+
*
|
|
12
|
+
* There, a probe that would spawn a process or touch a network share answers
|
|
13
|
+
* from its cache — possibly empty, possibly stale — and refreshes in the
|
|
14
|
+
* background. Everywhere else it keeps the synchronous behaviour, because a
|
|
15
|
+
* daemon about to spawn an agent needs the real answer, not a cached guess.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
let override = null;
|
|
21
|
+
|
|
22
|
+
/** True when a probe may block this thread until it has an answer. */
|
|
23
|
+
function canBlock() {
|
|
24
|
+
if (override !== null) return override;
|
|
25
|
+
return !(process.versions && process.versions.electron && process.type === 'browser');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Force the mode, for tests. `null` restores detection.
|
|
30
|
+
* @param {boolean|null} value
|
|
31
|
+
*/
|
|
32
|
+
function setBlockingProbes(value) {
|
|
33
|
+
override = value === null || value === undefined ? null : !!value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { canBlock, setBlockingProbes };
|