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.
- package/CHANGELOG.md +57 -1
- package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
- package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
- package/agent/wsl-agent.sh +47 -7
- package/cordis.patch.yml +15 -9
- package/docs/ARCHITECTURE.md +14 -6
- package/docs/CONFIGURATION.md +23 -0
- package/docs/CONFIGURATION.zh.md +288 -0
- package/docs/LIMITATIONS.md +133 -299
- package/docs/LIMITATIONS.zh.md +161 -0
- package/docs/QUICKSTART.md +2 -0
- package/docs/QUICKSTART.zh.md +168 -0
- package/docs/UPSTREAM-TERMINAL-TITLE.md +10 -12
- package/lib/agent-exec.d.ts +5 -0
- package/lib/agent-protocol.d.ts +18 -5
- package/lib/agent-protocol.js +11 -6
- package/lib/agent.d.ts +9 -1
- package/lib/agent.js +2 -2
- package/lib/git-exec.d.ts +117 -0
- package/lib/git-exec.js +105 -0
- package/lib/git-route.d.ts +43 -13
- package/lib/git-route.js +54 -15
- package/lib/subprocess.d.ts +21 -7
- package/lib/subprocess.js +63 -9
- package/package.json +4 -1
- package/scripts/doctor.mjs +20 -0
package/lib/git-exec.js
ADDED
|
@@ -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
|
+
}
|
package/lib/git-route.d.ts
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
+
}
|
package/lib/subprocess.d.ts
CHANGED
|
@@ -60,13 +60,17 @@
|
|
|
60
60
|
*
|
|
61
61
|
* ## What the operator must pair it with
|
|
62
62
|
*
|
|
63
|
-
* `terminalEnvironment()`
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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()`
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
-
|
|
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.
|
|
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",
|
package/scripts/doctor.mjs
CHANGED
|
@@ -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("/")) {
|