dsh-plugin-wsl-env 0.7.5 → 0.8.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 (79) hide show
  1. package/CHANGELOG.md +278 -0
  2. package/CONTRIBUTING.md +129 -39
  3. package/README.md +20 -20
  4. package/docs/ARCHITECTURE.md +6 -3
  5. package/docs/LIMITATIONS.md +13 -0
  6. package/docs/README.zh.md +20 -20
  7. package/lib/agent-confined.d.ts +42 -0
  8. package/lib/agent-confined.js +25 -41
  9. package/lib/agent-errors.d.ts +31 -0
  10. package/lib/agent-errors.js +19 -16
  11. package/lib/agent-exec.d.ts +159 -0
  12. package/lib/agent-exec.js +151 -165
  13. package/lib/agent-protocol.d.ts +192 -0
  14. package/lib/agent-protocol.js +76 -100
  15. package/lib/agent-shared.d.ts +27 -0
  16. package/lib/agent-shared.js +12 -12
  17. package/lib/agent.d.ts +353 -0
  18. package/lib/agent.js +536 -552
  19. package/lib/auto-preset.d.ts +110 -0
  20. package/lib/auto-preset.js +157 -141
  21. package/lib/bwrap.d.ts +90 -0
  22. package/lib/bwrap.js +18 -18
  23. package/lib/file-reference-wsl.d.ts +160 -0
  24. package/lib/file-reference-wsl.js +192 -173
  25. package/lib/fs-decisions.d.ts +107 -0
  26. package/lib/fs-decisions.js +64 -60
  27. package/lib/fs-routing.d.ts +182 -0
  28. package/lib/fs-routing.js +173 -116
  29. package/lib/fs-substrate.d.ts +258 -0
  30. package/lib/fs-substrate.js +248 -248
  31. package/lib/fs.d.ts +10 -0
  32. package/lib/fs.js +5 -0
  33. package/lib/fsio-agent.d.ts +307 -0
  34. package/lib/fsio-agent.js +414 -410
  35. package/lib/fsio-text.d.ts +115 -0
  36. package/lib/fsio-text.js +104 -84
  37. package/lib/git-route.d.ts +100 -0
  38. package/lib/git-route.js +46 -47
  39. package/lib/index.d.ts +1003 -0
  40. package/lib/index.js +1101 -1003
  41. package/lib/listing.d.ts +114 -0
  42. package/lib/listing.js +68 -55
  43. package/lib/paths.d.ts +182 -0
  44. package/lib/paths.js +134 -109
  45. package/lib/picker.d.ts +266 -0
  46. package/lib/picker.js +371 -351
  47. package/lib/ports.d.ts +44 -0
  48. package/lib/ports.js +39 -29
  49. package/lib/preset-choice.d.ts +33 -0
  50. package/lib/preset-choice.js +15 -7
  51. package/lib/probe-cache.d.ts +40 -0
  52. package/lib/probe-cache.js +35 -29
  53. package/lib/sandbox-core.d.ts +79 -0
  54. package/lib/sandbox-core.js +71 -88
  55. package/lib/sandbox.d.ts +48 -0
  56. package/lib/sandbox.js +4 -4
  57. package/lib/search-exec.d.ts +96 -0
  58. package/lib/search-exec.js +52 -79
  59. package/lib/search-route.d.ts +75 -0
  60. package/lib/search-route.js +30 -31
  61. package/lib/shell-env.d.ts +88 -0
  62. package/lib/shell-env.js +78 -74
  63. package/lib/shell.d.ts +10 -0
  64. package/lib/shell.js +5 -0
  65. package/lib/subprocess.d.ts +313 -0
  66. package/lib/subprocess.js +267 -262
  67. package/lib/terminal-activity.d.ts +98 -0
  68. package/lib/terminal-activity.js +29 -39
  69. package/lib/terminal-route.d.ts +27 -0
  70. package/lib/terminal-route.js +16 -8
  71. package/lib/watcher.d.ts +77 -0
  72. package/lib/watcher.js +120 -126
  73. package/lib/workspace-files-route.d.ts +132 -0
  74. package/lib/workspace-files-route.js +73 -81
  75. package/lib/workspace-files-wsl.d.ts +257 -0
  76. package/lib/workspace-files-wsl.js +311 -251
  77. package/lib/wsl.d.ts +266 -0
  78. package/lib/wsl.js +144 -155
  79. package/package.json +68 -20
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,78 @@ 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
+ return first;
107
110
  }
108
-
109
111
  /**
110
112
  * The Linux home directory of a distro's default user, as a Linux path. `$HOME`
111
113
  * is set by `wsl.exe` itself from the distro's default user, so this needs no
@@ -117,17 +119,25 @@ export async function defaultDistro(options = {}) {
117
119
  * @throws when the distro cannot be queried or reports no home.
118
120
  */
119
121
  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;
122
+ const stdout = await runCapture([options.wslPath ?? DEFAULT_WSL_PATH, "-d", distro, "--exec", "sh", "-c", 'printf %s "$HOME"'], options.signal);
123
+ return parseHomePath(distro, stdout);
124
+ }
125
+ /**
126
+ * Validate the distro's reported $HOME. A pure seam: the failure shapes
127
+ * (empty output, a Windows path, mojibake) are unit-testable without
128
+ * `wsl.exe`.
129
+ *
130
+ * @param distro - the distro name, for the error message.
131
+ * @param stdout - the raw `printf %s "$HOME"` output.
132
+ * @returns the Linux home path.
133
+ */
134
+ export function parseHomePath(distro, stdout) {
135
+ const home = String(stdout).trim();
136
+ if (home.length === 0 || !home.startsWith("/")) {
137
+ throw new Error(`dsh-plugin-wsl: distro "${distro}" reported no usable $HOME (got "${home}")`);
138
+ }
139
+ return home;
129
140
  }
130
-
131
141
  /**
132
142
  * The same home in the world coordinate system, which is what a host-side
133
143
  * consumer needs.
@@ -138,27 +148,8 @@ export async function linuxHomePath(distro, options = {}) {
138
148
  * @throws when the distro cannot be queried or reports no home.
139
149
  */
140
150
  export async function linuxHome(distro, options = {}) {
141
- return posixToUnc(distro, await linuxHomePath(distro, options));
151
+ return posixToUnc(distro, await linuxHomePath(distro, options));
142
152
  }
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
153
  /**
163
154
  * Shells known to accept the combined POSIX `-lc` login-plus-command form.
164
155
  *
@@ -168,7 +159,6 @@ export async function linuxHome(distro, options = {}) {
168
159
  * — it just does not get login-shell semantics.
169
160
  */
170
161
  const POSIX_LOGIN_SHELLS = new Set(["sh", "bash", "dash", "zsh", "ksh", "ksh93", "mksh", "ash", "busybox"]);
171
-
172
162
  /**
173
163
  * The argv flags that make `shell` run `command`, with login semantics when the
174
164
  * shell supports them.
@@ -177,6 +167,12 @@ const POSIX_LOGIN_SHELLS = new Set(["sh", "bash", "dash", "zsh", "ksh", "ksh93",
177
167
  * @param login - whether login-shell semantics were requested.
178
168
  * @returns the flags to place before the command string.
179
169
  */
170
+ export function shellArgs(shellPath, login) {
171
+ const name = String(shellPath).slice(String(shellPath).lastIndexOf("/") + 1).toLowerCase();
172
+ if (!POSIX_LOGIN_SHELLS.has(name))
173
+ return ["-c"];
174
+ return login ? ["-lc"] : ["-c"];
175
+ }
180
176
  /**
181
177
  * The working directory `wsl.exe` could not enter, when its relay said so.
182
178
  *
@@ -190,22 +186,15 @@ const POSIX_LOGIN_SHELLS = new Set(["sh", "bash", "dash", "zsh", "ksh", "ksh93",
190
186
  * run into an error. Verified against `wsl.exe -d <distro> --cd /missing --exec sh`:
191
187
  * exit 0, `hi` on stdout, this shape on stderr.
192
188
  *
193
- * @param stdout - a command's captured stdout.
194
- * @param stderr - its captured stderr.
189
+ * @param stdout - a command's captured stdout; absent reads as empty.
190
+ * @param stderr - its captured stderr; absent reads as empty.
195
191
  * @returns the directory that could not be entered, or undefined.
196
192
  */
197
193
  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];
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"];
194
+ const text = `${stdout ?? ""}\n${stderr ?? ""}`.replace(/\0/g, "");
195
+ const match = /<3>WSL[^\n]*CreateProcessCommon[^\n]*chdir\(([^)]*)\) failed/.exec(text);
196
+ return match?.[1];
207
197
  }
208
-
209
198
  /**
210
199
  * Shells whose `-l` flag means a login shell.
211
200
  *
@@ -215,7 +204,6 @@ export function shellArgs(shellPath, login) {
215
204
  * nushell/xonsh use long flags, so they deliberately get nothing.
216
205
  */
217
206
  const LOGIN_FLAG_SHELLS = new Set([...POSIX_LOGIN_SHELLS, "fish"]);
218
-
219
207
  /**
220
208
  * The flags that make an interactive shell a login shell.
221
209
  *
@@ -228,10 +216,9 @@ const LOGIN_FLAG_SHELLS = new Set([...POSIX_LOGIN_SHELLS, "fish"]);
228
216
  * @returns `["-l"]`, or an empty array for a shell whose login flag differs.
229
217
  */
230
218
  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"] : [];
219
+ const name = String(shellPath).slice(String(shellPath).lastIndexOf("/") + 1).toLowerCase();
220
+ return login && LOGIN_FLAG_SHELLS.has(name) ? ["-l"] : [];
233
221
  }
234
-
235
222
  /**
236
223
  * The `wsl.exe` argv that opens one interactive shell inside a distro.
237
224
  *
@@ -250,19 +237,17 @@ export function interactiveShellArgs(shellPath, login) {
250
237
  * rejects an empty `--cd` argument whereas its own default starts in the
251
238
  * distro user's home.
252
239
  *
253
- * @param options - the `wsl.exe` path, distro, optional shell, its args, cwd and login mode.
240
+ * @param options - the launch description.
254
241
  * @returns the argv to spawn on the host.
255
242
  */
256
243
  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];
244
+ const { wslPath = DEFAULT_WSL_PATH, distro = "", shellPath, args = [], linuxCwd, login = true } = options;
245
+ const directory = typeof linuxCwd === "string" && linuxCwd.length > 0 ? ["--cd", linuxCwd] : [];
246
+ const shell = typeof shellPath === "string" && shellPath.length > 0
247
+ ? ["--exec", shellPath, ...interactiveShellArgs(shellPath, login), ...args]
248
+ : [];
249
+ return [wslPath, ...(distro.length > 0 ? ["-d", distro] : []), ...directory, ...shell];
264
250
  }
265
-
266
251
  /**
267
252
  * Managed facts of the `DSH_*` namespace that carry a Windows path. WSL's
268
253
  * `WSLENV` `/p` flag translates them on the way into the distro, so a command
@@ -270,7 +255,6 @@ export function wslTerminalArgv(options) {
270
255
  * absent: it is already a POSIX path, and translating it would corrupt it.
271
256
  */
272
257
  const PATH_TRANSLATED_ENV = new Set(["DSH_HOME", "DSH_PROFILE_DIR"]);
273
-
274
258
  /**
275
259
  * Render WSLENV's value from the env names a call wants the distro to import.
276
260
  *
@@ -281,21 +265,8 @@ const PATH_TRANSLATED_ENV = new Set(["DSH_HOME", "DSH_PROFILE_DIR"]);
281
265
  * @returns the `:`-joined WSLENV value.
282
266
  */
283
267
  export function wslEnvValue(names) {
284
- return names.map((name) => (PATH_TRANSLATED_ENV.has(name) ? `${name}/p` : name)).join(":");
268
+ return names.map((name) => (PATH_TRANSLATED_ENV.has(name) ? `${name}/p` : name)).join(":");
285
269
  }
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
270
  /**
300
271
  * The `WSL_E_*` code inside `wsl.exe`'s own output, when it reported one.
301
272
  *
@@ -306,49 +277,67 @@ export function wslEnvValue(names) {
306
277
  * `wsl.exe -d nosuchdistro --exec …` exits 255 with an empty stderr and
307
278
  * `Wsl/Service/WSL_E_DISTRO_NOT_FOUND` on stdout.
308
279
  *
309
- * @param stdout - a command's captured stdout.
310
- * @param stderr - its captured stderr.
280
+ * @param stdout - a command's captured stdout; absent reads as empty.
281
+ * @param stderr - its captured stderr; absent reads as empty.
311
282
  * @returns the code, for example `WSL_E_DISTRO_NOT_FOUND`, or undefined.
312
283
  */
313
284
  export function wslErrorCode(stdout, stderr) {
314
- const text = `${stdout ?? ""}\n${stderr ?? ""}`.replace(/\0/g, "");
315
- return /WSL_E_[A-Z0-9_]+/.exec(text)?.[0];
285
+ const text = `${stdout ?? ""}\n${stderr ?? ""}`.replace(/\0/g, "");
286
+ return /WSL_E_[A-Z0-9_]+/.exec(text)?.[0];
316
287
  }
317
-
288
+ /**
289
+ * Resolve the shell a distro's user actually gets, rather than assuming bash.
290
+ *
291
+ * The authoritative source is the login shell field of the user's passwd entry;
292
+ * `$SHELL` (which `wsl.exe` derives from it) is the fallback for distros without
293
+ * `getent`, and bash is the last resort. The passwd field is preferred because a
294
+ * user can export a different `SHELL` without changing their login shell.
295
+ *
296
+ * @param distro - distro name.
297
+ * @param options - the `wsl.exe` path and optional cancellation.
298
+ * @returns an absolute path to the shell inside the distro, or undefined when
299
+ * both probes failed.
300
+ */
318
301
  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
- );
302
+ const wslPath = options.wslPath ?? DEFAULT_WSL_PATH;
303
+ /**
304
+ * Ask the distro one `sh -c` question; only absolute paths count as answers.
305
+ *
306
+ * @param script - the shell snippet to run.
307
+ * @returns the trimmed answer, or undefined.
308
+ */
309
+ const ask = async (script) => {
310
+ try {
311
+ const value = (await runCapture([wslPath, "-d", distro, "--exec", "sh", "-c", script], options.signal)).trim();
312
+ return value.startsWith("/") ? value : undefined;
313
+ }
314
+ catch {
315
+ return undefined;
316
+ }
317
+ };
318
+ // No "bash" placeholder: a failed probe (a cold distro, a flaky first
319
+ // `wsl.exe` call) used to be pinned as the literal shell for the
320
+ // executor's lifetime. `undefined` tells the caller both answers failed;
321
+ // it may retry later or fall back itself.
322
+ return ((await ask('getent passwd "$(id -u)" | cut -d: -f7')) ??
323
+ (await ask('printf %s "$SHELL"')) ??
324
+ undefined);
337
325
  }
338
-
339
326
  /**
340
327
  * Run one command inside a distro and return raw stdout. Used by callers that
341
328
  * want Linux semantics instead of the UNC share's.
342
329
  *
343
330
  * @param distro - distro name.
344
331
  * @param command - a shell command string.
345
- * @param options - the `wsl.exe` path, an explicit `shell`, and optional cancellation.
332
+ * @param options - the `wsl.exe` path, an explicit `shell`, and optional
333
+ * cancellation.
346
334
  * @returns captured stdout.
335
+ * @throws when neither the caller nor the distro names a usable shell.
347
336
  */
348
337
  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
- );
338
+ const shell = options.shell ?? (await defaultShell(distro, options));
339
+ if (shell === undefined) {
340
+ throw new Error(`dsh-plugin-wsl: distro "${distro}" named no usable shell (passwd probe and $SHELL both failed)`);
341
+ }
342
+ return runCapture([options.wslPath ?? DEFAULT_WSL_PATH, "-d", distro, "--exec", shell, ...shellArgs(shell, options.loginShell !== false), command], options.signal);
354
343
  }
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.8.0",
4
4
  "description": "让 DeepSeek Harness 以 WSL 子系统作为会话的执行环境:shell、文件读写与命令执行均在子系统内完成,目录选择器与 GUI 终端同样定位到子系统路径,并通过子系统内的 bubblewrap 对命令进行沙箱隔离",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -11,32 +11,72 @@
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
+ "./picker": {
51
+ "types": "./lib/picker.d.ts",
52
+ "default": "./lib/picker.js"
53
+ },
29
54
  "./picker/locale/en.json": "./locale/picker/en.json",
30
55
  "./picker/locale/zh.json": "./locale/picker/zh.json",
31
- "./shell": "./lib/shell.js",
32
- "./shell-env": "./lib/shell-env.js",
56
+ "./shell": {
57
+ "types": "./lib/shell.d.ts",
58
+ "default": "./lib/shell.js"
59
+ },
60
+ "./shell-env": {
61
+ "types": "./lib/shell-env.d.ts",
62
+ "default": "./lib/shell-env.js"
63
+ },
33
64
  "./shell-env/locale/en.json": "./locale/shell-env/en.json",
34
65
  "./shell-env/locale/zh.json": "./locale/shell-env/zh.json",
35
- "./subprocess": "./lib/subprocess.js",
66
+ "./subprocess": {
67
+ "types": "./lib/subprocess.d.ts",
68
+ "default": "./lib/subprocess.js"
69
+ },
36
70
  "./subprocess/locale/en.json": "./locale/subprocess/en.json",
37
71
  "./subprocess/locale/zh.json": "./locale/subprocess/zh.json",
38
- "./workspace-files-wsl": "./lib/workspace-files-wsl.js",
39
- "./wsl": "./lib/wsl.js"
72
+ "./workspace-files-wsl": {
73
+ "types": "./lib/workspace-files-wsl.d.ts",
74
+ "default": "./lib/workspace-files-wsl.js"
75
+ },
76
+ "./wsl": {
77
+ "types": "./lib/wsl.d.ts",
78
+ "default": "./lib/wsl.js"
79
+ }
40
80
  },
41
81
  "dsh": {
42
82
  "bundle": {
@@ -45,6 +85,7 @@
45
85
  },
46
86
  "files": [
47
87
  "lib/**/*.js",
88
+ "lib/**/*.d.ts",
48
89
  "agent/*.sh",
49
90
  "locale/**/*.json",
50
91
  "cordis.patch.yml",
@@ -66,12 +107,15 @@
66
107
  "scripts/bootstrap.sh"
67
108
  ],
68
109
  "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",
110
+ "test": "pnpm run lint:style && pnpm run test:syntax && pnpm run lint:build && pnpm run lint:types && pnpm run test:unit",
111
+ "lint:types": "tsc --noEmit",
112
+ "build": "node scripts/build.mjs",
113
+ "lint:build": "node test/build-freshness.mjs",
114
+ "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/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",
71
115
  "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",
116
+ "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/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
117
  "lint:style": "node test/style.mjs",
74
- "prepublishOnly": "npm test",
118
+ "prepublishOnly": "pnpm test",
75
119
  "probe": "bash test/probe/run.sh",
76
120
  "probe:missing-wsl": "bash test/probe/missing-wsl.sh",
77
121
  "probe:mode": "bash test/probe/mode.sh",
@@ -87,8 +131,9 @@
87
131
  "probe:exec": "bash test/probe/exec.sh",
88
132
  "bootstrap": "bash scripts/bootstrap.sh"
89
133
  },
134
+ "packageManager": "pnpm@11.7.0",
90
135
  "engines": {
91
- "node": ">=20"
136
+ "node": "^22.19.0 || >=24.0.0"
92
137
  },
93
138
  "author": {
94
139
  "name": "Andy Zhao",
@@ -122,7 +167,10 @@
122
167
  "@deepseek-ai/dsh-subprocess": "0.2.0-rc.2",
123
168
  "@deepseek-ai/dsh-subprocess-local": "0.2.0-rc.2",
124
169
  "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
125
- "js-yaml": "4.1.0"
170
+ "@types/js-yaml": "^4.0.9",
171
+ "@types/node": "~22.20.5",
172
+ "js-yaml": "4.3.2",
173
+ "typescript": "~6.0.3"
126
174
  },
127
175
  "peerDependencies": {
128
176
  "@deepseek-ai/cordis": "~4.0.4",