dsh-plugin-wsl-env 0.16.0 → 0.17.1

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.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The agent-backed execution handle for the git-snapshot spawn.
3
+ *
4
+ * `workspace-changes`' `GitRunner` consumes a small slice of the subprocess
5
+ * handle: `handle.done` for `{exitCode, signal}`, then
6
+ * `handle.collected.stdout/stderr.readFrom(0)` for `{text, lossy}` — with
7
+ * per-stream caps applied by the caller's `stdio` spec (a generous stdout
8
+ * budget and a 16 KiB stderr tail). This module builds exactly that surface
9
+ * around one resident-agent `exec`, so a distro snapshot pays the warm agent's
10
+ * ~2 ms round trip per git call instead of the one-shot `wsl.exe`'s ~230 ms
11
+ * process startup — measured 2026-11-01 on `ubuntu`; a full snapshot's 6-8
12
+ * calls (plus per-file `cat-file`) used to spend over a second inside
13
+ * `wsl.exe` alone.
14
+ *
15
+ * The caps map onto the agent's single per-request capture ceiling as the
16
+ * larger of the two stream budgets; the reader then slices each stream to its
17
+ * own budget and reports `lossy` from that slice, so the caller's contract
18
+ * (per-stream cut flags) holds even though the wire carries one cap.
19
+ *
20
+ * Environment rides the agent's per-request `env` (an `env --` argv prefix —
21
+ * no `SETENV` global pollution), holding only the translated `GIT_*` names:
22
+ * the one-shot path forwards exactly those through `WSLENV`, and forwarding a
23
+ * host `PATH` into the distro would be wrong, not just different.
24
+ *
25
+ * The agent being out is the fallback cue, exactly as `search-exec.ts`:
26
+ * `spawnFallback` (the caller's one-shot `wsl.exe` handle) is consulted
27
+ * lazily inside `done`, and from that point the facade proxies the delegate
28
+ * verbatim. Cancellation shares one channel: the caller's `signal` and
29
+ * `kill()` abort the agent exec, settling `done` as killed.
30
+ *
31
+ * Pure shaping over injected collaborators, unit-testable without a distro.
32
+ *
33
+ * This is a TypeScript source built to `lib/git-exec.js`; edit THIS file and
34
+ * run `pnpm run build` — the artifact under `lib/` is generated, and
35
+ * `pnpm test` fails when it drifts.
36
+ *
37
+ * @module dsh-plugin-wsl/git-exec
38
+ */
39
+ import { AgentUnavailableError } from "./agent-errors.js";
40
+ import { translateDiscoveryOutput } from "./git-route.js";
41
+ /**
42
+ * Run one git command through the agent and present it as a git-snapshot
43
+ * handle.
44
+ *
45
+ * @param object - the handle's collaborators.
46
+ * @returns the snapshot handle (`done`, `collected`, `kill`).
47
+ */
48
+ export function gitExecutionHandle({ agent, cwd, argv, env, stdin, caps, distro, discovery, signal, spawnFallback }) {
49
+ const state = { delegate: undefined, stdout: undefined, stderr: undefined };
50
+ const controller = new AbortController();
51
+ if (signal?.aborted)
52
+ controller.abort(signal.reason);
53
+ else
54
+ signal?.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
55
+ // The agent enforces ONE cap per request; declare the larger budget on the
56
+ // wire and cut each stream to its own budget here, so the caller's
57
+ // per-stream contract holds and a stream under its cap is never flagged.
58
+ const wireCap = Math.max(caps.stdout, caps.stderr);
59
+ const reader = (slot) => {
60
+ const cap = slot === "stdout" ? caps.stdout : caps.stderr;
61
+ return {
62
+ readFrom(from) {
63
+ const buffer = state[slot] ?? Buffer.alloc(0);
64
+ const lossy = buffer.length > cap;
65
+ const visible = lossy ? buffer.subarray(0, cap) : buffer;
66
+ const start = Math.min(from, visible.length);
67
+ return { text: visible.subarray(start).toString("utf8"), lossy, nextOffset: visible.length };
68
+ },
69
+ };
70
+ };
71
+ const handle = {
72
+ get collected() {
73
+ if (state.delegate !== undefined)
74
+ return state.delegate.collected;
75
+ return { stdout: reader("stdout"), stderr: reader("stderr") };
76
+ },
77
+ done: (async () => {
78
+ let result;
79
+ try {
80
+ result = await agent.exec({ cwd, argv, env, stdin, maxOutputBytes: wireCap, signal: controller.signal });
81
+ state.stdout = discovery ? translateDiscoveryOutput(distro ?? "", result.stdout) : result.stdout;
82
+ state.stderr = result.stderr;
83
+ }
84
+ catch (error) {
85
+ if (!(error instanceof AgentUnavailableError)) {
86
+ if (controller.signal.aborted) {
87
+ // Our kill() or the caller's cancellation: the killed outcome the
88
+ // one-shot path reports, not a failure.
89
+ return { exitCode: null, signal: "SIGTERM" };
90
+ }
91
+ throw error;
92
+ }
93
+ const delegate = spawnFallback();
94
+ state.delegate = delegate;
95
+ return delegate.done;
96
+ }
97
+ return { exitCode: result.exitCode, signal: null };
98
+ })(),
99
+ kill() {
100
+ controller.abort(new Error("killed"));
101
+ state.delegate?.kill?.();
102
+ },
103
+ };
104
+ return handle;
105
+ }
@@ -13,19 +13,28 @@
13
13
  * ## What is routed, and what deliberately is not
14
14
  *
15
15
  * A spawn is routed only when the working directory names the distro (a
16
- * `\\wsl.localhost\<distro>` UNC) and the argv carries none of the discovery
17
- * flags (`--show-toplevel`, `--absolute-git-dir`, `--git-path`). Those three
18
- * print **absolute paths**, which `workspace-changes` resolves against the
19
- * UNC workspace (`git.ts` `resolve(cwd, line)`) — a distro git would answer
20
- * in Linux paths and corrupt that resolution. Host git answers in UNC form,
21
- * which is exactly what the caller wants, and the discovery call is one
22
- * cheap command rather than a work-tree scan. Everything else — `add`,
23
- * `write-tree`, `ls-tree`, `cat-file`, `diff-tree`, `ls-files`,
24
- * `check-ignore` — is safe to route: snapshot calls run in an environment
25
- * fully isolated from the user's git state (`GIT_INDEX_FILE` /
26
- * `GIT_OBJECT_DIRECTORY` / `GIT_ALTERNATE_OBJECT_DIRECTORIES` pointing at the
27
- * tool's private scratch), so the distro git mutates scratch, never the
28
- * repository.
16
+ * `\\wsl.localhost\\<distro>` UNC). Inside that, two cases:
17
+ *
18
+ * - **Snapshot calls** (`add`, `write-tree`, `ls-tree`, `cat-file`,
19
+ * `diff-tree`, `ls-files`, `check-ignore`) route whenever the program is
20
+ * git — the runs are fully isolated from the user's git state
21
+ * (`GIT_INDEX_FILE` / `GIT_OBJECT_DIRECTORY` /
22
+ * `GIT_ALTERNATE_OBJECT_DIRECTORIES` pointing at the tool's private
23
+ * scratch), so the distro git mutates scratch, never the repository.
24
+ * - **Discovery calls** (`--show-toplevel`, `--absolute-git-dir`, `--git-path`)
25
+ * print absolute paths that the caller resolves against the UNC workspace,
26
+ * so a distro answer in Linux paths would corrupt that resolution. They
27
+ * route only when the program is the SENTINEL path `DISTRO_GIT_SENTINEL` —
28
+ * the guard `resolveExecutable` returns when the HOST has no git — and the
29
+ * facade translates the output lines into UNC form before the caller sees
30
+ * them (`translateDiscoveryOutput`). A host-resolved git keeps the shipped
31
+ * host-side discovery, which answers UNC natively (and needs the documented
32
+ * `safe.directory` whitelist for WSL shares).
33
+ *
34
+ * The split means a machine without Windows git still gets full WSL change
35
+ * review: the sentinel resolves, every spawn routes, nothing touches the
36
+ * host. See `lib/git-exec.js` for the facade side.
37
+ *
29
38
  *
30
39
  * ## Environment translation
31
40
  *
@@ -44,6 +53,14 @@
44
53
  *
45
54
  * @module dsh-plugin-wsl/git-route
46
55
  */
56
+ /**
57
+ * The program path `resolveExecutable` returns for git when the HOST has no
58
+ * git: a Linux absolute path that only makes sense inside the distro, so any
59
+ * spawn carrying it must route (and a discovery spawn's output must come back
60
+ * translated). Chosen to match a standard distro install so the argv stays
61
+ * honest even where it is read verbatim.
62
+ */
63
+ export declare const DISTRO_GIT_SENTINEL = "/usr/bin/git";
47
64
  /**
48
65
  * Whether a spawn's program is git.
49
66
  *
@@ -97,4 +114,17 @@ export declare function gitSpawnRewrite({ argv, cwd, wslPath }: {
97
114
  linuxCwd: string;
98
115
  gitArgv: string[];
99
116
  wslArgv: string[];
117
+ discovery?: boolean;
100
118
  } | undefined;
119
+ /**
120
+ * Translate one routed discovery call's stdout into the caller's UNC
121
+ * coordinate system: every absolute Linux path line becomes
122
+ * `\\wsl.localhost\<distro><path>`; relative lines (`--git-path` answers
123
+ * `.git/objects` when the repository is not bare) pass through untouched,
124
+ * because the caller resolves them against the UNC cwd natively.
125
+ *
126
+ * @param distro - the distro the discovery ran in.
127
+ * @param stdout - the distro git's captured stdout.
128
+ * @returns the stdout with absolute path lines rewritten.
129
+ */
130
+ export declare function translateDiscoveryOutput(distro: string, stdout: Buffer): Buffer;
package/lib/git-route.js CHANGED
@@ -13,19 +13,28 @@
13
13
  * ## What is routed, and what deliberately is not
14
14
  *
15
15
  * A spawn is routed only when the working directory names the distro (a
16
- * `\\wsl.localhost\<distro>` UNC) and the argv carries none of the discovery
17
- * flags (`--show-toplevel`, `--absolute-git-dir`, `--git-path`). Those three
18
- * print **absolute paths**, which `workspace-changes` resolves against the
19
- * UNC workspace (`git.ts` `resolve(cwd, line)`) — a distro git would answer
20
- * in Linux paths and corrupt that resolution. Host git answers in UNC form,
21
- * which is exactly what the caller wants, and the discovery call is one
22
- * cheap command rather than a work-tree scan. Everything else — `add`,
23
- * `write-tree`, `ls-tree`, `cat-file`, `diff-tree`, `ls-files`,
24
- * `check-ignore` — is safe to route: snapshot calls run in an environment
25
- * fully isolated from the user's git state (`GIT_INDEX_FILE` /
26
- * `GIT_OBJECT_DIRECTORY` / `GIT_ALTERNATE_OBJECT_DIRECTORIES` pointing at the
27
- * tool's private scratch), so the distro git mutates scratch, never the
28
- * repository.
16
+ * `\\wsl.localhost\\<distro>` UNC). Inside that, two cases:
17
+ *
18
+ * - **Snapshot calls** (`add`, `write-tree`, `ls-tree`, `cat-file`,
19
+ * `diff-tree`, `ls-files`, `check-ignore`) route whenever the program is
20
+ * git — the runs are fully isolated from the user's git state
21
+ * (`GIT_INDEX_FILE` / `GIT_OBJECT_DIRECTORY` /
22
+ * `GIT_ALTERNATE_OBJECT_DIRECTORIES` pointing at the tool's private
23
+ * scratch), so the distro git mutates scratch, never the repository.
24
+ * - **Discovery calls** (`--show-toplevel`, `--absolute-git-dir`, `--git-path`)
25
+ * print absolute paths that the caller resolves against the UNC workspace,
26
+ * so a distro answer in Linux paths would corrupt that resolution. They
27
+ * route only when the program is the SENTINEL path `DISTRO_GIT_SENTINEL` —
28
+ * the guard `resolveExecutable` returns when the HOST has no git — and the
29
+ * facade translates the output lines into UNC form before the caller sees
30
+ * them (`translateDiscoveryOutput`). A host-resolved git keeps the shipped
31
+ * host-side discovery, which answers UNC natively (and needs the documented
32
+ * `safe.directory` whitelist for WSL shares).
33
+ *
34
+ * The split means a machine without Windows git still gets full WSL change
35
+ * review: the sentinel resolves, every spawn routes, nothing touches the
36
+ * host. See `lib/git-exec.js` for the facade side.
37
+ *
29
38
  *
30
39
  * ## Environment translation
31
40
  *
@@ -47,7 +56,15 @@
47
56
  import { isWslUnc, uncToPosix, windowsToLinuxMount } from "./paths.js";
48
57
  /** The one program this rewrite passes through to the distro. */
49
58
  const GIT_PROGRAM = "git";
50
- /** Discovery flags print absolute paths — host git keeps that contract. */
59
+ /**
60
+ * The program path `resolveExecutable` returns for git when the HOST has no
61
+ * git: a Linux absolute path that only makes sense inside the distro, so any
62
+ * spawn carrying it must route (and a discovery spawn's output must come back
63
+ * translated). Chosen to match a standard distro install so the argv stays
64
+ * honest even where it is read verbatim.
65
+ */
66
+ export const DISTRO_GIT_SENTINEL = "/usr/bin/git";
67
+ /** Discovery flags print absolute paths — the caller resolves them. */
51
68
  const DISCOVERY_FLAGS = ["--show-toplevel", "--absolute-git-dir", "--git-path"];
52
69
  /**
53
70
  * Whether a spawn's program is git.
@@ -125,7 +142,8 @@ export function translateGitEnv(env) {
125
142
  export function gitSpawnRewrite({ argv, cwd, wslPath }) {
126
143
  if (!Array.isArray(argv) || !isGitProgram(argv[0]))
127
144
  return undefined;
128
- if (namesDiscoveryPaths(argv))
145
+ const sentinel = argv[0] === DISTRO_GIT_SENTINEL;
146
+ if (namesDiscoveryPaths(argv) && !sentinel)
129
147
  return undefined;
130
148
  const named = uncToPosix(cwd);
131
149
  if (named === undefined)
@@ -137,5 +155,26 @@ export function gitSpawnRewrite({ argv, cwd, wslPath }) {
137
155
  linuxCwd,
138
156
  gitArgv,
139
157
  wslArgv: [wslPath, "-d", named.distro, "--cd", linuxCwd, "--exec", ...gitArgv],
158
+ discovery: sentinel && namesDiscoveryPaths(argv),
140
159
  };
141
160
  }
161
+ /**
162
+ * Translate one routed discovery call's stdout into the caller's UNC
163
+ * coordinate system: every absolute Linux path line becomes
164
+ * `\\wsl.localhost\<distro><path>`; relative lines (`--git-path` answers
165
+ * `.git/objects` when the repository is not bare) pass through untouched,
166
+ * because the caller resolves them against the UNC cwd natively.
167
+ *
168
+ * @param distro - the distro the discovery ran in.
169
+ * @param stdout - the distro git's captured stdout.
170
+ * @returns the stdout with absolute path lines rewritten.
171
+ */
172
+ export function translateDiscoveryOutput(distro, stdout) {
173
+ const text = stdout.toString("utf8");
174
+ if (!text.includes("\n") && !text.startsWith("/"))
175
+ return stdout;
176
+ const translated = text.split("\n").map((line) => line.startsWith("/")
177
+ ? `\\\\wsl.localhost\\${distro}${line.replaceAll("/", "\\")}`
178
+ : line);
179
+ return Buffer.from(translated.join("\n"), "utf8");
180
+ }
@@ -60,13 +60,17 @@
60
60
  *
61
61
  * ## What the operator must pair it with
62
62
  *
63
- * `terminalEnvironment()` and `resolveExecutable()` are also resolved from the
64
- * root realm, and neither receives a Session or a directory, so this provider
65
- * deliberately leaves them alone: rewriting them would change executable lookup
66
- * for every other root consumer (the pwsh executor, the LSP host, the file
67
- * search) to make the *shell menu* of one window prettier. The profile instead
68
- * configures the terminal controller with the single shell profile that matters:
69
- * `shell: { path: wsl.exe, name: WSL }` and `shellCandidates: []`.
63
+ * `terminalEnvironment()` is also resolved from the root realm and receives no
64
+ * Session or directory, so this provider deliberately leaves it alone:
65
+ * rewriting it would change environment lookup for every other root consumer
66
+ * to make the *shell menu* of one window prettier. `resolveExecutable()` gets
67
+ * ONE scoped exception — a git fallback when the host has none (see the
68
+ * override below); every other name's lookup is untouched. The profile also
69
+ * configures the terminal controller with the distro shell as the default
70
+ * profile plus deliberate host candidates: `shell: { path: wsl.exe, name: WSL }`
71
+ * and `shellCandidates: [wsl.exe, powershell, cmd]` — the first dedupes into
72
+ * the default's `wsl` kind, and the other two are launches this terminal really
73
+ * starts (host sessions, or an explicit user selection, titled after itself).
70
74
  *
71
75
  * Consequence worth stating plainly: the terminal is a property of the
72
76
  * composition, not of the Session. A Session whose workspace is a Windows folder
@@ -297,6 +301,16 @@ export declare class WslSubprocessRuntime extends LocalSubprocessRuntime {
297
301
  * @returns the live search handle.
298
302
  */
299
303
  spawn(spec: SubprocessSpawnSpec): SubprocessHandle;
304
+ /**
305
+ * Guarded git fallback: when the HOST has no git, resolve the name to the
306
+ * distro sentinel so the change-review pipeline's every spawn (discovery
307
+ * included) routes into the distro instead of dying at the executable
308
+ * lookup. Scoped to the one name — every other consumer's lookup (pwsh,
309
+ * LSP, ripgrep) is untouched — and only on the super's failure, so a
310
+ * machine with Windows git behaves exactly as shipped. See
311
+ * `git-route.ts`'s `DISTRO_GIT_SENTINEL` for the routing side.
312
+ */
313
+ resolveExecutable(command: string, env: Record<string, string> | undefined, signal: AbortSignal | undefined): Promise<string>;
300
314
  /**
301
315
  * The environment one `wsl.exe` launch forwards: the safe list plus every
302
316
  * managed `DSH_*` name, admitted through `WSLENV` and never `PATH` — see the
package/lib/subprocess.js CHANGED
@@ -60,13 +60,17 @@
60
60
  *
61
61
  * ## What the operator must pair it with
62
62
  *
63
- * `terminalEnvironment()` and `resolveExecutable()` are also resolved from the
64
- * root realm, and neither receives a Session or a directory, so this provider
65
- * deliberately leaves them alone: rewriting them would change executable lookup
66
- * for every other root consumer (the pwsh executor, the LSP host, the file
67
- * search) to make the *shell menu* of one window prettier. The profile instead
68
- * configures the terminal controller with the single shell profile that matters:
69
- * `shell: { path: wsl.exe, name: WSL }` and `shellCandidates: []`.
63
+ * `terminalEnvironment()` is also resolved from the root realm and receives no
64
+ * Session or directory, so this provider deliberately leaves it alone:
65
+ * rewriting it would change environment lookup for every other root consumer
66
+ * to make the *shell menu* of one window prettier. `resolveExecutable()` gets
67
+ * ONE scoped exception — a git fallback when the host has none (see the
68
+ * override below); every other name's lookup is untouched. The profile also
69
+ * configures the terminal controller with the distro shell as the default
70
+ * profile plus deliberate host candidates: `shell: { path: wsl.exe, name: WSL }`
71
+ * and `shellCandidates: [wsl.exe, powershell, cmd]` — the first dedupes into
72
+ * the default's `wsl` kind, and the other two are launches this terminal really
73
+ * starts (host sessions, or an explicit user selection, titled after itself).
70
74
  *
71
75
  * Consequence worth stating plainly: the terminal is a property of the
72
76
  * composition, not of the Session. A Session whose workspace is a Windows folder
@@ -79,13 +83,15 @@
79
83
  * @module dsh-plugin-wsl/subprocess
80
84
  */
81
85
  import { randomUUID } from "node:crypto";
86
+ import { Buffer } from "node:buffer";
82
87
  import z from "@deepseek-ai/schemastery";
83
88
  import { LocalSubprocessRuntime } from "@deepseek-ai/dsh-subprocess-local";
84
89
  import { isPosixAbsolute, toLinuxPath, uncToPosix, windowsToLinuxMount } from "./paths.js";
85
90
  import { DEFAULT_WSL_PATH, defaultDistro, wslEnvValue, wslTerminalArgv } from "./wsl.js";
86
91
  import { searchSpawnRewrite } from "./search-route.js";
87
- import { gitSpawnRewrite, translateGitEnv } from "./git-route.js";
92
+ import { DISTRO_GIT_SENTINEL, gitSpawnRewrite, translateGitEnv } from "./git-route.js";
88
93
  import { searchExecutionHandle } from "./search-exec.js";
94
+ import { gitExecutionHandle } from "./git-exec.js";
89
95
  import { sharedAgent } from "./agent-shared.js";
90
96
  import { parseTerminalActivity, TERMINAL_ID_ENV, terminalActivityProbe, wrapTerminalHandle } from "./terminal-activity.js";
91
97
  import { AgentUnavailableError } from "./agent-errors.js";
@@ -341,12 +347,40 @@ export class WslSubprocessRuntime extends LocalSubprocessRuntime {
341
347
  // object store, work tree, and refs stay untouched by design — the
342
348
  // snapshot runs in the tool's private scratch.
343
349
  const { env, wslenv } = translateGitEnv(spec.env);
344
- return super.spawn({
350
+ const oneShot = () => super.spawn({
345
351
  ...spec,
346
352
  argv: gitRewrite.wslArgv,
347
353
  cwd: this.hostCwd(),
348
354
  env: wslenv.length > 0 ? { ...env, WSLENV: wslEnvValue(wslenv) } : env,
349
355
  });
356
+ // Agent-first: the warm resident answers a git call in ~2 ms where a
357
+ // one-shot `wsl.exe` spends ~230 ms of process startup, and a snapshot
358
+ // makes 6-8 of those calls plus per-file `cat-file`. Only the translated
359
+ // GIT_* names ride the agent's per-request environment — a host PATH
360
+ // must not leak into the distro, and the one-shot forwards exactly those
361
+ // names through WSLENV too. The agent being out falls back to the
362
+ // one-shot handle lazily (see git-exec.ts).
363
+ const agentEnv = {};
364
+ for (const [name, value] of Object.entries(env)) {
365
+ if (name.startsWith("GIT_") && typeof value === "string")
366
+ agentEnv[name] = value;
367
+ }
368
+ const stdinSpec = spec.stdio.stdin;
369
+ const stdinData = typeof stdinSpec === "object" && stdinSpec !== null && typeof stdinSpec.data === "string" ? stdinSpec.data : undefined;
370
+ const stdoutCap = spec.stdio.stdout?.maxBytes ?? 0;
371
+ const stderrCap = spec.stdio.stderr?.maxBytes ?? 0;
372
+ return gitExecutionHandle({
373
+ agent: sharedAgent(gitRewrite.distro),
374
+ cwd: gitRewrite.linuxCwd,
375
+ argv: gitRewrite.gitArgv,
376
+ env: agentEnv,
377
+ stdin: stdinData === undefined ? undefined : Buffer.from(stdinData, "utf8"),
378
+ caps: { stdout: stdoutCap, stderr: stderrCap },
379
+ distro: gitRewrite.distro,
380
+ discovery: gitRewrite.discovery === true,
381
+ signal: spec.signal,
382
+ spawnFallback: oneShot,
383
+ });
350
384
  }
351
385
  const rewrite = searchSpawnRewrite({ argv: spec.argv, cwd: spec.cwd, wslPath: this.config.wslPath });
352
386
  if (rewrite === undefined)
@@ -372,6 +406,26 @@ export class WslSubprocessRuntime extends LocalSubprocessRuntime {
372
406
  }),
373
407
  });
374
408
  }
409
+ /**
410
+ * Guarded git fallback: when the HOST has no git, resolve the name to the
411
+ * distro sentinel so the change-review pipeline's every spawn (discovery
412
+ * included) routes into the distro instead of dying at the executable
413
+ * lookup. Scoped to the one name — every other consumer's lookup (pwsh,
414
+ * LSP, ripgrep) is untouched — and only on the super's failure, so a
415
+ * machine with Windows git behaves exactly as shipped. See
416
+ * `git-route.ts`'s `DISTRO_GIT_SENTINEL` for the routing side.
417
+ */
418
+ async resolveExecutable(command, env, signal) {
419
+ try {
420
+ return await super.resolveExecutable(command, env, signal);
421
+ }
422
+ catch (error) {
423
+ const base = command.slice(Math.max(command.lastIndexOf("/"), command.lastIndexOf("\\")) + 1).toLowerCase().replace(/\.exe$/u, "");
424
+ if (base === "git")
425
+ return DISTRO_GIT_SENTINEL;
426
+ throw error;
427
+ }
428
+ }
375
429
  /**
376
430
  * The environment one `wsl.exe` launch forwards: the safe list plus every
377
431
  * managed `DSH_*` name, admitted through `WSLENV` and never `PATH` — see the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-wsl-env",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "在 WSL 的 Linux 实例中打开项目工作区,在对应 Linux 空间中进行命令执行、运行 shell 脚本和文件访问,以获得全套 Linux Shell 工具链收益,并通过 bubblewrap 对命令进行沙箱隔离",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -100,6 +100,9 @@
100
100
  "docs/PERFORMANCE.md",
101
101
  "docs/COMPATIBILITY.md",
102
102
  "docs/CONFIGURATION.md",
103
+ "docs/LIMITATIONS.zh.md",
104
+ "docs/CONFIGURATION.zh.md",
105
+ "docs/QUICKSTART.zh.md",
103
106
  "docs/LIMITATIONS.md",
104
107
  "docs/RELEASING.md",
105
108
  "docs/REQUIREMENTS.zh.md",
@@ -394,6 +394,26 @@ async function main(options) {
394
394
  if (m === null) return [];
395
395
  return (m[1] ?? "").trim().split(/\s+/).filter((p) => p.length > 0);
396
396
  });
397
+ // ---- Windows git trust (WSL change review) ------------------------------
398
+ // The change-review pipeline's discovery call runs the WINDOWS git against
399
+ // a \\wsl.localhost UNC when Windows git is installed; git's
400
+ // dubious-ownership check refuses the share unless it is whitelisted, and
401
+ // the pipeline silently degrades to "git is unavailable" (no diff cards for
402
+ // WSL workspaces). A machine without Windows git is fine too — the provider
403
+ // resolves git to a distro sentinel and routes everything. What the doctor
404
+ // can probe cheaply from here is interop git.exe and its whitelist.
405
+ const winGitVersion = await run(["git.exe", "--version"]);
406
+ if (!winGitVersion.ok) {
407
+ check("git.win-safe", "info", "Windows git trust", "Windows git not found through interop — WSL change review uses the distro fallback; host (C:\\...) workspaces would need git for their own review");
408
+ } else {
409
+ const entries = (await run(["git.exe", "config", "--global", "--get-all", "safe.directory"])).stdout.split(/\r?\n/).filter(Boolean);
410
+ const whitelisted = entries.some((entry) => /wsl\.localhost/i.test(entry));
411
+ check("git.win-safe", whitelisted ? "ok" : "warn", "Windows git trust",
412
+ whitelisted ? `the \\\\wsl.localhost share is whitelisted (${entries.length} entr${entries.length === 1 ? "y" : "ies"})`
413
+ : "Windows git does not trust the \\\\wsl.localhost share; WSL workspaces' change review silently degrades to file-tool edits",
414
+ whitelisted ? undefined : 'on Windows: git config --global --add safe.directory "//wsl.localhost/*", then restart the app');
415
+ }
416
+
397
417
  // ---- resident handshake --------------------------------------------------
398
418
  const agentScript = join(ROOT, "agent", "wsl-agent.sh");
399
419
  if (!agentScript.startsWith("/")) {