@openagents-org/agent-launcher 0.2.180 → 0.2.182
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/antigravity.svg +1 -0
- package/icons/mini-swe-agent.svg +7 -0
- package/package.json +3 -2
- package/registry.json +1137 -622
- package/src/adapters/aider.js +10 -3
- package/src/adapters/amp.js +11 -3
- package/src/adapters/antigravity.js +10 -2
- package/src/adapters/base.js +46 -0
- package/src/adapters/claude.js +6 -1
- package/src/adapters/cline.js +5 -1
- package/src/adapters/codebuddy.js +6 -1
- package/src/adapters/codex.js +10 -2
- package/src/adapters/commandcode.js +5 -1
- package/src/adapters/copilot-stream-parser.js +58 -8
- package/src/adapters/copilot.js +20 -4
- package/src/adapters/cursor.js +5 -1
- package/src/adapters/deepseek.js +5 -1
- package/src/adapters/gemini.js +10 -2
- package/src/adapters/goose.js +6 -2
- package/src/adapters/hermes.js +23 -53
- package/src/adapters/kimi.js +5 -1
- package/src/adapters/llm-direct.js +6 -0
- package/src/adapters/mini.js +10 -3
- package/src/adapters/openclaw.js +5 -1
- package/src/adapters/opencode.js +6 -1
- package/src/adapters/openworker.js +15 -1
- package/src/adapters/pi.js +6 -1
- package/src/adapters/workspace-prompt.js +23 -12
- package/src/daemon.js +60 -1
- package/src/index.js +120 -0
- package/src/installer.js +57 -16
- package/src/paths.js +98 -34
- package/src/probe.js +4 -1
- package/src/workspace-client.js +38 -0
- package/src/wsl.js +579 -0
package/src/wsl.js
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Windows/WSL boundary — making agent CLIs on the other side of it both
|
|
3
|
+
* DETECTABLE and RUNNABLE.
|
|
4
|
+
*
|
|
5
|
+
* Two setups reported the same symptom ("my agent is installed, the launcher
|
|
6
|
+
* says Not installed"), and they are mirror images of each other:
|
|
7
|
+
*
|
|
8
|
+
* 1. Launcher/daemon on native Windows, the agent CLI installed inside a WSL
|
|
9
|
+
* distro. Every tier of the existing detection (`where`, %APPDATA%\npm,
|
|
10
|
+
* ~/.openagents, the known-bin-dir sweep in paths.js) looks at the Windows
|
|
11
|
+
* filesystem only, so a perfectly good `claude` at
|
|
12
|
+
* /home/<user>/.nvm/versions/node/v22.14.0/bin/claude is invisible. Hermes
|
|
13
|
+
* grew a one-off fallback for exactly this (adapters/hermes.js) and no
|
|
14
|
+
* other agent had one.
|
|
15
|
+
* 2. Connector running INSIDE a distro, the agent CLI installed on the
|
|
16
|
+
* Windows side. `which claude` misses because the file is `claude.cmd`,
|
|
17
|
+
* and even when found, a Windows shim cannot be exec'd from Linux.
|
|
18
|
+
*
|
|
19
|
+
* The representation stays a plain string, so detection keeps its signatures
|
|
20
|
+
* and only the SPAWN site needs to know the difference: an in-distro binary
|
|
21
|
+
* comes back prefixed, as `wsl:/home/u/.local/bin/claude`. The prefix is there
|
|
22
|
+
* rather than relying on "an absolute path starting with / on win32" because
|
|
23
|
+
* that inference is silently wrong for any caller that deals in POSIX paths on
|
|
24
|
+
* Windows for its own reasons — it would bridge a path nobody meant as WSL, and
|
|
25
|
+
* a marker that can be triggered by accident is worse than no marker.
|
|
26
|
+
*
|
|
27
|
+
* The mirror direction needs no marker: a path ending in .exe/.cmd/.bat while
|
|
28
|
+
* we are running inside a distro is unambiguously a Windows binary.
|
|
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.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
'use strict';
|
|
37
|
+
|
|
38
|
+
const fs = require('fs');
|
|
39
|
+
const { spawn: nodeSpawn, execFileSync } = require('child_process');
|
|
40
|
+
|
|
41
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
42
|
+
const PROBE_TIMEOUT_MS = 10000;
|
|
43
|
+
const CACHE_TTL_MS = 30 * 1000;
|
|
44
|
+
|
|
45
|
+
/** Marks a resolved path as living inside the distro: `wsl:/home/u/bin/claude`. */
|
|
46
|
+
const WSL_PREFIX = 'wsl:';
|
|
47
|
+
|
|
48
|
+
/** Env names that must never cross the boundary: each side has its own. */
|
|
49
|
+
const ENV_NEVER_FORWARD = new Set(
|
|
50
|
+
[
|
|
51
|
+
'PATH', 'HOME', 'USER', 'USERNAME', 'USERPROFILE', 'LOGNAME', 'SHELL',
|
|
52
|
+
'PWD', 'OLDPWD', 'TEMP', 'TMP', 'TMPDIR', 'APPDATA', 'LOCALAPPDATA',
|
|
53
|
+
'PROGRAMFILES', 'PROGRAMFILES(X86)', 'PROGRAMDATA', 'SYSTEMROOT',
|
|
54
|
+
'SYSTEMDRIVE', 'WINDIR', 'COMSPEC', 'PATHEXT', 'WSLENV', 'WSL_DISTRO_NAME',
|
|
55
|
+
'WSL_INTEROP', 'NODE_OPTIONS', 'NVM_DIR', 'NVM_BIN', 'VOLTA_HOME',
|
|
56
|
+
// getEnhancedEnv() sets LANG for the Windows console's benefit. Ubuntu
|
|
57
|
+
// generates C.UTF-8 and not en_US.UTF-8, so forwarding it makes every CLI
|
|
58
|
+
// in the distro open with a setlocale warning it had no reason to print.
|
|
59
|
+
'LANG', 'LC_ALL',
|
|
60
|
+
].map((n) => n.toUpperCase()),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Which side of the boundary are we on?
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
let inWslCache;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* True when THIS process is running inside a WSL distro.
|
|
71
|
+
*
|
|
72
|
+
* WSL_DISTRO_NAME is set by modern WSL but is lost by anything that sanitises
|
|
73
|
+
* the environment, so the kernel release string — which carries "microsoft" on
|
|
74
|
+
* both WSL1 and WSL2 — is the authority and the env var only a fast path.
|
|
75
|
+
*/
|
|
76
|
+
function isRunningInWsl() {
|
|
77
|
+
if (inWslCache !== undefined) return inWslCache;
|
|
78
|
+
inWslCache = false;
|
|
79
|
+
if (process.platform !== 'linux') return inWslCache;
|
|
80
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) {
|
|
81
|
+
inWslCache = true;
|
|
82
|
+
return inWslCache;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const release = fs.readFileSync('/proc/sys/kernel/osrelease', 'utf-8');
|
|
86
|
+
inWslCache = /microsoft|wsl/i.test(release);
|
|
87
|
+
} catch {
|
|
88
|
+
/* not Linux-with-procfs — leave false */
|
|
89
|
+
}
|
|
90
|
+
return inWslCache;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Windows side: is there a distro to talk to at all?
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
let distrosCache = { value: null, at: 0 };
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Registered, non-broken distro names, default first.
|
|
101
|
+
*
|
|
102
|
+
* `wsl.exe -l -q` writes UTF-16LE (it is one of wsl.exe's own management
|
|
103
|
+
* commands, unlike `-e` which passes the child's bytes straight through), so it
|
|
104
|
+
* is read as a buffer and decoded explicitly — a utf-8 read yields NUL-riddled
|
|
105
|
+
* garbage that no name ever matches.
|
|
106
|
+
*
|
|
107
|
+
* An empty list is the normal answer on a machine where `wsl --install` was
|
|
108
|
+
* never finished: wsl.exe EXISTS in System32 on every Windows 11 install, so
|
|
109
|
+
* its presence proves nothing and only a registered distro does.
|
|
110
|
+
*/
|
|
111
|
+
function wslDistros() {
|
|
112
|
+
if (!IS_WINDOWS) return [];
|
|
113
|
+
if (distrosCache.value && Date.now() - distrosCache.at < CACHE_TTL_MS) {
|
|
114
|
+
return [...distrosCache.value];
|
|
115
|
+
}
|
|
116
|
+
let names = [];
|
|
117
|
+
try {
|
|
118
|
+
const buf = execFileSync('wsl.exe', ['-l', '-q'], {
|
|
119
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
120
|
+
windowsHide: true,
|
|
121
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
122
|
+
maxBuffer: 1024 * 1024,
|
|
123
|
+
});
|
|
124
|
+
names = buf
|
|
125
|
+
.toString('utf16le')
|
|
126
|
+
.split(/\r?\n/)
|
|
127
|
+
.map((l) => l.replace(/\0/g, '').trim())
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
} catch {
|
|
130
|
+
names = [];
|
|
131
|
+
}
|
|
132
|
+
distrosCache = { value: names, at: Date.now() };
|
|
133
|
+
return [...names];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** True when a WSL distro is registered and reachable from this Windows host. */
|
|
137
|
+
function isWslAvailable() {
|
|
138
|
+
return IS_WINDOWS && wslDistros().length > 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The distro every bridged command targets — wsl.exe's own default. */
|
|
142
|
+
function defaultDistro() {
|
|
143
|
+
return wslDistros()[0] || null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Windows side: reaching into the distro's filesystem without starting one
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
let uncRootCache = { value: null, at: 0 };
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The UNC prefix that maps the default distro's root into Windows, or null.
|
|
154
|
+
* WSL2 serves \\wsl.localhost\<distro>; WSL1 and older builds only \\wsl$\.
|
|
155
|
+
*/
|
|
156
|
+
function _uncRoot() {
|
|
157
|
+
if (uncRootCache.at && Date.now() - uncRootCache.at < CACHE_TTL_MS) {
|
|
158
|
+
return uncRootCache.value;
|
|
159
|
+
}
|
|
160
|
+
const distro = defaultDistro();
|
|
161
|
+
if (!distro) return null;
|
|
162
|
+
let root = null;
|
|
163
|
+
for (const prefix of ['\\\\wsl.localhost\\', '\\\\wsl$\\']) {
|
|
164
|
+
const candidate = prefix + distro;
|
|
165
|
+
try {
|
|
166
|
+
if (fs.existsSync(candidate)) { root = candidate; break; }
|
|
167
|
+
} catch {
|
|
168
|
+
/* an unreachable share throws rather than returning false */
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
uncRootCache = { value: root, at: Date.now() };
|
|
172
|
+
return root;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The distro's $HOME as a path Windows can read, or null.
|
|
177
|
+
*
|
|
178
|
+
* An agent installed in WSL keeps its sign-in where IT lives — ~/.claude,
|
|
179
|
+
* ~/.gemini, ~/.codex inside the distro — so a readiness check that expands "~"
|
|
180
|
+
* against the Windows profile finds nothing and calls a signed-in agent
|
|
181
|
+
* "Not logged in".
|
|
182
|
+
*/
|
|
183
|
+
function wslHomeUnc() {
|
|
184
|
+
const home = _shellProbe().home;
|
|
185
|
+
return home ? toUncPath(home) : null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Map an in-distro absolute path to the UNC path Windows can stat. */
|
|
189
|
+
function toUncPath(linuxPath) {
|
|
190
|
+
const root = _uncRoot();
|
|
191
|
+
if (!root || !linuxPath || !linuxPath.startsWith('/')) return null;
|
|
192
|
+
return root + linuxPath.replace(/\//g, '\\');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Windows side: what the distro's login shell would put on PATH
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
const PROBE_DELIM = '__OPENAGENTS_WSL__';
|
|
200
|
+
let shellProbeCache = { value: null, at: 0 };
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* One wsl.exe call per process (30s TTL) that answers everything later lookups
|
|
204
|
+
* need: the login PATH, $HOME, and the real automount root.
|
|
205
|
+
*
|
|
206
|
+
* `bash -ilc` — INTERACTIVE as well as login — because the version managers
|
|
207
|
+
* that actually hold the agent CLIs write to ~/.bashrc, and Ubuntu's stock
|
|
208
|
+
* ~/.bashrc returns early for a non-interactive shell BEFORE the nvm/fnm block
|
|
209
|
+
* at the bottom ever runs. A plain `-lc` therefore reports a PATH with no nvm
|
|
210
|
+
* in it, which is precisely the install this whole module exists to find.
|
|
211
|
+
* stderr is dropped: an interactive shell with no tty warns about job control.
|
|
212
|
+
*/
|
|
213
|
+
function _shellProbe() {
|
|
214
|
+
const empty = { dirs: [], home: null, automount: '/mnt/' };
|
|
215
|
+
if (shellProbeCache.at && Date.now() - shellProbeCache.at < CACHE_TTL_MS) {
|
|
216
|
+
return shellProbeCache.value || empty;
|
|
217
|
+
}
|
|
218
|
+
if (!isWslAvailable()) return empty;
|
|
219
|
+
|
|
220
|
+
const script =
|
|
221
|
+
'echo ' + PROBE_DELIM + '; echo "$PATH"; echo "$HOME"; ' +
|
|
222
|
+
"wslpath -u 'C:\\' 2>/dev/null || echo /mnt/c/";
|
|
223
|
+
|
|
224
|
+
let out = null;
|
|
225
|
+
for (const argv of [['-e', 'bash', '-ilc', script], ['-e', 'sh', '-lc', script]]) {
|
|
226
|
+
try {
|
|
227
|
+
const raw = execFileSync('wsl.exe', argv, {
|
|
228
|
+
encoding: 'utf-8',
|
|
229
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
230
|
+
windowsHide: true,
|
|
231
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
232
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
233
|
+
});
|
|
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;
|
|
257
|
+
} catch {
|
|
258
|
+
/* try the next shell */
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
shellProbeCache = { value: out || empty, at: Date.now() };
|
|
262
|
+
return shellProbeCache.value;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Every in-distro directory an agent CLI can plausibly live in: the login
|
|
267
|
+
* PATH first (it is the ground truth for how the user installed things), then
|
|
268
|
+
* the same curated set paths.js keeps for native Linux, so a CLI whose
|
|
269
|
+
* installer only edited a shell rc file we could not read is still found.
|
|
270
|
+
*/
|
|
271
|
+
function wslBinDirs() {
|
|
272
|
+
const probe = _shellProbe();
|
|
273
|
+
const dirs = [...probe.dirs];
|
|
274
|
+
const home = probe.home;
|
|
275
|
+
const push = (d) => { if (d && !dirs.includes(d)) dirs.push(d); };
|
|
276
|
+
if (home) {
|
|
277
|
+
push(home + '/.local/bin');
|
|
278
|
+
push(home + '/.npm-global/bin');
|
|
279
|
+
push(home + '/.openagents/nodejs/node_modules/.bin');
|
|
280
|
+
push(home + '/.cursor/bin');
|
|
281
|
+
push(home + '/.amp/bin');
|
|
282
|
+
push(home + '/.opencode/bin');
|
|
283
|
+
push(home + '/.cargo/bin');
|
|
284
|
+
push(home + '/bin');
|
|
285
|
+
}
|
|
286
|
+
push('/usr/local/bin');
|
|
287
|
+
push('/usr/bin');
|
|
288
|
+
push('/home/linuxbrew/.linuxbrew/bin');
|
|
289
|
+
return dirs;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const wslBinaryCache = new Map();
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Resolve an agent binary inside the default distro, or null.
|
|
296
|
+
*
|
|
297
|
+
* Returns the path as the DISTRO sees it, behind the `wsl:` marker
|
|
298
|
+
* (`wsl:/home/u/.local/bin/claude`) — the inner path is what `wsl.exe -e` must
|
|
299
|
+
* 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.
|
|
301
|
+
*
|
|
302
|
+
* @param {string|string[]} names binary name(s), e.g. 'claude' or ['cursor-agent','agent']
|
|
303
|
+
* @returns {string|null} marked in-distro path
|
|
304
|
+
*/
|
|
305
|
+
function resolveWslBinary(names) {
|
|
306
|
+
if (!IS_WINDOWS) return null;
|
|
307
|
+
const list = (Array.isArray(names) ? names : [names]).filter(Boolean);
|
|
308
|
+
if (!list.length) return null;
|
|
309
|
+
const key = list.join('\0');
|
|
310
|
+
const cached = wslBinaryCache.get(key);
|
|
311
|
+
if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.value;
|
|
312
|
+
|
|
313
|
+
let value = null;
|
|
314
|
+
if (isWslAvailable() && _uncRoot()) {
|
|
315
|
+
const dirs = wslBinDirs();
|
|
316
|
+
search:
|
|
317
|
+
for (const dir of dirs) {
|
|
318
|
+
for (const name of list) {
|
|
319
|
+
const linuxPath = dir.replace(/\/+$/, '') + '/' + name;
|
|
320
|
+
const unc = toUncPath(linuxPath);
|
|
321
|
+
if (!unc) continue;
|
|
322
|
+
try {
|
|
323
|
+
if (fs.existsSync(unc) && fs.statSync(unc).isFile()) {
|
|
324
|
+
value = WSL_PREFIX + linuxPath;
|
|
325
|
+
break search;
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
/* unreadable path — keep looking */
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
wslBinaryCache.set(key, { value, at: Date.now() });
|
|
334
|
+
return value;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Forget which binaries were found, so an agent installed into the distro while
|
|
339
|
+
* the launcher is already running is picked up. Called from
|
|
340
|
+
* paths.clearBinaryLookupCache(); the distro listing and the login-shell probe
|
|
341
|
+
* survive it deliberately — they each cost a wsl.exe start, and they describe
|
|
342
|
+
* the machine rather than any one install.
|
|
343
|
+
*/
|
|
344
|
+
function clearWslBinaryCache() {
|
|
345
|
+
wslBinaryCache.clear();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Full reset, including the per-process probes. For tests and rare rescans. */
|
|
349
|
+
function clearWslCache() {
|
|
350
|
+
distrosCache = { value: null, at: 0 };
|
|
351
|
+
uncRootCache = { value: null, at: 0 };
|
|
352
|
+
shellProbeCache = { value: null, at: 0 };
|
|
353
|
+
wslBinaryCache.clear();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Classifying a resolved binary
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
/** A binary that lives inside WSL and must be run through wsl.exe. */
|
|
361
|
+
function isWslBinary(bin) {
|
|
362
|
+
return typeof bin === 'string' && bin.startsWith(WSL_PREFIX);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** The in-distro path behind the marker, for anything that has to see it raw. */
|
|
366
|
+
function wslBinaryPath(bin) {
|
|
367
|
+
return isWslBinary(bin) ? bin.slice(WSL_PREFIX.length) : bin;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* A Windows binary seen from inside a distro, classified by whether it can
|
|
372
|
+
* actually be run from here:
|
|
373
|
+
* 'exe' — a real PE image; Linux exec + WSL interop runs it with argv intact.
|
|
374
|
+
* 'shim' — a .cmd/.bat batch file. Only cmd.exe can run one, and cmd.exe
|
|
375
|
+
* rebuilds its command line from a single string, so every &, |, ^,
|
|
376
|
+
* > and " in a prompt would be reinterpreted as shell syntax. We
|
|
377
|
+
* refuse rather than corrupt (or worse, execute) user text.
|
|
378
|
+
*
|
|
379
|
+
* `inWsl` is injectable for the same reason win-exec.ts injects its platform:
|
|
380
|
+
* this branch only ever runs inside a distro, and a test that can only run
|
|
381
|
+
* there is a test that never runs.
|
|
382
|
+
*/
|
|
383
|
+
function windowsBinaryKind(bin, inWsl = isRunningInWsl()) {
|
|
384
|
+
if (!inWsl || typeof bin !== 'string') return null;
|
|
385
|
+
if (/\.exe$/i.test(bin)) return 'exe';
|
|
386
|
+
if (/\.(cmd|bat)$/i.test(bin)) return 'shim';
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Path as the Windows side sees it (best effort; UNC for in-distro paths). */
|
|
391
|
+
function toWindowsPath(p) {
|
|
392
|
+
if (!p || !p.startsWith('/')) return p;
|
|
393
|
+
const root = isRunningInWsl() ? '/mnt/' : _shellProbe().automount;
|
|
394
|
+
const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
395
|
+
const m = new RegExp('^' + escaped + '([a-z])(/|$)', 'i').exec(p);
|
|
396
|
+
if (m) {
|
|
397
|
+
return m[1].toUpperCase() + ':\\' + p.slice(m[0].length).replace(/\//g, '\\');
|
|
398
|
+
}
|
|
399
|
+
if (isRunningInWsl()) {
|
|
400
|
+
const distro = process.env.WSL_DISTRO_NAME;
|
|
401
|
+
return distro ? '\\\\wsl.localhost\\' + distro + p.replace(/\//g, '\\') : p;
|
|
402
|
+
}
|
|
403
|
+
return toUncPath(p) || p;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Path as the distro sees it (best effort; drive letters only). */
|
|
407
|
+
function toWslPath(p) {
|
|
408
|
+
if (!p || !/^[A-Za-z]:[\\/]/.test(p)) return p;
|
|
409
|
+
const root = isRunningInWsl() ? '/mnt/' : _shellProbe().automount;
|
|
410
|
+
return root + p[0].toLowerCase() + '/' + p.slice(3).replace(/\\/g, '/');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
// The spawn bridge
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Which environment entries should cross into the distro.
|
|
419
|
+
*
|
|
420
|
+
* Only what the CALLER added on top of this process's own environment: those
|
|
421
|
+
* are the agent's credentials, endpoints and workspace settings, and they are
|
|
422
|
+
* the entire reason a bridged agent would otherwise start up unauthenticated.
|
|
423
|
+
* Anything inherited unchanged from Windows is dropped — the distro has its own
|
|
424
|
+
* PATH/HOME/TEMP, and handing it Windows values breaks the shell before the
|
|
425
|
+
* agent is even reached.
|
|
426
|
+
*/
|
|
427
|
+
function _forwardedEnvNames(env) {
|
|
428
|
+
if (!env) return [];
|
|
429
|
+
const names = [];
|
|
430
|
+
for (const [k, v] of Object.entries(env)) {
|
|
431
|
+
if (v == null) continue;
|
|
432
|
+
if (ENV_NEVER_FORWARD.has(k.toUpperCase())) continue;
|
|
433
|
+
if (Object.prototype.hasOwnProperty.call(process.env, k) && process.env[k] === v) continue;
|
|
434
|
+
names.push(k);
|
|
435
|
+
}
|
|
436
|
+
return names;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* WSLENV is the only channel Windows has for handing variables to a distro —
|
|
441
|
+
* without it `wsl.exe -e` starts with a clean Linux environment and every
|
|
442
|
+
* OPENAGENTS_* / *_API_KEY the adapter set is silently dropped.
|
|
443
|
+
*
|
|
444
|
+
* A value that is a Windows path gets the `/p` flag (and a `;`-separated list
|
|
445
|
+
* of them `/l`) so WSL translates it on the way in, instead of handing the
|
|
446
|
+
* agent a `D:\work` that means nothing on the other side.
|
|
447
|
+
*/
|
|
448
|
+
function _withWslEnv(env) {
|
|
449
|
+
const out = { ...(env || process.env) };
|
|
450
|
+
const entries = [];
|
|
451
|
+
for (const name of _forwardedEnvNames(out)) {
|
|
452
|
+
const value = String(out[name]);
|
|
453
|
+
const looksLikePath = (s) => /^[A-Za-z]:[\\/]/.test(s);
|
|
454
|
+
let flag = '';
|
|
455
|
+
// The list test comes first: a `C:\a;D:\b` also matches the single-path
|
|
456
|
+
// pattern on its first segment, and tagging it /p would hand the distro one
|
|
457
|
+
// mangled string instead of a translated PATH-style list.
|
|
458
|
+
if (value.includes(';') && value.split(';').filter(Boolean).every(looksLikePath)) {
|
|
459
|
+
flag = '/l';
|
|
460
|
+
} else if (looksLikePath(value)) {
|
|
461
|
+
flag = '/p';
|
|
462
|
+
}
|
|
463
|
+
entries.push(name + flag);
|
|
464
|
+
}
|
|
465
|
+
if (out.WSLENV) entries.unshift(out.WSLENV);
|
|
466
|
+
if (entries.length) out.WSLENV = entries.join(':');
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function _existsDir(p) {
|
|
471
|
+
try { return fs.existsSync(p); } catch { return false; }
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Translate argv entries that are Windows paths into the distro's view of them.
|
|
476
|
+
*
|
|
477
|
+
* Adapters pass real paths on the command line — `--add-dir D:\work`,
|
|
478
|
+
* `--config C:\Users\…\settings.json` — and a Windows path handed to a Linux
|
|
479
|
+
* process is simply a file that does not exist. The entry has to BE a path and
|
|
480
|
+
* that path has to EXIST for it to be rewritten, so ordinary prompt text (which
|
|
481
|
+
* is prose, and does not name a real file) is never touched.
|
|
482
|
+
*/
|
|
483
|
+
function _translateArgs(args) {
|
|
484
|
+
return args.map((a) => {
|
|
485
|
+
const s = String(a);
|
|
486
|
+
if (!/^[A-Za-z]:[\\/]/.test(s)) return a;
|
|
487
|
+
if (!_existsDir(s)) return a;
|
|
488
|
+
return toWslPath(s);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Rewrite a (file, args, opts) triple so it runs on the right side of the
|
|
494
|
+
* boundary. A no-op — the same triple back — for every ordinary local binary,
|
|
495
|
+
* which is why callers can use it unconditionally.
|
|
496
|
+
*
|
|
497
|
+
* @throws {Error} when the binary is a Windows .cmd/.bat shim seen from inside
|
|
498
|
+
* WSL: there is no way to run one without letting cmd.exe re-parse the
|
|
499
|
+
* prompt, so the caller gets an actionable message instead of mangled input.
|
|
500
|
+
*
|
|
501
|
+
* @param {{inWsl?: boolean}} [seam] test injection — see windowsBinaryKind.
|
|
502
|
+
*/
|
|
503
|
+
function bridgeSpawn(file, args = [], opts = {}, seam = {}) {
|
|
504
|
+
if (isWslBinary(file)) {
|
|
505
|
+
return {
|
|
506
|
+
file: 'wsl.exe',
|
|
507
|
+
args: ['-e', wslBinaryPath(file), ..._translateArgs(args)],
|
|
508
|
+
opts: {
|
|
509
|
+
...opts,
|
|
510
|
+
env: _withWslEnv(opts.env),
|
|
511
|
+
// wsl.exe inherits this process's Windows cwd and translates it for the
|
|
512
|
+
// distro, so opts.cwd is handed through untouched — but only if it
|
|
513
|
+
// really exists, since spawn() itself fails ENOENT otherwise and that
|
|
514
|
+
// reads as "the agent isn't installed".
|
|
515
|
+
cwd: opts.cwd && _existsDir(opts.cwd) ? opts.cwd : undefined,
|
|
516
|
+
// A shell would be asked to run a Linux path as a Windows command, and
|
|
517
|
+
// there is no process group to detach into on the far side of wsl.exe.
|
|
518
|
+
shell: false,
|
|
519
|
+
detached: false,
|
|
520
|
+
windowsHide: true,
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (windowsBinaryKind(file, seam.inWsl) === 'shim') {
|
|
526
|
+
throw new Error(
|
|
527
|
+
'Cannot run the Windows CLI ' + file + ' from inside WSL: .cmd/.bat shims have to ' +
|
|
528
|
+
'go through cmd.exe, which would re-interpret the prompt text. Install the agent ' +
|
|
529
|
+
'inside this distro (e.g. npm install -g …), or run the launcher on Windows.',
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
// A '.exe' needs no rewrite: WSL interop execs a PE image directly and
|
|
533
|
+
// preserves argv exactly, unlike the shim case above.
|
|
534
|
+
|
|
535
|
+
return { file, args, opts };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* child_process.spawn with the boundary handled. Same signature, same return —
|
|
540
|
+
* drop-in for every adapter that spawns an agent CLI.
|
|
541
|
+
*/
|
|
542
|
+
function spawn(file, args = [], opts = {}) {
|
|
543
|
+
const bridged = bridgeSpawn(file, args, opts);
|
|
544
|
+
return nodeSpawn(bridged.file, bridged.args, bridged.opts);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* A shell command string that runs `<bin> <args…>` across the boundary, for the
|
|
549
|
+
* execSync-based version/verify probes. Returns null when no bridging applies
|
|
550
|
+
* and the caller should build its own command exactly as before.
|
|
551
|
+
*/
|
|
552
|
+
function bridgedCommandString(bin, args = []) {
|
|
553
|
+
if (!isWslBinary(bin)) return null;
|
|
554
|
+
const quoted = [wslBinaryPath(bin), ...args]
|
|
555
|
+
.map((a) => '"' + String(a).replace(/"/g, '\\"') + '"')
|
|
556
|
+
.join(' ');
|
|
557
|
+
return 'wsl.exe -e ' + quoted;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
module.exports = {
|
|
561
|
+
isRunningInWsl,
|
|
562
|
+
isWslAvailable,
|
|
563
|
+
wslDistros,
|
|
564
|
+
defaultDistro,
|
|
565
|
+
wslBinDirs,
|
|
566
|
+
wslHomeUnc,
|
|
567
|
+
resolveWslBinary,
|
|
568
|
+
clearWslBinaryCache,
|
|
569
|
+
clearWslCache,
|
|
570
|
+
isWslBinary,
|
|
571
|
+
wslBinaryPath,
|
|
572
|
+
windowsBinaryKind,
|
|
573
|
+
toWindowsPath,
|
|
574
|
+
toWslPath,
|
|
575
|
+
toUncPath,
|
|
576
|
+
bridgeSpawn,
|
|
577
|
+
bridgedCommandString,
|
|
578
|
+
spawn,
|
|
579
|
+
};
|