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.
- package/CHANGELOG.md +345 -8
- package/CONTRIBUTING.md +221 -68
- package/README.md +50 -35
- package/SECURITY.md +18 -2
- package/SUPPORT.md +23 -7
- package/cordis.patch.yml +9 -0
- package/docs/ARCHITECTURE.md +200 -57
- package/docs/CONFIGURATION.md +180 -16
- package/docs/LIMITATIONS.md +91 -33
- package/docs/PARITY.md +66 -38
- package/docs/PEER-PARITY.md +95 -32
- package/docs/README.zh.md +49 -31
- package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
- package/docs/UPSTREAM-TERMINAL-TITLE.md +49 -14
- package/lib/agent-confined.d.ts +42 -0
- package/lib/agent-confined.js +25 -41
- package/lib/agent-errors.d.ts +31 -0
- package/lib/agent-errors.js +19 -16
- package/lib/agent-exec.d.ts +159 -0
- package/lib/agent-exec.js +151 -165
- package/lib/agent-protocol.d.ts +192 -0
- package/lib/agent-protocol.js +76 -100
- package/lib/agent-shared.d.ts +27 -0
- package/lib/agent-shared.js +12 -12
- package/lib/agent.d.ts +353 -0
- package/lib/agent.js +536 -552
- package/lib/auto-preset.d.ts +110 -0
- package/lib/auto-preset.js +157 -141
- package/lib/bwrap.d.ts +90 -0
- package/lib/bwrap.js +18 -18
- package/lib/file-reference-wsl.d.ts +160 -0
- package/lib/file-reference-wsl.js +192 -173
- package/lib/fs-decisions.d.ts +107 -0
- package/lib/fs-decisions.js +64 -60
- package/lib/fs-routing.d.ts +182 -0
- package/lib/fs-routing.js +173 -116
- package/lib/fs-substrate.d.ts +258 -0
- package/lib/fs-substrate.js +248 -248
- package/lib/fs.d.ts +10 -0
- package/lib/fs.js +5 -0
- package/lib/fsio-agent.d.ts +307 -0
- package/lib/fsio-agent.js +414 -410
- package/lib/fsio-text.d.ts +115 -0
- package/lib/fsio-text.js +104 -84
- package/lib/git-route.d.ts +100 -0
- package/lib/git-route.js +46 -47
- package/lib/index.d.ts +1003 -0
- package/lib/index.js +1106 -1003
- package/lib/listing.d.ts +114 -0
- package/lib/listing.js +68 -55
- package/lib/paths.d.ts +182 -0
- package/lib/paths.js +134 -109
- package/lib/picker.d.ts +266 -0
- package/lib/picker.js +371 -351
- package/lib/ports.d.ts +44 -0
- package/lib/ports.js +39 -29
- package/lib/preflight.d.ts +91 -0
- package/lib/preflight.js +94 -0
- package/lib/preset-choice.d.ts +33 -0
- package/lib/preset-choice.js +15 -7
- package/lib/probe-cache.d.ts +51 -0
- package/lib/probe-cache.js +43 -29
- package/lib/sandbox-core.d.ts +149 -0
- package/lib/sandbox-core.js +239 -87
- package/lib/sandbox.d.ts +48 -0
- package/lib/sandbox.js +4 -4
- package/lib/search-exec.d.ts +96 -0
- package/lib/search-exec.js +52 -79
- package/lib/search-route.d.ts +75 -0
- package/lib/search-route.js +30 -31
- package/lib/shell-env.d.ts +88 -0
- package/lib/shell-env.js +78 -74
- package/lib/shell.d.ts +10 -0
- package/lib/shell.js +5 -0
- package/lib/subprocess.d.ts +313 -0
- package/lib/subprocess.js +267 -262
- package/lib/terminal-activity.d.ts +98 -0
- package/lib/terminal-activity.js +29 -39
- package/lib/terminal-route.d.ts +27 -0
- package/lib/terminal-route.js +16 -8
- package/lib/watcher.d.ts +77 -0
- package/lib/watcher.js +120 -126
- package/lib/workspace-files-route.d.ts +132 -0
- package/lib/workspace-files-route.js +73 -81
- package/lib/workspace-files-wsl.d.ts +257 -0
- package/lib/workspace-files-wsl.js +311 -251
- package/lib/wsl.d.ts +266 -0
- package/lib/wsl.js +150 -155
- package/locale/preflight/en.json +6 -0
- package/locale/preflight/zh.json +6 -0
- package/package.json +78 -22
- package/scripts/diagnose.sh +92 -0
package/lib/wsl.js
CHANGED
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
* own Loader entry and must not drag the shell executor's and filesystem
|
|
7
7
|
* backend's peer dependencies into that row.
|
|
8
8
|
*
|
|
9
|
+
* This is a TypeScript source built to `lib/wsl.js`; edit THIS file and run
|
|
10
|
+
* `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
11
|
+
* fails when it drifts.
|
|
12
|
+
*
|
|
9
13
|
* @module dsh-plugin-wsl/wsl
|
|
10
14
|
*/
|
|
11
|
-
|
|
12
15
|
import { execFile } from "node:child_process";
|
|
13
16
|
import { posixToUnc } from "./paths.js";
|
|
14
|
-
|
|
15
17
|
/** Default executable name; resolution is left to PATH. */
|
|
16
18
|
export const DEFAULT_WSL_PATH = "wsl.exe";
|
|
17
|
-
|
|
18
19
|
/**
|
|
19
20
|
* The deadline every `wsl.exe` call runs under, when the caller names none.
|
|
20
21
|
*
|
|
@@ -26,7 +27,6 @@ export const DEFAULT_WSL_PATH = "wsl.exe";
|
|
|
26
27
|
* genuinely wants to wait forever.
|
|
27
28
|
*/
|
|
28
29
|
export const DEFAULT_WSL_DEADLINE_MS = 60_000;
|
|
29
|
-
|
|
30
30
|
/**
|
|
31
31
|
* Run one host command and capture UTF-8 output.
|
|
32
32
|
*
|
|
@@ -36,76 +36,81 @@ export const DEFAULT_WSL_DEADLINE_MS = 60_000;
|
|
|
36
36
|
*
|
|
37
37
|
* @param argv - executable plus arguments, spawned without a shell.
|
|
38
38
|
* @param signal - optional cancellation.
|
|
39
|
-
* @param deadlineMs - kill the call after this many milliseconds; the
|
|
40
|
-
* is {@link DEFAULT_WSL_DEADLINE_MS}, and `0` waits forever.
|
|
39
|
+
* @param deadlineMs - kill the call after this many milliseconds; the
|
|
40
|
+
* default is {@link DEFAULT_WSL_DEADLINE_MS}, and `0` waits forever.
|
|
41
41
|
* @returns captured stdout.
|
|
42
42
|
* @throws the execFile error, with a hint attached when the cause is the sandbox.
|
|
43
43
|
*/
|
|
44
44
|
export function runCapture(argv, signal, deadlineMs = DEFAULT_WSL_DEADLINE_MS) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
: ""
|
|
74
|
-
}`;
|
|
75
|
-
reject(error);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
resolve(String(stdout ?? "").replace(/\u0000/g, ""));
|
|
79
|
-
},
|
|
80
|
-
);
|
|
81
|
-
});
|
|
45
|
+
const [file, ...args] = argv;
|
|
46
|
+
if (file === undefined)
|
|
47
|
+
throw new Error("wsl: runCapture needs a program to run");
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
execFile(file, args, { env: { ...process.env, WSL_UTF8: "1" }, windowsHide: true, signal, encoding: "utf8", timeout: deadlineMs }, (error, stdout, stderr) => {
|
|
50
|
+
if (error) {
|
|
51
|
+
// A denied WSL call is the likeliest failure here and its own message
|
|
52
|
+
// is unhelpful, so name the cause when it is recognisable.
|
|
53
|
+
const detail = `${stdout ?? ""}${stderr ?? ""}`.replace(/\u0000/g, "").trim();
|
|
54
|
+
const denied = /E_ACCESSDENIED/i.test(detail) || error.code === "EACCES";
|
|
55
|
+
const missing = error.code === "ENOENT";
|
|
56
|
+
// The deadline kill arrives as a SIGTERM with no WSL diagnostic, so
|
|
57
|
+
// it must be named for what it is — not left looking like WSL's own
|
|
58
|
+
// failure. An abort is cancellation, not a deadline.
|
|
59
|
+
const stuck = error.killed === true && error.name !== "AbortError";
|
|
60
|
+
error.message = `${file} failed: ${detail || error.message}${denied
|
|
61
|
+
? " (WSL is unreachable from a sandboxed process: DSH's Windows ACL sandbox runs commands under a restricted low-integrity token. Compose the non-sandboxing providers.)"
|
|
62
|
+
: ""}${missing
|
|
63
|
+
? ` (the executable "${file}" could not be started: check \`wslPath\` in this row, and that WSL is installed)`
|
|
64
|
+
: ""}${stuck
|
|
65
|
+
? ` (no completion within ${deadlineMs}ms — the WSL service may be wedged; retry once, then \`wsl.exe --shutdown\` and reopen)`
|
|
66
|
+
: ""}`;
|
|
67
|
+
reject(error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
resolve(String(stdout ?? "").replace(/\u0000/g, ""));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
82
73
|
}
|
|
83
|
-
|
|
84
74
|
/**
|
|
85
75
|
* List installed distros with `wsl.exe -l -q`.
|
|
76
|
+
*
|
|
86
77
|
* @param options - the `wsl.exe` path and optional cancellation.
|
|
87
78
|
* @returns distro names in WSL's own order, default distro first.
|
|
88
79
|
*/
|
|
89
80
|
export async function listDistros(options = {}) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
const stdout = await runCapture([options.wslPath ?? DEFAULT_WSL_PATH, "-l", "-q"], options.signal);
|
|
82
|
+
return parseDistroList(stdout);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Parse `wsl.exe -l -q` output into distro names. A pure seam so the CRLF and
|
|
86
|
+
* blank-line shapes of the real output are unit-testable on hosts without
|
|
87
|
+
* `wsl.exe`.
|
|
88
|
+
*
|
|
89
|
+
* @param stdout - the raw `wsl.exe -l -q` stdout.
|
|
90
|
+
* @returns distro names, default distro first.
|
|
91
|
+
*/
|
|
92
|
+
export function parseDistroList(stdout) {
|
|
93
|
+
return String(stdout)
|
|
94
|
+
.split(/\r?\n/)
|
|
95
|
+
.map((line) => line.trim())
|
|
96
|
+
.filter((line) => line.length > 0);
|
|
95
97
|
}
|
|
96
|
-
|
|
97
98
|
/**
|
|
98
99
|
* Resolve the distro to use when none is configured: WSL's own default.
|
|
100
|
+
*
|
|
99
101
|
* @param options - the `wsl.exe` path and optional cancellation.
|
|
100
102
|
* @returns the first entry of {@link listDistros}.
|
|
101
103
|
* @throws when no distro is installed.
|
|
102
104
|
*/
|
|
103
105
|
export async function defaultDistro(options = {}) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
const [first] = await listDistros(options);
|
|
107
|
+
if (first === undefined) {
|
|
108
|
+
throw new Error("dsh-plugin-wsl: no WSL distro is installed (`wsl.exe -l -q` returned nothing). " +
|
|
109
|
+
"Install one: `wsl.exe --list --online` lists the available names, then `wsl --install -d <name>`, " +
|
|
110
|
+
"or point this row's `distro` at an existing one.");
|
|
111
|
+
}
|
|
112
|
+
return first;
|
|
107
113
|
}
|
|
108
|
-
|
|
109
114
|
/**
|
|
110
115
|
* The Linux home directory of a distro's default user, as a Linux path. `$HOME`
|
|
111
116
|
* is set by `wsl.exe` itself from the distro's default user, so this needs no
|
|
@@ -117,17 +122,26 @@ export async function defaultDistro(options = {}) {
|
|
|
117
122
|
* @throws when the distro cannot be queried or reports no home.
|
|
118
123
|
*/
|
|
119
124
|
export async function linuxHomePath(distro, options = {}) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
const stdout = await runCapture([options.wslPath ?? DEFAULT_WSL_PATH, "-d", distro, "--exec", "sh", "-c", 'printf %s "$HOME"'], options.signal);
|
|
126
|
+
return parseHomePath(distro, stdout);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Validate the distro's reported $HOME. A pure seam: the failure shapes
|
|
130
|
+
* (empty output, a Windows path, mojibake) are unit-testable without
|
|
131
|
+
* `wsl.exe`.
|
|
132
|
+
*
|
|
133
|
+
* @param distro - the distro name, for the error message.
|
|
134
|
+
* @param stdout - the raw `printf %s "$HOME"` output.
|
|
135
|
+
* @returns the Linux home path.
|
|
136
|
+
*/
|
|
137
|
+
export function parseHomePath(distro, stdout) {
|
|
138
|
+
const home = String(stdout).trim();
|
|
139
|
+
if (home.length === 0 || !home.startsWith("/")) {
|
|
140
|
+
throw new Error(`dsh-plugin-wsl: distro "${distro}" reported no usable $HOME (got "${home}"). ` +
|
|
141
|
+
`Check the distro's default user in /etc/wsl.conf, and that the user's /etc/passwd entry carries a home directory.`);
|
|
142
|
+
}
|
|
143
|
+
return home;
|
|
129
144
|
}
|
|
130
|
-
|
|
131
145
|
/**
|
|
132
146
|
* The same home in the world coordinate system, which is what a host-side
|
|
133
147
|
* consumer needs.
|
|
@@ -138,27 +152,8 @@ export async function linuxHomePath(distro, options = {}) {
|
|
|
138
152
|
* @throws when the distro cannot be queried or reports no home.
|
|
139
153
|
*/
|
|
140
154
|
export async function linuxHome(distro, options = {}) {
|
|
141
|
-
|
|
155
|
+
return posixToUnc(distro, await linuxHomePath(distro, options));
|
|
142
156
|
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Resolve a Linux path to its canonical form INSIDE the distro with
|
|
146
|
-
* `readlink -f`.
|
|
147
|
-
*
|
|
148
|
-
* This exists because the UNC share cannot traverse Linux symlinks: the share
|
|
149
|
-
* exposes one as a Windows reparse point whose relative POSIX target has no
|
|
150
|
-
* meaning to Windows path resolution, so `stat`/`realpath`/`read` on a path such
|
|
151
|
-
* as `/etc/os-release` (-> ../usr/lib/os-release) or `/lib` (-> usr/lib) fail
|
|
152
|
-
* with `ENOENT`, even though `lstat` reaches the link. Asking the distro for the
|
|
153
|
-
* real path is the only reliable answer, at one process per distinct path.
|
|
154
|
-
*
|
|
155
|
-
* @param distro - distro name.
|
|
156
|
-
* @param linuxPath - the Linux path to canonicalize.
|
|
157
|
-
* @param options - the `wsl.exe` path and optional cancellation.
|
|
158
|
-
* @returns the canonical absolute Linux path, or undefined when it cannot be
|
|
159
|
-
* determined (missing file, broken link, or a failed query).
|
|
160
|
-
*/
|
|
161
|
-
|
|
162
157
|
/**
|
|
163
158
|
* Shells known to accept the combined POSIX `-lc` login-plus-command form.
|
|
164
159
|
*
|
|
@@ -168,7 +163,6 @@ export async function linuxHome(distro, options = {}) {
|
|
|
168
163
|
* — it just does not get login-shell semantics.
|
|
169
164
|
*/
|
|
170
165
|
const POSIX_LOGIN_SHELLS = new Set(["sh", "bash", "dash", "zsh", "ksh", "ksh93", "mksh", "ash", "busybox"]);
|
|
171
|
-
|
|
172
166
|
/**
|
|
173
167
|
* The argv flags that make `shell` run `command`, with login semantics when the
|
|
174
168
|
* shell supports them.
|
|
@@ -177,6 +171,12 @@ const POSIX_LOGIN_SHELLS = new Set(["sh", "bash", "dash", "zsh", "ksh", "ksh93",
|
|
|
177
171
|
* @param login - whether login-shell semantics were requested.
|
|
178
172
|
* @returns the flags to place before the command string.
|
|
179
173
|
*/
|
|
174
|
+
export function shellArgs(shellPath, login) {
|
|
175
|
+
const name = String(shellPath).slice(String(shellPath).lastIndexOf("/") + 1).toLowerCase();
|
|
176
|
+
if (!POSIX_LOGIN_SHELLS.has(name))
|
|
177
|
+
return ["-c"];
|
|
178
|
+
return login ? ["-lc"] : ["-c"];
|
|
179
|
+
}
|
|
180
180
|
/**
|
|
181
181
|
* The working directory `wsl.exe` could not enter, when its relay said so.
|
|
182
182
|
*
|
|
@@ -190,22 +190,15 @@ const POSIX_LOGIN_SHELLS = new Set(["sh", "bash", "dash", "zsh", "ksh", "ksh93",
|
|
|
190
190
|
* run into an error. Verified against `wsl.exe -d <distro> --cd /missing --exec sh`:
|
|
191
191
|
* exit 0, `hi` on stdout, this shape on stderr.
|
|
192
192
|
*
|
|
193
|
-
* @param stdout - a command's captured stdout.
|
|
194
|
-
* @param stderr - its captured stderr.
|
|
193
|
+
* @param stdout - a command's captured stdout; absent reads as empty.
|
|
194
|
+
* @param stderr - its captured stderr; absent reads as empty.
|
|
195
195
|
* @returns the directory that could not be entered, or undefined.
|
|
196
196
|
*/
|
|
197
197
|
export function workdirFailure(stdout, stderr) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
const text = `${stdout ?? ""}\n${stderr ?? ""}`.replace(/\0/g, "");
|
|
199
|
+
const match = /<3>WSL[^\n]*CreateProcessCommon[^\n]*chdir\(([^)]*)\) failed/.exec(text);
|
|
200
|
+
return match?.[1];
|
|
201
201
|
}
|
|
202
|
-
|
|
203
|
-
export function shellArgs(shellPath, login) {
|
|
204
|
-
const name = String(shellPath).slice(String(shellPath).lastIndexOf("/") + 1).toLowerCase();
|
|
205
|
-
if (!POSIX_LOGIN_SHELLS.has(name)) return ["-c"];
|
|
206
|
-
return login ? ["-lc"] : ["-c"];
|
|
207
|
-
}
|
|
208
|
-
|
|
209
202
|
/**
|
|
210
203
|
* Shells whose `-l` flag means a login shell.
|
|
211
204
|
*
|
|
@@ -215,7 +208,6 @@ export function shellArgs(shellPath, login) {
|
|
|
215
208
|
* nushell/xonsh use long flags, so they deliberately get nothing.
|
|
216
209
|
*/
|
|
217
210
|
const LOGIN_FLAG_SHELLS = new Set([...POSIX_LOGIN_SHELLS, "fish"]);
|
|
218
|
-
|
|
219
211
|
/**
|
|
220
212
|
* The flags that make an interactive shell a login shell.
|
|
221
213
|
*
|
|
@@ -228,10 +220,9 @@ const LOGIN_FLAG_SHELLS = new Set([...POSIX_LOGIN_SHELLS, "fish"]);
|
|
|
228
220
|
* @returns `["-l"]`, or an empty array for a shell whose login flag differs.
|
|
229
221
|
*/
|
|
230
222
|
export function interactiveShellArgs(shellPath, login) {
|
|
231
|
-
|
|
232
|
-
|
|
223
|
+
const name = String(shellPath).slice(String(shellPath).lastIndexOf("/") + 1).toLowerCase();
|
|
224
|
+
return login && LOGIN_FLAG_SHELLS.has(name) ? ["-l"] : [];
|
|
233
225
|
}
|
|
234
|
-
|
|
235
226
|
/**
|
|
236
227
|
* The `wsl.exe` argv that opens one interactive shell inside a distro.
|
|
237
228
|
*
|
|
@@ -250,19 +241,17 @@ export function interactiveShellArgs(shellPath, login) {
|
|
|
250
241
|
* rejects an empty `--cd` argument whereas its own default starts in the
|
|
251
242
|
* distro user's home.
|
|
252
243
|
*
|
|
253
|
-
* @param options - the
|
|
244
|
+
* @param options - the launch description.
|
|
254
245
|
* @returns the argv to spawn on the host.
|
|
255
246
|
*/
|
|
256
247
|
export function wslTerminalArgv(options) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return [wslPath, ...(distro.length > 0 ? ["-d", distro] : []), ...directory, ...shell];
|
|
248
|
+
const { wslPath = DEFAULT_WSL_PATH, distro = "", shellPath, args = [], linuxCwd, login = true } = options;
|
|
249
|
+
const directory = typeof linuxCwd === "string" && linuxCwd.length > 0 ? ["--cd", linuxCwd] : [];
|
|
250
|
+
const shell = typeof shellPath === "string" && shellPath.length > 0
|
|
251
|
+
? ["--exec", shellPath, ...interactiveShellArgs(shellPath, login), ...args]
|
|
252
|
+
: [];
|
|
253
|
+
return [wslPath, ...(distro.length > 0 ? ["-d", distro] : []), ...directory, ...shell];
|
|
264
254
|
}
|
|
265
|
-
|
|
266
255
|
/**
|
|
267
256
|
* Managed facts of the `DSH_*` namespace that carry a Windows path. WSL's
|
|
268
257
|
* `WSLENV` `/p` flag translates them on the way into the distro, so a command
|
|
@@ -270,7 +259,6 @@ export function wslTerminalArgv(options) {
|
|
|
270
259
|
* absent: it is already a POSIX path, and translating it would corrupt it.
|
|
271
260
|
*/
|
|
272
261
|
const PATH_TRANSLATED_ENV = new Set(["DSH_HOME", "DSH_PROFILE_DIR"]);
|
|
273
|
-
|
|
274
262
|
/**
|
|
275
263
|
* Render WSLENV's value from the env names a call wants the distro to import.
|
|
276
264
|
*
|
|
@@ -281,21 +269,8 @@ const PATH_TRANSLATED_ENV = new Set(["DSH_HOME", "DSH_PROFILE_DIR"]);
|
|
|
281
269
|
* @returns the `:`-joined WSLENV value.
|
|
282
270
|
*/
|
|
283
271
|
export function wslEnvValue(names) {
|
|
284
|
-
|
|
272
|
+
return names.map((name) => (PATH_TRANSLATED_ENV.has(name) ? `${name}/p` : name)).join(":");
|
|
285
273
|
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Resolve the shell a distro's user actually gets, rather than assuming bash.
|
|
289
|
-
*
|
|
290
|
-
* The authoritative source is the login shell field of the user's passwd entry;
|
|
291
|
-
* `$SHELL` (which `wsl.exe` derives from it) is the fallback for distros without
|
|
292
|
-
* `getent`, and bash is the last resort. The passwd field is preferred because a
|
|
293
|
-
* user can export a different `SHELL` without changing their login shell.
|
|
294
|
-
*
|
|
295
|
-
* @param distro - distro name.
|
|
296
|
-
* @param options - the `wsl.exe` path and optional cancellation.
|
|
297
|
-
* @returns an absolute path to the shell inside the distro.
|
|
298
|
-
*/
|
|
299
274
|
/**
|
|
300
275
|
* The `WSL_E_*` code inside `wsl.exe`'s own output, when it reported one.
|
|
301
276
|
*
|
|
@@ -306,49 +281,69 @@ export function wslEnvValue(names) {
|
|
|
306
281
|
* `wsl.exe -d nosuchdistro --exec …` exits 255 with an empty stderr and
|
|
307
282
|
* `Wsl/Service/WSL_E_DISTRO_NOT_FOUND` on stdout.
|
|
308
283
|
*
|
|
309
|
-
* @param stdout - a command's captured stdout.
|
|
310
|
-
* @param stderr - its captured stderr.
|
|
284
|
+
* @param stdout - a command's captured stdout; absent reads as empty.
|
|
285
|
+
* @param stderr - its captured stderr; absent reads as empty.
|
|
311
286
|
* @returns the code, for example `WSL_E_DISTRO_NOT_FOUND`, or undefined.
|
|
312
287
|
*/
|
|
313
288
|
export function wslErrorCode(stdout, stderr) {
|
|
314
|
-
|
|
315
|
-
|
|
289
|
+
const text = `${stdout ?? ""}\n${stderr ?? ""}`.replace(/\0/g, "");
|
|
290
|
+
return /WSL_E_[A-Z0-9_]+/.exec(text)?.[0];
|
|
316
291
|
}
|
|
317
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Resolve the shell a distro's user actually gets, rather than assuming bash.
|
|
294
|
+
*
|
|
295
|
+
* The authoritative source is the login shell field of the user's passwd entry;
|
|
296
|
+
* `$SHELL` (which `wsl.exe` derives from it) is the fallback for distros without
|
|
297
|
+
* `getent`, and bash is the last resort. The passwd field is preferred because a
|
|
298
|
+
* user can export a different `SHELL` without changing their login shell.
|
|
299
|
+
*
|
|
300
|
+
* @param distro - distro name.
|
|
301
|
+
* @param options - the `wsl.exe` path and optional cancellation.
|
|
302
|
+
* @returns an absolute path to the shell inside the distro, or undefined when
|
|
303
|
+
* both probes failed.
|
|
304
|
+
*/
|
|
318
305
|
export async function defaultShell(distro, options = {}) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
306
|
+
const wslPath = options.wslPath ?? DEFAULT_WSL_PATH;
|
|
307
|
+
/**
|
|
308
|
+
* Ask the distro one `sh -c` question; only absolute paths count as answers.
|
|
309
|
+
*
|
|
310
|
+
* @param script - the shell snippet to run.
|
|
311
|
+
* @returns the trimmed answer, or undefined.
|
|
312
|
+
*/
|
|
313
|
+
const ask = async (script) => {
|
|
314
|
+
try {
|
|
315
|
+
const value = (await runCapture([wslPath, "-d", distro, "--exec", "sh", "-c", script], options.signal)).trim();
|
|
316
|
+
return value.startsWith("/") ? value : undefined;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
// No "bash" placeholder: a failed probe (a cold distro, a flaky first
|
|
323
|
+
// `wsl.exe` call) used to be pinned as the literal shell for the
|
|
324
|
+
// executor's lifetime. `undefined` tells the caller both answers failed;
|
|
325
|
+
// it may retry later or fall back itself.
|
|
326
|
+
return ((await ask('getent passwd "$(id -u)" | cut -d: -f7')) ??
|
|
327
|
+
(await ask('printf %s "$SHELL"')) ??
|
|
328
|
+
undefined);
|
|
337
329
|
}
|
|
338
|
-
|
|
339
330
|
/**
|
|
340
331
|
* Run one command inside a distro and return raw stdout. Used by callers that
|
|
341
332
|
* want Linux semantics instead of the UNC share's.
|
|
342
333
|
*
|
|
343
334
|
* @param distro - distro name.
|
|
344
335
|
* @param command - a shell command string.
|
|
345
|
-
* @param options - the `wsl.exe` path, an explicit `shell`, and optional
|
|
336
|
+
* @param options - the `wsl.exe` path, an explicit `shell`, and optional
|
|
337
|
+
* cancellation.
|
|
346
338
|
* @returns captured stdout.
|
|
339
|
+
* @throws when neither the caller nor the distro names a usable shell.
|
|
347
340
|
*/
|
|
348
341
|
export async function runInDistro(distro, command, options = {}) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
342
|
+
const shell = options.shell ?? (await defaultShell(distro, options));
|
|
343
|
+
if (shell === undefined) {
|
|
344
|
+
throw new Error(`dsh-plugin-wsl: distro "${distro}" named no usable shell (passwd probe and $SHELL both failed). ` +
|
|
345
|
+
`Check the user's login shell in /etc/passwd (\`getent passwd "$(id -u)"\` inside the distro), ` +
|
|
346
|
+
`or point this row's \`shell\` at an existing absolute path.`);
|
|
347
|
+
}
|
|
348
|
+
return runCapture([options.wslPath ?? DEFAULT_WSL_PATH, "-d", distro, "--exec", shell, ...shellArgs(shell, options.loginShell !== false), command], options.signal);
|
|
354
349
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "WSL sandbox preflight",
|
|
4
|
+
"description": "Probes bubblewrap when a distro session opens and surfaces a missing or unusable bwrap as a warning with the exact remedy — instead of the first command's fail-closed error being the discovery moment. Advisory only; never installs anything."
|
|
5
|
+
}
|
|
6
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wsl-env",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "让 DeepSeek Harness 以 WSL 子系统作为会话的执行环境:shell、文件读写与命令执行均在子系统内完成,目录选择器与 GUI 终端同样定位到子系统路径,并通过子系统内的 bubblewrap 对命令进行沙箱隔离",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -11,32 +11,78 @@
|
|
|
11
11
|
"windows"
|
|
12
12
|
],
|
|
13
13
|
"type": "module",
|
|
14
|
+
"types": "./lib/shell.d.ts",
|
|
14
15
|
"main": "lib/shell.js",
|
|
15
16
|
"exports": {
|
|
16
|
-
".":
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/shell.d.ts",
|
|
19
|
+
"default": "./lib/shell.js"
|
|
20
|
+
},
|
|
21
|
+
"./agent": {
|
|
22
|
+
"types": "./lib/agent.d.ts",
|
|
23
|
+
"default": "./lib/agent.js"
|
|
24
|
+
},
|
|
25
|
+
"./auto-preset": {
|
|
26
|
+
"types": "./lib/auto-preset.d.ts",
|
|
27
|
+
"default": "./lib/auto-preset.js"
|
|
28
|
+
},
|
|
19
29
|
"./auto-preset/locale/en.json": "./locale/auto-preset/en.json",
|
|
20
30
|
"./auto-preset/locale/zh.json": "./locale/auto-preset/zh.json",
|
|
21
|
-
"./file-reference-wsl":
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
"./file-reference-wsl": {
|
|
32
|
+
"types": "./lib/file-reference-wsl.d.ts",
|
|
33
|
+
"default": "./lib/file-reference-wsl.js"
|
|
34
|
+
},
|
|
35
|
+
"./fs": {
|
|
36
|
+
"types": "./lib/fs.d.ts",
|
|
37
|
+
"default": "./lib/fs.js"
|
|
38
|
+
},
|
|
39
|
+
"./fs-routing": {
|
|
40
|
+
"types": "./lib/fs-routing.d.ts",
|
|
41
|
+
"default": "./lib/fs-routing.js"
|
|
42
|
+
},
|
|
24
43
|
"./locale/en.json": "./locale/en.json",
|
|
25
44
|
"./locale/zh.json": "./locale/zh.json",
|
|
26
45
|
"./package.json": "./package.json",
|
|
27
|
-
"./paths":
|
|
28
|
-
|
|
46
|
+
"./paths": {
|
|
47
|
+
"types": "./lib/paths.d.ts",
|
|
48
|
+
"default": "./lib/paths.js"
|
|
49
|
+
},
|
|
50
|
+
"./preflight": {
|
|
51
|
+
"types": "./lib/preflight.d.ts",
|
|
52
|
+
"default": "./lib/preflight.js"
|
|
53
|
+
},
|
|
54
|
+
"./picker": {
|
|
55
|
+
"types": "./lib/picker.d.ts",
|
|
56
|
+
"default": "./lib/picker.js"
|
|
57
|
+
},
|
|
29
58
|
"./picker/locale/en.json": "./locale/picker/en.json",
|
|
30
59
|
"./picker/locale/zh.json": "./locale/picker/zh.json",
|
|
31
|
-
"./
|
|
32
|
-
"./
|
|
60
|
+
"./preflight/locale/en.json": "./locale/preflight/en.json",
|
|
61
|
+
"./preflight/locale/zh.json": "./locale/preflight/zh.json",
|
|
62
|
+
"./shell": {
|
|
63
|
+
"types": "./lib/shell.d.ts",
|
|
64
|
+
"default": "./lib/shell.js"
|
|
65
|
+
},
|
|
66
|
+
"./shell-env": {
|
|
67
|
+
"types": "./lib/shell-env.d.ts",
|
|
68
|
+
"default": "./lib/shell-env.js"
|
|
69
|
+
},
|
|
33
70
|
"./shell-env/locale/en.json": "./locale/shell-env/en.json",
|
|
34
71
|
"./shell-env/locale/zh.json": "./locale/shell-env/zh.json",
|
|
35
|
-
"./subprocess":
|
|
72
|
+
"./subprocess": {
|
|
73
|
+
"types": "./lib/subprocess.d.ts",
|
|
74
|
+
"default": "./lib/subprocess.js"
|
|
75
|
+
},
|
|
36
76
|
"./subprocess/locale/en.json": "./locale/subprocess/en.json",
|
|
37
77
|
"./subprocess/locale/zh.json": "./locale/subprocess/zh.json",
|
|
38
|
-
"./workspace-files-wsl":
|
|
39
|
-
|
|
78
|
+
"./workspace-files-wsl": {
|
|
79
|
+
"types": "./lib/workspace-files-wsl.d.ts",
|
|
80
|
+
"default": "./lib/workspace-files-wsl.js"
|
|
81
|
+
},
|
|
82
|
+
"./wsl": {
|
|
83
|
+
"types": "./lib/wsl.d.ts",
|
|
84
|
+
"default": "./lib/wsl.js"
|
|
85
|
+
}
|
|
40
86
|
},
|
|
41
87
|
"dsh": {
|
|
42
88
|
"bundle": {
|
|
@@ -45,6 +91,7 @@
|
|
|
45
91
|
},
|
|
46
92
|
"files": [
|
|
47
93
|
"lib/**/*.js",
|
|
94
|
+
"lib/**/*.d.ts",
|
|
48
95
|
"agent/*.sh",
|
|
49
96
|
"locale/**/*.json",
|
|
50
97
|
"cordis.patch.yml",
|
|
@@ -63,15 +110,19 @@
|
|
|
63
110
|
"SECURITY.md",
|
|
64
111
|
"SUPPORT.md",
|
|
65
112
|
"LICENSE",
|
|
66
|
-
"scripts/bootstrap.sh"
|
|
113
|
+
"scripts/bootstrap.sh",
|
|
114
|
+
"scripts/diagnose.sh"
|
|
67
115
|
],
|
|
68
116
|
"scripts": {
|
|
69
|
-
"test": "
|
|
70
|
-
"
|
|
117
|
+
"test": "pnpm run lint:style && pnpm run test:syntax && pnpm run lint:build && pnpm run lint:types && pnpm run test:unit",
|
|
118
|
+
"lint:types": "tsc --noEmit",
|
|
119
|
+
"build": "node scripts/build.mjs",
|
|
120
|
+
"lint:build": "node test/build-freshness.mjs",
|
|
121
|
+
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 test/agent-exec.test.mjs test/agent-fs-script.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/fs-substrate.test.mjs test/fs.boot.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/fsio-agent.test.mjs test/fsio-text.test.mjs test/git-route.test.mjs test/listing.test.mjs test/paths.test.mjs test/ports.test.mjs test/preset-choice.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/sandbox.test.mjs test/search-exec.test.mjs test/search-route.test.mjs test/shell.test.mjs test/terminal-activity.test.mjs test/terminal.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs",
|
|
71
122
|
"test:syntax": "node test/syntax.mjs",
|
|
72
|
-
"test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/file-reference-wsl.test.mjs test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preset-choice.test.mjs test/sandbox.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/agent-fs-script.test.mjs test/fsio-agent.test.mjs test/fs-substrate.test.mjs test/agent-exec.test.mjs test/ports.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/git-route.test.mjs test/fsio-text.test.mjs test/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs",
|
|
123
|
+
"test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/preset-choice.test.mjs test/sandbox.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/agent-fs-script.test.mjs test/fsio-agent.test.mjs test/fs-substrate.test.mjs test/agent-exec.test.mjs test/ports.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/git-route.test.mjs test/fsio-text.test.mjs test/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs",
|
|
73
124
|
"lint:style": "node test/style.mjs",
|
|
74
|
-
"prepublishOnly": "
|
|
125
|
+
"prepublishOnly": "pnpm test",
|
|
75
126
|
"probe": "bash test/probe/run.sh",
|
|
76
127
|
"probe:missing-wsl": "bash test/probe/missing-wsl.sh",
|
|
77
128
|
"probe:mode": "bash test/probe/mode.sh",
|
|
@@ -85,10 +136,12 @@
|
|
|
85
136
|
"probe:agent": "bash test/probe/agent.sh",
|
|
86
137
|
"probe:substrate": "bash test/probe/substrate.sh",
|
|
87
138
|
"probe:exec": "bash test/probe/exec.sh",
|
|
88
|
-
"bootstrap": "bash scripts/bootstrap.sh"
|
|
139
|
+
"bootstrap": "bash scripts/bootstrap.sh",
|
|
140
|
+
"diagnose": "bash scripts/diagnose.sh"
|
|
89
141
|
},
|
|
142
|
+
"packageManager": "pnpm@11.7.0",
|
|
90
143
|
"engines": {
|
|
91
|
-
"node": ">=
|
|
144
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
92
145
|
},
|
|
93
146
|
"author": {
|
|
94
147
|
"name": "Andy Zhao",
|
|
@@ -122,7 +175,10 @@
|
|
|
122
175
|
"@deepseek-ai/dsh-subprocess": "0.2.0-rc.2",
|
|
123
176
|
"@deepseek-ai/dsh-subprocess-local": "0.2.0-rc.2",
|
|
124
177
|
"@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
|
|
125
|
-
"js-yaml": "4.
|
|
178
|
+
"@types/js-yaml": "^4.0.9",
|
|
179
|
+
"@types/node": "~22.20.5",
|
|
180
|
+
"js-yaml": "4.3.2",
|
|
181
|
+
"typescript": "~6.0.3"
|
|
126
182
|
},
|
|
127
183
|
"peerDependencies": {
|
|
128
184
|
"@deepseek-ai/cordis": "~4.0.4",
|