dsh-plugin-wsl-env 0.7.5 → 0.9.0

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +345 -8
  2. package/CONTRIBUTING.md +221 -68
  3. package/README.md +50 -35
  4. package/SECURITY.md +18 -2
  5. package/SUPPORT.md +23 -7
  6. package/cordis.patch.yml +9 -0
  7. package/docs/ARCHITECTURE.md +200 -57
  8. package/docs/CONFIGURATION.md +180 -16
  9. package/docs/LIMITATIONS.md +91 -33
  10. package/docs/PARITY.md +66 -38
  11. package/docs/PEER-PARITY.md +95 -32
  12. package/docs/README.zh.md +49 -31
  13. package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
  14. package/docs/UPSTREAM-TERMINAL-TITLE.md +49 -14
  15. package/lib/agent-confined.d.ts +42 -0
  16. package/lib/agent-confined.js +25 -41
  17. package/lib/agent-errors.d.ts +31 -0
  18. package/lib/agent-errors.js +19 -16
  19. package/lib/agent-exec.d.ts +159 -0
  20. package/lib/agent-exec.js +151 -165
  21. package/lib/agent-protocol.d.ts +192 -0
  22. package/lib/agent-protocol.js +76 -100
  23. package/lib/agent-shared.d.ts +27 -0
  24. package/lib/agent-shared.js +12 -12
  25. package/lib/agent.d.ts +353 -0
  26. package/lib/agent.js +536 -552
  27. package/lib/auto-preset.d.ts +110 -0
  28. package/lib/auto-preset.js +157 -141
  29. package/lib/bwrap.d.ts +90 -0
  30. package/lib/bwrap.js +18 -18
  31. package/lib/file-reference-wsl.d.ts +160 -0
  32. package/lib/file-reference-wsl.js +192 -173
  33. package/lib/fs-decisions.d.ts +107 -0
  34. package/lib/fs-decisions.js +64 -60
  35. package/lib/fs-routing.d.ts +182 -0
  36. package/lib/fs-routing.js +173 -116
  37. package/lib/fs-substrate.d.ts +258 -0
  38. package/lib/fs-substrate.js +248 -248
  39. package/lib/fs.d.ts +10 -0
  40. package/lib/fs.js +5 -0
  41. package/lib/fsio-agent.d.ts +307 -0
  42. package/lib/fsio-agent.js +414 -410
  43. package/lib/fsio-text.d.ts +115 -0
  44. package/lib/fsio-text.js +104 -84
  45. package/lib/git-route.d.ts +100 -0
  46. package/lib/git-route.js +46 -47
  47. package/lib/index.d.ts +1003 -0
  48. package/lib/index.js +1106 -1003
  49. package/lib/listing.d.ts +114 -0
  50. package/lib/listing.js +68 -55
  51. package/lib/paths.d.ts +182 -0
  52. package/lib/paths.js +134 -109
  53. package/lib/picker.d.ts +266 -0
  54. package/lib/picker.js +371 -351
  55. package/lib/ports.d.ts +44 -0
  56. package/lib/ports.js +39 -29
  57. package/lib/preflight.d.ts +91 -0
  58. package/lib/preflight.js +94 -0
  59. package/lib/preset-choice.d.ts +33 -0
  60. package/lib/preset-choice.js +15 -7
  61. package/lib/probe-cache.d.ts +51 -0
  62. package/lib/probe-cache.js +43 -29
  63. package/lib/sandbox-core.d.ts +149 -0
  64. package/lib/sandbox-core.js +239 -87
  65. package/lib/sandbox.d.ts +48 -0
  66. package/lib/sandbox.js +4 -4
  67. package/lib/search-exec.d.ts +96 -0
  68. package/lib/search-exec.js +52 -79
  69. package/lib/search-route.d.ts +75 -0
  70. package/lib/search-route.js +30 -31
  71. package/lib/shell-env.d.ts +88 -0
  72. package/lib/shell-env.js +78 -74
  73. package/lib/shell.d.ts +10 -0
  74. package/lib/shell.js +5 -0
  75. package/lib/subprocess.d.ts +313 -0
  76. package/lib/subprocess.js +267 -262
  77. package/lib/terminal-activity.d.ts +98 -0
  78. package/lib/terminal-activity.js +29 -39
  79. package/lib/terminal-route.d.ts +27 -0
  80. package/lib/terminal-route.js +16 -8
  81. package/lib/watcher.d.ts +77 -0
  82. package/lib/watcher.js +120 -126
  83. package/lib/workspace-files-route.d.ts +132 -0
  84. package/lib/workspace-files-route.js +73 -81
  85. package/lib/workspace-files-wsl.d.ts +257 -0
  86. package/lib/workspace-files-wsl.js +311 -251
  87. package/lib/wsl.d.ts +266 -0
  88. package/lib/wsl.js +150 -155
  89. package/locale/preflight/en.json +6 -0
  90. package/locale/preflight/zh.json +6 -0
  91. package/package.json +78 -22
  92. package/scripts/diagnose.sh +92 -0
package/lib/subprocess.js CHANGED
@@ -72,34 +72,34 @@
72
72
  * composition, not of the Session. A Session whose workspace is a Windows folder
73
73
  * gets the same terminal, with that folder mounted at `/mnt/<drive>/…`.
74
74
  *
75
+ * This is a TypeScript source built to `lib/subprocess.js`; edit THIS file and
76
+ * run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
77
+ * fails when it drifts.
78
+ *
75
79
  * @module dsh-plugin-wsl/subprocess
76
80
  */
77
-
78
81
  import { randomUUID } from "node:crypto";
79
82
  import z from "@deepseek-ai/schemastery";
80
83
  import { LocalSubprocessRuntime } from "@deepseek-ai/dsh-subprocess-local";
81
84
  import { isPosixAbsolute, toLinuxPath, uncToPosix, windowsToLinuxMount } from "./paths.js";
82
85
  import { DEFAULT_WSL_PATH, defaultDistro, wslEnvValue, wslTerminalArgv } from "./wsl.js";
83
86
  import { searchSpawnRewrite } from "./search-route.js";
87
+ import { gitSpawnRewrite, translateGitEnv } from "./git-route.js";
84
88
  import { searchExecutionHandle } from "./search-exec.js";
85
89
  import { sharedAgent } from "./agent-shared.js";
86
90
  import { parseTerminalActivity, TERMINAL_ID_ENV, terminalActivityProbe, wrapTerminalHandle } from "./terminal-activity.js";
87
91
  import { AgentUnavailableError } from "./agent-errors.js";
88
92
  import { terminalRoute } from "./terminal-route.js";
89
-
90
93
  /**
91
94
  * Env names a terminal launch keeps for the distro. `TERM` rides along because
92
95
  * the PTY consumer sets it for the host process, and WSL imports no name that
93
96
  * `WSLENV` does not list.
94
97
  */
95
98
  const SAFE_FORWARD = ["TERM", "NO_COLOR", "PAGER", "GIT_PAGER", "LANG", "LC_ALL"];
96
-
97
99
  /** Namespace of the managed facts; every `DSH_*` name in a terminal request is forwarded. */
98
100
  const DSH_ENV_PREFIX = "DSH_";
99
-
100
101
  /** The one program a launch of this provider's does not pass through. */
101
102
  const WSL_PROGRAM = "wsl";
102
-
103
103
  /**
104
104
  * Subprocess provider that serves a WSL distro as the composition's terminal
105
105
  * execution world.
@@ -110,273 +110,278 @@ const WSL_PROGRAM = "wsl";
110
110
  * literally the shipped implementation.
111
111
  */
112
112
  export class WslSubprocessRuntime extends LocalSubprocessRuntime {
113
- static Config = z.object({
114
- /** Distro name; empty means WSL's own default distro. */
115
- distro: z.string().default(""),
116
- /** Path to `wsl.exe`; overridable for a non-standard install. */
117
- wslPath: z.string().default(DEFAULT_WSL_PATH),
113
+ static Config = z.object({
114
+ /** Distro name; empty means WSL's own default distro. */
115
+ distro: z.string().default(""),
116
+ /** Path to `wsl.exe`; overridable for a non-standard install. */
117
+ wslPath: z.string().default(DEFAULT_WSL_PATH),
118
+ /**
119
+ * A shell to pin inside the distro, as an absolute Linux path. Empty — the
120
+ * default — leaves the choice to `wsl.exe`, which starts the distro user's
121
+ * login shell exactly as an interactive `wsl.exe` does.
122
+ */
123
+ shell: z.string().default(""),
124
+ /**
125
+ * Login-shell semantics for a pinned shell. Ignored when no shell is pinned,
126
+ * because then the login shell is the shell.
127
+ */
128
+ loginShell: z.boolean().default(true),
129
+ /** Fallback directory when a launch names none; accepts any coordinate system. */
130
+ cwd: z.string().default(""),
131
+ /** Windows directory the `wsl.exe` process itself starts in. */
132
+ hostCwd: z.string().volatile(),
133
+ /** Extra env names to forward into the distro through WSLENV. */
134
+ forwardEnv: z.array(z.string()).default([]),
135
+ /**
136
+ * Give a session opened on a WINDOWS folder a host shell instead of the
137
+ * distro's shell started at `/mnt/<drive>/…`. WSL-folder sessions always
138
+ * get the distro shell — that is this provider's purpose. `false` restores
139
+ * the composition-owned terminal for every session.
140
+ */
141
+ hostSessions: z.boolean().default(true),
142
+ /**
143
+ * Let the controller reclaim an idle distro terminal. The terminal is
144
+ * marked at launch (`DSH_TERMINAL_ID` through the existing `WSLENV`
145
+ * forwarding) and its handle's `inspectActivity` is replaced with a
146
+ * distro-side observation: the resident agent scans `/proc` for the
147
+ * marker — a shell with live children is busy, a shell without children
148
+ * is idle. The shipped observation can only answer `unknown` for a distro
149
+ * terminal, and `unknown` never accumulates idle, which is why these
150
+ * terminals had to be closed by hand. When the agent is out the
151
+ * observation answers `unknown` (no reclaim) — a probe that cannot answer
152
+ * must not authorize a close. `false` restores that posture.
153
+ */
154
+ terminalIdleReclaim: z.boolean().default(true),
155
+ });
118
156
  /**
119
- * A shell to pin inside the distro, as an absolute Linux path. Empty — the
120
- * default — leaves the choice to `wsl.exe`, which starts the distro user's
121
- * login shell exactly as an interactive `wsl.exe` does.
157
+ * @param ctx - the cordis context.
158
+ * @param config - the schema-validated config.
122
159
  */
123
- shell: z.string().default(""),
160
+ constructor(ctx, config) {
161
+ super(ctx);
162
+ this.config = config;
163
+ this.resolvedDistro = config.distro;
164
+ }
124
165
  /**
125
- * Login-shell semantics for a pinned shell. Ignored when no shell is pinned,
126
- * because then the login shell is the shell.
166
+ * The distro a launch uses when the request's directory does not name one,
167
+ * resolving WSL's default on first use.
168
+ *
169
+ * @returns the distro name.
127
170
  */
128
- loginShell: z.boolean().default(true),
129
- /** Fallback directory when a launch names none; accepts any coordinate system. */
130
- cwd: z.string().default(""),
131
- /** Windows directory the `wsl.exe` process itself starts in. */
132
- hostCwd: z.string().volatile(),
133
- /** Extra env names to forward into the distro through WSLENV. */
134
- forwardEnv: z.array(z.string()).default([]),
171
+ async distro() {
172
+ if (this.resolvedDistro.length === 0)
173
+ this.resolvedDistro = await defaultDistro({ wslPath: this.config.wslPath });
174
+ return this.resolvedDistro;
175
+ }
176
+ /** Windows directory the `wsl.exe` process is started from. */
177
+ hostCwd() {
178
+ return this.config.hostCwd.get() ?? process.env.SystemRoot ?? process.cwd();
179
+ }
135
180
  /**
136
- * Give a session opened on a WINDOWS folder a host shell instead of the
137
- * distro's shell started at `/mnt/<drive>/…`. WSL-folder sessions always
138
- * get the distro shell — that is this provider's purpose. `false` restores
139
- * the composition-owned terminal for every session.
181
+ * The distro and Linux directory one terminal request names.
182
+ *
183
+ * The Session workspace the terminal controller passes in is the coordinate
184
+ * the harness already speaks, and it is also the strongest evidence of the
185
+ * intended distro: a UNC path names one outright. Anything else — a Windows
186
+ * drive path or a directory-less request — may legitimately be served by the
187
+ * configured distro.
188
+ *
189
+ * @param cwd - the request's working directory, in any coordinate system.
190
+ * @returns the distro name and its absolute Linux directory.
140
191
  */
141
- hostSessions: z.boolean().default(true),
192
+ async terminalTarget(cwd) {
193
+ const text = String(cwd ?? "").trim();
194
+ const named = uncToPosix(text);
195
+ if (named !== undefined)
196
+ return { distro: named.distro, linuxCwd: named.linuxPath };
197
+ const distro = await this.distro();
198
+ if (isPosixAbsolute(text))
199
+ return { distro, linuxCwd: text };
200
+ const mounted = windowsToLinuxMount(text);
201
+ if (mounted !== undefined)
202
+ return { distro, linuxCwd: mounted };
203
+ return { distro, linuxCwd: toLinuxPath(this.config.cwd, { distro }) };
204
+ }
142
205
  /**
143
- * Let the controller reclaim an idle distro terminal. The terminal is
144
- * marked at launch (`DSH_TERMINAL_ID` through the existing `WSLENV`
145
- * forwarding) and its handle's `inspectActivity` is replaced with a
146
- * distro-side observation: the resident agent scans `/proc` for the
147
- * marker — a shell with live children is busy, a shell without children
148
- * is idle. The shipped observation can only answer `unknown` for a distro
149
- * terminal, and `unknown` never accumulates idle, which is why these
150
- * terminals had to be closed by hand. When the agent is out the
151
- * observation answers `unknown` (no reclaim) — a probe that cannot answer
152
- * must not authorize a close. `false` restores that posture.
206
+ * Open one interactive shell inside the distro over the host's PTY.
207
+ *
208
+ * Only a `wsl.exe` launch is rewritten. Everything else — a shipped host
209
+ * shell, the PTY backend's `bash -i`, any other consumer's terminal — is the
210
+ * shipped implementation's, which is what keeps replacing a root provider
211
+ * safe.
212
+ *
213
+ * The request's remaining arguments are intentionally not forwarded: the
214
+ * controller's shell profile carries only the program, and a second set of
215
+ * `wsl.exe` arguments would fight the distro and `--cd` this method derives
216
+ * from the Session.
217
+ *
218
+ * @param spec - the terminal request from the PTY consumer.
219
+ * @returns the live terminal handle.
153
220
  */
154
- terminalIdleReclaim: z.boolean().default(true),
155
- });
156
-
157
- constructor(ctx, config) {
158
- super(ctx);
159
- this.config = config;
160
- /** Cached default distro, resolved once from `wsl.exe -l -q`. */
161
- this.resolvedDistro = config.distro;
162
- }
163
-
164
- /**
165
- * The distro a launch uses when the request's directory does not name one,
166
- * resolving WSL's default on first use.
167
- * @returns the distro name.
168
- */
169
- async distro() {
170
- if (this.resolvedDistro.length === 0) this.resolvedDistro = await defaultDistro({ wslPath: this.config.wslPath });
171
- return this.resolvedDistro;
172
- }
173
-
174
- /** Windows directory the `wsl.exe` process is started from. */
175
- hostCwd() {
176
- return this.config.hostCwd.get() ?? process.env.SystemRoot ?? process.cwd();
177
- }
178
-
179
- /**
180
- * The distro and Linux directory one terminal request names.
181
- *
182
- * The Session workspace the terminal controller passes in is the coordinate
183
- * the harness already speaks, and it is also the strongest evidence of the
184
- * intended distro: a UNC path names one outright. Anything else — a Windows
185
- * drive path or a directory-less request — may legitimately be served by the
186
- * configured distro.
187
- *
188
- * @param cwd - the request's working directory, in any coordinate system.
189
- * @returns the distro name and its absolute Linux directory.
190
- */
191
- async terminalTarget(cwd) {
192
- const text = String(cwd ?? "").trim();
193
- const named = uncToPosix(text);
194
- if (named !== undefined) return { distro: named.distro, linuxCwd: named.linuxPath };
195
- const distro = await this.distro();
196
- if (isPosixAbsolute(text)) return { distro, linuxCwd: text };
197
- const mounted = windowsToLinuxMount(text);
198
- if (mounted !== undefined) return { distro, linuxCwd: mounted };
199
- return { distro, linuxCwd: toLinuxPath(this.config.cwd, { distro }) };
200
- }
201
-
202
- /**
203
- * Open one interactive shell inside the distro over the host's PTY.
204
- *
205
- * Only a `wsl.exe` launch is rewritten. Everything else — a shipped host
206
- * shell, the PTY backend's `bash -i`, any other consumer's terminal — is the
207
- * shipped implementation's, which is what keeps replacing a root provider
208
- * safe.
209
- *
210
- * The request's remaining arguments are intentionally not forwarded: the
211
- * controller's shell profile carries only the program, and a second set of
212
- * `wsl.exe` arguments would fight the distro and `--cd` this method derives
213
- * from the Session.
214
- *
215
- * @param spec - the terminal request from the PTY consumer.
216
- * @returns the live terminal handle.
217
- */
218
- async spawnTerminal(spec) {
219
- if (!this.launchesDistro(spec.argv)) return super.spawnTerminal(spec);
220
- // A session on a Windows folder is a HOST session: it gets the host shell
221
- // in its own directory, not the distro's shell at `/mnt/<drive>/…`. The
222
- // wsl.exe argv still names this provider's launch, so the substitution
223
- // stays inside the override — every other consumer is untouched.
224
- if (this.config.hostSessions && terminalRoute(spec.cwd) === "host") {
225
- return super.spawnTerminal({ ...spec, argv: ["powershell.exe", "-NoLogo"] });
226
- }
227
- const target = await this.terminalTarget(spec.cwd);
228
- const argv = wslTerminalArgv({
229
- wslPath: this.config.wslPath,
230
- distro: target.distro,
231
- shellPath: this.config.shell,
232
- linuxCwd: target.linuxCwd,
233
- login: this.config.loginShell,
234
- });
235
- // The idle-reclaim mark: one env name per terminal, riding the WSLENV
236
- // forwarding this launch already does. The distro shell and everything it
237
- // spawns carry it in /proc/<pid>/environ — what the activity probe scans.
238
- const terminalId = this.config.terminalIdleReclaim ? randomUUID() : undefined;
239
- const specWithMark = terminalId === undefined ? spec : { ...spec, env: { ...spec.env, [TERMINAL_ID_ENV]: terminalId } };
240
- const handle = await super.spawnTerminal({
241
- ...specWithMark,
242
- argv,
243
- // A Windows CreateProcess cannot use a Linux path as its working
244
- // directory; the Linux directory rides on `--cd` instead.
245
- cwd: this.hostCwd(),
246
- env: this.forwardedEnv(specWithMark),
247
- });
248
- if (terminalId === undefined) return handle;
249
- return wrapTerminalHandle(handle, this.distroActivity(target.distro, terminalId));
250
- }
251
-
252
- /**
253
- * The replacement `inspectActivity` for one distro terminal: ask the
254
- * resident agent whether the marked shell is at its prompt.
255
- *
256
- * The revision increments only when the reported state changes, so the
257
- * controller's retention accumulates idle across consecutive `idle`
258
- * observations and restarts the clock on every transition. Any probe
259
- * failure — the agent out, the distro stopped, a script error — answers
260
- * `unknown`, which the retention reads as "not idle": the probe that cannot
261
- * answer never authorizes a close. The first unexpected (non-availability)
262
- * failure is logged once, so a broken probe is diagnosable without
263
- * spamming the log every poll.
264
- *
265
- * @param distro - the distro the terminal lives in.
266
- * @param terminalId - the launch's `DSH_TERMINAL_ID` mark.
267
- * @returns the seam's activity observation.
268
- */
269
- distroActivity(distro, terminalId) {
270
- let revision = 0;
271
- let lastState = "";
272
- let warned = false;
273
- return async () => {
274
- try {
275
- const probe = terminalActivityProbe(terminalId);
276
- const result = await sharedAgent(distro).exec({
277
- cwd: probe.cwd,
278
- argv: probe.argv,
279
- maxOutputBytes: 256,
280
- timeoutMs: 10_000,
281
- });
282
- const state = parseTerminalActivity(result.stdout) ?? "unknown";
283
- if (state !== lastState) {
284
- revision += 1;
285
- lastState = state;
221
+ async spawnTerminal(spec) {
222
+ if (!this.launchesDistro(spec.argv))
223
+ return super.spawnTerminal(spec);
224
+ // A session on a Windows folder is a HOST session: it gets the host shell
225
+ // in its own directory, not the distro's shell at `/mnt/<drive>/…`. The
226
+ // wsl.exe argv still names this provider's launch, so the substitution
227
+ // stays inside the override — every other consumer is untouched.
228
+ if (this.config.hostSessions && terminalRoute(spec.cwd) === "host") {
229
+ return super.spawnTerminal({ ...spec, argv: ["powershell.exe", "-NoLogo"] });
286
230
  }
287
- return { state, revision };
288
- } catch (error) {
289
- if (!(error instanceof AgentUnavailableError) && !warned) {
290
- warned = true;
291
- this.ctx.logger?.warn(`wsl-subprocess: the terminal activity probe failed and idle reclamation is paused (${String(error?.message ?? error)})`);
231
+ const target = await this.terminalTarget(spec.cwd);
232
+ const argv = wslTerminalArgv({
233
+ wslPath: this.config.wslPath,
234
+ distro: target.distro,
235
+ shellPath: this.config.shell,
236
+ linuxCwd: target.linuxCwd,
237
+ login: this.config.loginShell,
238
+ });
239
+ // The idle-reclaim mark: one env name per terminal, riding the WSLENV
240
+ // forwarding this launch already does. The distro shell and everything it
241
+ // spawns carry it in /proc/<pid>/environ — what the activity probe scans.
242
+ const terminalId = this.config.terminalIdleReclaim ? randomUUID() : undefined;
243
+ const specWithMark = terminalId === undefined ? spec : { ...spec, env: { ...spec.env, [TERMINAL_ID_ENV]: terminalId } };
244
+ const handle = await super.spawnTerminal({
245
+ ...specWithMark,
246
+ argv,
247
+ // A Windows CreateProcess cannot use a Linux path as its working
248
+ // directory; the Linux directory rides on `--cd` instead.
249
+ cwd: this.hostCwd(),
250
+ // The terminal spec's env is a string record; forwarding adds `WSLENV`.
251
+ env: this.forwardedEnv(specWithMark),
252
+ });
253
+ if (terminalId === undefined)
254
+ return handle;
255
+ return wrapTerminalHandle(handle, this.distroActivity(target.distro, terminalId));
256
+ }
257
+ /**
258
+ * The replacement `inspectActivity` for one distro terminal: ask the
259
+ * resident agent whether the marked shell is at its prompt.
260
+ *
261
+ * The revision increments only when the reported state changes, so the
262
+ * controller's retention accumulates idle across consecutive `idle`
263
+ * observations and restarts the clock on every transition. Any probe
264
+ * failure — the agent out, the distro stopped, a script error — answers
265
+ * `unknown`, which the retention reads as "not idle": the probe that cannot
266
+ * answer never authorizes a close. The first unexpected (non-availability)
267
+ * failure is logged once, so a broken probe is diagnosable without
268
+ * spamming the log every poll.
269
+ *
270
+ * @param distro - the distro the terminal lives in.
271
+ * @param terminalId - the launch's `DSH_TERMINAL_ID` mark.
272
+ * @returns the seam's activity observation.
273
+ */
274
+ distroActivity(distro, terminalId) {
275
+ let revision = 0;
276
+ let lastState = "";
277
+ let warned = false;
278
+ return async () => {
279
+ try {
280
+ const probe = terminalActivityProbe(terminalId);
281
+ const result = await sharedAgent(distro).exec({
282
+ cwd: probe.cwd,
283
+ argv: probe.argv,
284
+ maxOutputBytes: 256,
285
+ timeoutMs: 10_000,
286
+ });
287
+ const state = parseTerminalActivity(result.stdout) ?? "unknown";
288
+ if (state !== lastState) {
289
+ revision += 1;
290
+ lastState = state;
291
+ }
292
+ return { state, revision };
293
+ }
294
+ catch (error) {
295
+ if (!(error instanceof AgentUnavailableError) && !warned) {
296
+ warned = true;
297
+ this.ctx.logger?.warn(`wsl-subprocess: the terminal activity probe failed and idle reclamation is paused (${String(error?.message ?? error)})`);
298
+ }
299
+ return { state: "unknown", revision };
300
+ }
301
+ };
302
+ }
303
+ /**
304
+ * Open one host-side process, rewriting two kinds of launches into the
305
+ * distro: the file-search tool's ripgrep (agent-first, one-shot fallback)
306
+ * and git snapshot commands (one-shot only — snapshot operations are not
307
+ * latency-sensitive, and their stdin relay and collected streams are the
308
+ * shipped handle's). Both decisions are pure coordinate functions
309
+ * (`search-route.js`, `git-route.js`); a spawn neither names (any other
310
+ * program, any drive or relative directory) reaches the shipped
311
+ * implementation verbatim.
312
+ *
313
+ * @param spec - the spawn request.
314
+ * @returns the live search handle.
315
+ */
316
+ spawn(spec) {
317
+ const gitRewrite = gitSpawnRewrite({ argv: spec.argv, cwd: spec.cwd, wslPath: this.config.wslPath });
318
+ if (gitRewrite !== undefined) {
319
+ // The snapshot environment (GIT_INDEX_FILE / GIT_OBJECT_DIRECTORY /
320
+ // GIT_ALTERNATE_OBJECT_DIRECTORIES and the git hygiene flags) is
321
+ // path-shaped in Windows form; the values are translated into distro
322
+ // paths and every forwarded name rides WSLENV. The user's real index,
323
+ // object store, work tree, and refs stay untouched by design — the
324
+ // snapshot runs in the tool's private scratch.
325
+ const { env, wslenv } = translateGitEnv(spec.env);
326
+ return super.spawn({
327
+ ...spec,
328
+ argv: gitRewrite.wslArgv,
329
+ cwd: this.hostCwd(),
330
+ env: wslenv.length > 0 ? { ...env, WSLENV: wslEnvValue(wslenv) } : env,
331
+ });
292
332
  }
293
- return { state: "unknown", revision };
294
- }
295
- };
296
- }
297
-
298
- /**
299
- * Open one host-side process, rewriting two kinds of launches into the
300
- * distro: the file-search tool's ripgrep (agent-first, one-shot fallback)
301
- * and git snapshot commands (one-shot only — snapshot operations are not
302
- * latency-sensitive, and their stdin relay and collected streams are the
303
- * shipped handle's). Both decisions are pure coordinate functions
304
- * (`search-route.js`, `git-route.js`); a spawn neither names (any other
305
- * program, any drive or relative directory) reaches the shipped
306
- * implementation verbatim.
307
- *
308
- * @param spec - the spawn request.
309
- * @returns the live search handle.
310
- */
311
- spawn(spec) {
312
- const gitRewrite = gitSpawnRewrite({ argv: spec.argv, cwd: spec.cwd, wslPath: this.config.wslPath });
313
- if (gitRewrite !== undefined) {
314
- // The snapshot environment (GIT_INDEX_FILE / GIT_OBJECT_DIRECTORY /
315
- // GIT_ALTERNATE_OBJECT_DIRECTORIES and the git hygiene flags) is
316
- // path-shaped in Windows form; the values are translated into distro
317
- // paths and every forwarded name rides WSLENV. The user's real index,
318
- // object store, work tree, and refs stay untouched by design — the
319
- // snapshot runs in the tool's private scratch.
320
- const { env, wslenv } = translateGitEnv(spec.env);
321
- return super.spawn({
322
- ...spec,
323
- argv: gitRewrite.wslArgv,
324
- cwd: this.hostCwd(),
325
- env: wslenv.length > 0 ? { ...env, WSLENV: wslEnvValue(wslenv) } : env,
326
- });
333
+ const rewrite = searchSpawnRewrite({ argv: spec.argv, cwd: spec.cwd, wslPath: this.config.wslPath });
334
+ if (rewrite === undefined)
335
+ return super.spawn(spec);
336
+ // The search handle carries the slice `dsh-tool-fs-search` consumes
337
+ // (`done`, `collected`, `kill`); the seam's full handle surface is the
338
+ // one-shot fallback's, so the override's declared return is the cast's.
339
+ return searchExecutionHandle({
340
+ agent: sharedAgent(rewrite.distro),
341
+ cwd: rewrite.linuxCwd,
342
+ argv: rewrite.rgArgv,
343
+ // One budget serves both streams; the stdout budget is the binding one,
344
+ // and a pathological stderr is still bounded by it.
345
+ maxOutputBytes: spec.stdio.stdout?.maxBytes ?? 0,
346
+ signal: spec.signal,
347
+ spawnFallback: () => super.spawn({
348
+ ...spec,
349
+ argv: rewrite.wslArgv,
350
+ // `wsl.exe --cd` carries the Linux directory; the host process itself
351
+ // must start in a Windows directory, as with a terminal launch.
352
+ cwd: this.hostCwd(),
353
+ env: this.forwardedEnv(spec),
354
+ }),
355
+ });
356
+ }
357
+ /**
358
+ * The environment one `wsl.exe` launch forwards: the safe list plus every
359
+ * managed `DSH_*` name, admitted through `WSLENV` and never `PATH` — see the
360
+ * WSLENV note in the recipes.
361
+ *
362
+ * @param spec - the launch request whose env is forwarded.
363
+ * @returns the environment the host launch carries.
364
+ */
365
+ forwardedEnv(spec) {
366
+ const forward = new Set([...SAFE_FORWARD, ...this.config.forwardEnv]);
367
+ const imported = Object.keys(spec.env ?? {}).filter((name) => forward.has(name) || name.startsWith(DSH_ENV_PREFIX));
368
+ return imported.length > 0 ? { ...spec.env, WSLENV: wslEnvValue(imported) } : spec.env;
369
+ }
370
+ /**
371
+ * Whether a terminal request is a `wsl.exe` launch.
372
+ *
373
+ * A bare `argv[0]` is judged by its own name, so the shipped program name and
374
+ * an operator-pinned absolute path both match.
375
+ *
376
+ * @param argv - the request's program and arguments.
377
+ * @returns true when this provider owns the launch.
378
+ */
379
+ launchesDistro(argv) {
380
+ const program = argv?.[0];
381
+ if (typeof program !== "string" || program.length === 0)
382
+ return false;
383
+ const base = program.slice(Math.max(program.lastIndexOf("/"), program.lastIndexOf("\\")) + 1);
384
+ return base.toLowerCase().replace(/\.exe$/u, "") === WSL_PROGRAM;
327
385
  }
328
- const rewrite = searchSpawnRewrite({ argv: spec.argv, cwd: spec.cwd, wslPath: this.config.wslPath });
329
- if (rewrite === undefined) return super.spawn(spec);
330
- return searchExecutionHandle({
331
- agent: sharedAgent(rewrite.distro),
332
- cwd: rewrite.linuxCwd,
333
- argv: rewrite.rgArgv,
334
- // One budget serves both streams; the stdout budget is the binding one,
335
- // and a pathological stderr is still bounded by it.
336
- maxOutputBytes: spec.stdio?.stdout?.maxBytes ?? 0,
337
- signal: spec.signal,
338
- spawnFallback: () =>
339
- super.spawn({
340
- ...spec,
341
- argv: rewrite.wslArgv,
342
- // `wsl.exe --cd` carries the Linux directory; the host process itself
343
- // must start in a Windows directory, as with a terminal launch.
344
- cwd: this.hostCwd(),
345
- env: this.forwardedEnv(spec),
346
- }),
347
- });
348
- }
349
-
350
- /**
351
- * The environment one `wsl.exe` launch forwards: the safe list plus every
352
- * managed `DSH_*` name, admitted through `WSLENV` and never `PATH` — see the
353
- * WSLENV note in the recipes.
354
- * @param spec - the launch request whose env is forwarded.
355
- * @returns the environment the host launch carries.
356
- */
357
- forwardedEnv(spec) {
358
- const forward = new Set([...SAFE_FORWARD, ...this.config.forwardEnv]);
359
- const imported = Object.keys(spec.env ?? {}).filter(
360
- (name) => forward.has(name) || name.startsWith(DSH_ENV_PREFIX),
361
- );
362
- return imported.length > 0 ? { ...spec.env, WSLENV: wslEnvValue(imported) } : spec.env;
363
- }
364
-
365
- /**
366
- * Whether a terminal request is a `wsl.exe` launch.
367
- *
368
- * A bare `argv[0]` is judged by its own name, so the shipped program name and
369
- * an operator-pinned absolute path both match.
370
- *
371
- * @param argv - the request's program and arguments.
372
- * @returns true when this provider owns the launch.
373
- */
374
- launchesDistro(argv) {
375
- const program = argv?.[0];
376
- if (typeof program !== "string" || program.length === 0) return false;
377
- const base = program.slice(Math.max(program.lastIndexOf("/"), program.lastIndexOf("\\")) + 1);
378
- return base.toLowerCase().replace(/\.exe$/u, "") === WSL_PROGRAM;
379
- }
380
386
  }
381
-
382
387
  export default WslSubprocessRuntime;