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/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 default
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
- const [file, ...args] = argv;
46
- return new Promise((resolve, reject) => {
47
- execFile(
48
- file,
49
- args,
50
- { env: { ...process.env, WSL_UTF8: "1" }, windowsHide: true, signal, encoding: "utf8", timeout: deadlineMs },
51
- (error, stdout, stderr) => {
52
- if (error) {
53
- // A denied WSL call is the likeliest failure here and its own message
54
- // is unhelpful, so name the cause when it is recognisable.
55
- const detail = `${stdout ?? ""}${stderr ?? ""}`.replace(/\u0000/g, "").trim();
56
- const denied = /E_ACCESSDENIED/i.test(detail) || error.code === "EACCES";
57
- const missing = error.code === "ENOENT";
58
- // The deadline kill arrives as a SIGTERM with no WSL diagnostic, so
59
- // it must be named for what it is — not left looking like WSL's own
60
- // failure. An abort is cancellation, not a deadline.
61
- const stuck = error.killed === true && error.name !== "AbortError";
62
- error.message = `${file} failed: ${detail || error.message}${
63
- denied
64
- ? " (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.)"
65
- : ""
66
- }${
67
- missing
68
- ? ` (the executable "${file}" could not be started: check \`wslPath\` in this row, and that WSL is installed)`
69
- : ""
70
- }${
71
- stuck
72
- ? ` (no completion within ${deadlineMs}ms — the WSL service may be wedged; retry once, then \`wsl.exe --shutdown\` and reopen)`
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
- const stdout = await runCapture([options.wslPath ?? DEFAULT_WSL_PATH, "-l", "-q"], options.signal);
91
- return stdout
92
- .split(/\r?\n/)
93
- .map((line) => line.trim())
94
- .filter((line) => line.length > 0);
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
- const [first] = await listDistros(options);
105
- if (first === undefined) throw new Error("dsh-plugin-wsl: no WSL distro is installed (`wsl.exe -l -q` returned nothing)");
106
- return first;
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
- const stdout = await runCapture(
121
- [options.wslPath ?? DEFAULT_WSL_PATH, "-d", distro, "--exec", "sh", "-c", 'printf %s "$HOME"'],
122
- options.signal,
123
- );
124
- const home = stdout.trim();
125
- if (home.length === 0 || !home.startsWith("/")) {
126
- throw new Error(`dsh-plugin-wsl: distro "${distro}" reported no usable $HOME (got "${home}")`);
127
- }
128
- return home;
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
- return posixToUnc(distro, await linuxHomePath(distro, options));
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
- const text = `${stdout ?? ""}\n${stderr ?? ""}`.replace(/\0/g, "");
199
- const match = /<3>WSL[^\n]*CreateProcessCommon[^\n]*chdir\(([^)]*)\) failed/.exec(text);
200
- return match?.[1];
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
- const name = String(shellPath).slice(String(shellPath).lastIndexOf("/") + 1).toLowerCase();
232
- return login && LOGIN_FLAG_SHELLS.has(name) ? ["-l"] : [];
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 `wsl.exe` path, distro, optional shell, its args, cwd and login mode.
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
- const { wslPath = DEFAULT_WSL_PATH, distro = "", shellPath, args = [], linuxCwd, login = true } = options;
258
- const directory = typeof linuxCwd === "string" && linuxCwd.length > 0 ? ["--cd", linuxCwd] : [];
259
- const shell =
260
- typeof shellPath === "string" && shellPath.length > 0
261
- ? ["--exec", shellPath, ...interactiveShellArgs(shellPath, login), ...args]
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
- return names.map((name) => (PATH_TRANSLATED_ENV.has(name) ? `${name}/p` : name)).join(":");
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
- const text = `${stdout ?? ""}\n${stderr ?? ""}`.replace(/\0/g, "");
315
- return /WSL_E_[A-Z0-9_]+/.exec(text)?.[0];
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
- const wslPath = options.wslPath ?? DEFAULT_WSL_PATH;
320
- const ask = async (script) => {
321
- try {
322
- const value = (await runCapture([wslPath, "-d", distro, "--exec", "sh", "-c", script], options.signal)).trim();
323
- return value.startsWith("/") ? value : undefined;
324
- } catch {
325
- return undefined;
326
- }
327
- };
328
- // No "bash" placeholder: a failed probe (a cold distro, a flaky first
329
- // `wsl.exe` call) used to be pinned as the literal shell for the
330
- // executor's lifetime. `undefined` tells the caller both answers failed;
331
- // it may retry later or fall back itself.
332
- return (
333
- (await ask('getent passwd "$(id -u)" | cut -d: -f7')) ??
334
- (await ask('printf %s "$SHELL"')) ??
335
- undefined
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 cancellation.
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
- const shell = options.shell ?? (await defaultShell(distro, options));
350
- return runCapture(
351
- [options.wslPath ?? DEFAULT_WSL_PATH, "-d", distro, "--exec", shell, ...shellArgs(shell, options.loginShell !== false), command],
352
- options.signal,
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
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "WSL 沙箱预检",
4
+ "description": "发行版会话打开时探测 bubblewrap,把缺失或不可用的 bwrap 连同确切的修复指引作为警告提前给出——而不是让第一条命令的 fail-closed 报错充当发现时刻。纯建议性,绝不安装任何东西。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-wsl-env",
3
- "version": "0.7.5",
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
- ".": "./lib/shell.js",
17
- "./agent": "./lib/agent.js",
18
- "./auto-preset": "./lib/auto-preset.js",
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": "./lib/file-reference-wsl.js",
22
- "./fs": "./lib/fs.js",
23
- "./fs-routing": "./lib/fs-routing.js",
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": "./lib/paths.js",
28
- "./picker": "./lib/picker.js",
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
- "./shell": "./lib/shell.js",
32
- "./shell-env": "./lib/shell-env.js",
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": "./lib/subprocess.js",
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": "./lib/workspace-files-wsl.js",
39
- "./wsl": "./lib/wsl.js"
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": "npm run lint:style && npm run test:syntax && npm run test:unit",
70
- "test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=85 --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/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/sandbox.test.mjs test/search-exec.test.mjs test/search-route.test.mjs test/fs.boot.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",
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": "npm test",
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": ">=20"
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.1.0"
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",