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/paths.js CHANGED
@@ -13,89 +13,106 @@
13
13
  * These helpers are pure and dependency-free so they can be unit-tested without
14
14
  * booting Cordis.
15
15
  *
16
+ * This is a TypeScript source built to `lib/paths.js`; edit THIS file and run
17
+ * `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
18
+ * fails when it drifts.
19
+ *
16
20
  * @module dsh-plugin-wsl/paths
17
21
  */
18
-
19
22
  /** The WSL UNC share prefix Node can read. `\\wsl$\<distro>\...` is the legacy spelling. */
20
23
  const UNC_RE = /^\\\\wsl(?:\.localhost|\$)\\([^\\]+)(?:\\([\s\S]*))?$/i;
21
24
  const DRIVE_RE = /^([A-Za-z]):[\\/]([\s\S]*)$/;
22
-
23
- /** Normalize Windows separators without touching a POSIX path. */
25
+ /**
26
+ * Normalize Windows separators without touching a POSIX path.
27
+ *
28
+ * @param value - the path to normalize.
29
+ * @returns the same path with `/` written as `\`.
30
+ */
24
31
  function toWindowsSlashes(value) {
25
- return String(value).replace(/\//g, "\\");
32
+ return String(value).replace(/\//g, "\\");
26
33
  }
27
-
28
34
  /**
29
35
  * Whether a string is the WSL UNC share rather than an ordinary Windows path.
36
+ *
30
37
  * @param value - candidate path.
31
38
  * @returns true for `\\wsl.localhost\...` and `\\wsl$\...`.
32
39
  */
33
40
  export function isWslUnc(value) {
34
- return UNC_RE.test(String(value));
41
+ return UNC_RE.test(String(value));
35
42
  }
36
-
37
43
  /**
38
44
  * Whether a string is POSIX-absolute. On Windows `path.isAbsolute('/home/x')`
39
45
  * is also true, so the WSL mapper must test this before falling back to the
40
46
  * host helpers.
47
+ *
41
48
  * @param value - candidate path.
42
49
  * @returns true when the path starts with a single forward slash.
43
50
  */
44
51
  export function isPosixAbsolute(value) {
45
- return /^\/(?!\/)/.test(String(value));
52
+ return /^\/(?!\/)/.test(String(value));
46
53
  }
47
-
48
54
  /**
49
55
  * Whether a path names a location only relative to a base. The fs provider
50
56
  * consults its default workdir exactly for this input; every absolute form —
51
57
  * POSIX, drive, WSL UNC, or any other UNC share — is a location in its own
52
58
  * right and short-circuits before a base is ever needed.
59
+ *
53
60
  * @param value - candidate path.
54
61
  * @returns true when the path names no absolute location.
55
62
  */
56
63
  export function isRelativeWorldPath(value) {
57
- const text = String(value ?? "");
58
- if (text.length === 0) return false;
59
- return !isWslUnc(text) && !isPosixAbsolute(text) && !DRIVE_RE.test(text) && !text.startsWith("\\");
64
+ const text = String(value ?? "");
65
+ if (text.length === 0)
66
+ return false;
67
+ return !isWslUnc(text) && !isPosixAbsolute(text) && !DRIVE_RE.test(text) && !text.startsWith("\\");
60
68
  }
61
-
62
- /** Drop trailing separators, keeping a bare root (`C:\`, `\\wsl.localhost\x\`) meaningful. */
69
+ /**
70
+ * Drop trailing separators, keeping a bare root (`C:\`, `\\wsl.localhost\x\`) meaningful.
71
+ *
72
+ * @param value - the path to trim.
73
+ * @returns the path without its trailing separators.
74
+ */
63
75
  function trimTrailingSeparators(value) {
64
- return value.replace(/[\\/]+$/, "") || value;
76
+ return value.replace(/[\\/]+$/, "") || value;
65
77
  }
66
-
67
78
  /**
68
79
  * A UNC, drive, or (with a distro) Linux path as a comparable Windows key.
69
80
  * Windows semantics are case-insensitive and separator-tolerant, so the key is
70
81
  * lowercased and backslashed; a Linux path maps onto the distro's UNC share so
71
82
  * it can be compared with a target reached through that share.
83
+ *
72
84
  * @param value - the path to key.
73
85
  * @param distro - the distro whose share a Linux path maps onto, if any.
74
86
  * @returns the key, or undefined when the path is not in that vocabulary.
75
87
  */
76
88
  function toWindowsKey(value, distro) {
77
- const text = String(value ?? "");
78
- if (text.length === 0) return undefined;
79
- if (DRIVE_RE.test(text) || isWslUnc(text)) {
80
- const key = trimTrailingSeparators(toWindowsSlashes(text)).toLowerCase();
81
- // A drive root trims to `c:`; without its separator back the prefix test
82
- // would accept a sibling like `C:\ish` as a child of `C:\`.
83
- return /^[a-z]:$/.test(key) ? `${key}\\` : key;
84
- }
85
- if (distro !== undefined && distro !== "" && isPosixAbsolute(text)) {
86
- return trimTrailingSeparators(toWindowsSlashes(posixToUnc(distro, text))).toLowerCase();
87
- }
88
- return undefined;
89
+ const text = String(value ?? "");
90
+ if (text.length === 0)
91
+ return undefined;
92
+ if (DRIVE_RE.test(text) || isWslUnc(text)) {
93
+ const key = trimTrailingSeparators(toWindowsSlashes(text)).toLowerCase();
94
+ // A drive root trims to `c:`; without its separator back the prefix test
95
+ // would accept a sibling like `C:\ish` as a child of `C:\`.
96
+ return /^[a-z]:$/.test(key) ? `${key}\\` : key;
97
+ }
98
+ if (distro !== undefined && distro !== "" && isPosixAbsolute(text)) {
99
+ return trimTrailingSeparators(toWindowsSlashes(posixToUnc(distro, text))).toLowerCase();
100
+ }
101
+ return undefined;
89
102
  }
90
-
91
- /** A POSIX-absolute path as a comparable key, without its trailing separators. */
103
+ /**
104
+ * A POSIX-absolute path as a comparable key, without its trailing separators.
105
+ *
106
+ * @param value - the path to key.
107
+ * @returns the key, or undefined when the path is not POSIX-absolute.
108
+ */
92
109
  function toPosixKey(value) {
93
- const text = String(value ?? "");
94
- if (!isPosixAbsolute(text)) return undefined;
95
- const trimmed = text.replace(/\/+$/, "");
96
- return trimmed.length === 0 ? "/" : trimmed;
110
+ const text = String(value ?? "");
111
+ if (!isPosixAbsolute(text))
112
+ return undefined;
113
+ const trimmed = text.replace(/\/+$/, "");
114
+ return trimmed.length === 0 ? "/" : trimmed;
97
115
  }
98
-
99
116
  /**
100
117
  * Whether a world path is a granted root or lies beneath it, compared in the
101
118
  * coordinate system the two spellings share.
@@ -113,83 +130,86 @@ function toPosixKey(value) {
113
130
  * @returns true when target is root itself or a descendant of it.
114
131
  */
115
132
  export function isWorldPathUnder(target, root, options = {}) {
116
- const winTarget = toWindowsKey(target, options.distro);
117
- const winRoot = toWindowsKey(root, options.distro);
118
- if (winTarget !== undefined && winRoot !== undefined) {
119
- // A drive root keeps its separator in the key, so the child boundary is
120
- // "root already ends with one" rather than "append another".
121
- const prefix = winRoot.endsWith("\\") ? winRoot : `${winRoot}\\`;
122
- return winTarget === winRoot || winTarget.startsWith(prefix);
123
- }
124
- const posixTarget = toPosixKey(target);
125
- const posixRoot = toPosixKey(root);
126
- if (posixTarget !== undefined && posixRoot !== undefined) {
127
- return posixRoot === "/" || posixTarget === posixRoot || posixTarget.startsWith(`${posixRoot}/`);
128
- }
129
- return false;
133
+ const winTarget = toWindowsKey(target, options.distro);
134
+ const winRoot = toWindowsKey(root, options.distro);
135
+ if (winTarget !== undefined && winRoot !== undefined) {
136
+ // A drive root keeps its separator in the key, so the child boundary is
137
+ // "root already ends with one" rather than "append another".
138
+ const prefix = winRoot.endsWith("\\") ? winRoot : `${winRoot}\\`;
139
+ return winTarget === winRoot || winTarget.startsWith(prefix);
140
+ }
141
+ const posixTarget = toPosixKey(target);
142
+ const posixRoot = toPosixKey(root);
143
+ if (posixTarget !== undefined && posixRoot !== undefined) {
144
+ return posixRoot === "/" || posixTarget === posixRoot || posixTarget.startsWith(`${posixRoot}/`);
145
+ }
146
+ return false;
130
147
  }
131
-
132
148
  /**
133
149
  * Map a Linux path inside a distro onto the UNC path the host can read.
150
+ *
134
151
  * @param distro - distro name, e.g. `ubuntu`.
135
152
  * @param linuxPath - absolute Linux path, e.g. `/home/andy/proj`.
136
153
  * @returns the equivalent `\\wsl.localhost\<distro>\...` path.
137
154
  */
138
155
  export function posixToUnc(distro, linuxPath) {
139
- const rest = String(linuxPath).replace(/^\/+/, "").replace(/\//g, "\\");
140
- return `\\\\wsl.localhost\\${distro}\\${rest}`.replace(/\\+$/, "\\");
156
+ const rest = String(linuxPath).replace(/^\/+/, "").replace(/\//g, "\\");
157
+ return `\\\\wsl.localhost\\${distro}\\${rest}`.replace(/\\+$/, "\\");
141
158
  }
142
-
143
159
  /**
144
160
  * Map a WSL UNC path back onto the distro and Linux path it names.
145
- * @param uncPath - a `\\wsl.localhost\<distro>\...` path.
146
- * @returns `{ distro, linuxPath }`, or undefined for any other path.
161
+ *
162
+ * @param uncPath - a `\\wsl.localhost\<distro>\...` path; absent is not a UNC path.
163
+ * @returns the distro and Linux path, or undefined for any other path.
147
164
  */
148
165
  export function uncToPosix(uncPath) {
149
- const match = UNC_RE.exec(String(uncPath));
150
- if (!match?.[1]) return undefined;
151
- const rest = (match[2] ?? "").replace(/\\/g, "/");
152
- return { distro: match[1], linuxPath: rest.length > 0 ? `/${rest}` : "/" };
166
+ const match = UNC_RE.exec(String(uncPath));
167
+ if (!match?.[1])
168
+ return undefined;
169
+ const rest = (match[2] ?? "").replace(/\\/g, "/");
170
+ return { distro: match[1], linuxPath: rest.length > 0 ? `/${rest}` : "/" };
153
171
  }
154
-
155
172
  /**
156
173
  * Whether a UNC path belongs to the named distro. Used to confine the WSL
157
174
  * filesystem backend to one distro instead of silently serving every mounted
158
175
  * share.
176
+ *
159
177
  * @param uncPath - candidate UNC path.
160
178
  * @param distro - the distro this session is pinned to.
161
179
  * @returns true when the path is the same distro (case-insensitive).
162
180
  */
163
181
  export function isUnderDistro(uncPath, distro) {
164
- const parsed = uncToPosix(uncPath);
165
- return parsed !== undefined && parsed.distro.toLowerCase() === String(distro).toLowerCase();
182
+ const parsed = uncToPosix(uncPath);
183
+ return parsed !== undefined && parsed.distro.toLowerCase() === String(distro).toLowerCase();
166
184
  }
167
-
168
185
  /**
169
186
  * A WSL UNC that names a distro OTHER than the pinned one — the shape both
170
187
  * shell seams refuse: the shell executor will not run commands in it (running
171
188
  * there would mean silently switching distros on path similarity), and the
172
189
  * shell-env facts describe only the pinned distro, so a foreign session gets
173
190
  * no facts rather than another distro's home and shell.
174
- * @param uncPath - the candidate path (any spelling; non-UNC values are not foreign).
191
+ *
192
+ * @param value - the candidate path (any spelling; non-UNC values are not foreign).
175
193
  * @param distro - the pinned distro name.
176
194
  * @returns true when the value is a WSL UNC under a different distro.
177
195
  */
178
196
  export function isAnotherDistrosUnc(value, distro) {
179
- return isWslUnc(value) && !isUnderDistro(value, distro);
197
+ return isWslUnc(value) && !isUnderDistro(value, distro);
180
198
  }
181
-
182
199
  /**
183
200
  * Translate a Windows drive path into its WSL interop mount.
201
+ *
184
202
  * @param winPath - e.g. `C:\Users\andyz\Documents`.
185
203
  * @returns e.g. `/mnt/c/Users/andyz/Documents`, or undefined for a non-drive path.
186
204
  */
187
205
  export function windowsToLinuxMount(winPath) {
188
- const match = DRIVE_RE.exec(String(winPath));
189
- if (!match?.[1]) return undefined;
190
- return `/mnt/${match[1].toLowerCase()}/${match[2].replace(/\\/g, "/")}`;
206
+ const match = DRIVE_RE.exec(String(winPath));
207
+ const drive = match?.[1];
208
+ const rest = match?.[2];
209
+ if (drive === undefined || rest === undefined)
210
+ return undefined;
211
+ return `/mnt/${drive.toLowerCase()}/${rest.replace(/\\/g, "/")}`;
191
212
  }
192
-
193
213
  /**
194
214
  * Resolve a caller-supplied workdir into a path `wsl.exe --cd` accepts.
195
215
  *
@@ -202,19 +222,22 @@ export function windowsToLinuxMount(winPath) {
202
222
  * @returns an absolute Linux path, or the input unchanged when it cannot be mapped.
203
223
  */
204
224
  export function toLinuxPath(value, options) {
205
- const text = String(value ?? "");
206
- if (text.length === 0) return text;
207
- const parsed = uncToPosix(text);
208
- if (parsed !== undefined) return parsed.linuxPath;
209
- if (isPosixAbsolute(text)) return text;
210
- const mounted = windowsToLinuxMount(text);
211
- if (mounted !== undefined) return mounted;
212
- // Relative input is resolved against the configured default workdir.
213
- const base = options?.cwd ?? "/";
214
- const baseLinux = toLinuxPath(base, { distro: options?.distro });
215
- return `${baseLinux.replace(/\/+$/, "")}/${text.replace(/^\.\//, "")}`;
225
+ const text = String(value ?? "");
226
+ if (text.length === 0)
227
+ return text;
228
+ const parsed = uncToPosix(text);
229
+ if (parsed !== undefined)
230
+ return parsed.linuxPath;
231
+ if (isPosixAbsolute(text))
232
+ return text;
233
+ const mounted = windowsToLinuxMount(text);
234
+ if (mounted !== undefined)
235
+ return mounted;
236
+ // Relative input is resolved against the configured default workdir.
237
+ const base = options?.cwd ?? "/";
238
+ const baseLinux = toLinuxPath(base, { distro: options?.distro });
239
+ return `${baseLinux.replace(/\/+$/, "")}/${text.replace(/^\.\//, "")}`;
216
240
  }
217
-
218
241
  /**
219
242
  * Resolve a caller-supplied path into the UNC coordinate system the host's fs
220
243
  * calls use. Linux paths from the model, UNC paths from the harness, and
@@ -226,25 +249,29 @@ export function toLinuxPath(value, options) {
226
249
  * @returns an absolute host path (UNC for anything inside the distro).
227
250
  */
228
251
  export function toWorldPath(value, options) {
229
- const text = String(value ?? "");
230
- if (text.length === 0) return text;
231
- if (isWslUnc(text)) return toWindowsSlashes(text);
232
- // No distro fallback: an identity minted under a guessed distro name would
233
- // address a filesystem that does not exist.
234
- if (isPosixAbsolute(text)) {
235
- if (!options?.distro) return text;
236
- return posixToUnc(options.distro, text);
237
- }
238
- if (DRIVE_RE.test(text)) return toWindowsSlashes(text);
239
- // Any other UNC share is already a world path in its own right — the host
240
- // can open it directly, and joining it onto a base would name a file that
241
- // exists nowhere.
242
- if (text.startsWith("\\")) return toWindowsSlashes(text);
243
- const base = options?.cwd ?? process.cwd();
244
- const baseWorld = toWorldPath(base, { distro: options?.distro });
245
- return `${baseWorld.replace(/[\\/]+$/, "")}\\${toWindowsSlashes(text).replace(/^\.\\/, "")}`;
252
+ const text = String(value ?? "");
253
+ if (text.length === 0)
254
+ return text;
255
+ if (isWslUnc(text))
256
+ return toWindowsSlashes(text);
257
+ // No distro fallback: an identity minted under a guessed distro name would
258
+ // address a filesystem that does not exist.
259
+ if (isPosixAbsolute(text)) {
260
+ if (!options?.distro)
261
+ return text;
262
+ return posixToUnc(options.distro, text);
263
+ }
264
+ if (DRIVE_RE.test(text))
265
+ return toWindowsSlashes(text);
266
+ // Any other UNC share is already a world path in its own right — the host
267
+ // can open it directly, and joining it onto a base would name a file that
268
+ // exists nowhere.
269
+ if (text.startsWith("\\"))
270
+ return toWindowsSlashes(text);
271
+ const base = options?.cwd ?? process.cwd();
272
+ const baseWorld = toWorldPath(base, { distro: options?.distro });
273
+ return `${baseWorld.replace(/[\\/]+$/, "")}\\${toWindowsSlashes(text).replace(/^\.\\/, "")}`;
246
274
  }
247
-
248
275
  /**
249
276
  * Render a world path back into the form the model and the UI should see: a
250
277
  * Linux path inside the distro, and the ordinary Windows path everywhere else.
@@ -256,14 +283,13 @@ export function toWorldPath(value, options) {
256
283
  * @returns the display path.
257
284
  */
258
285
  export function toDisplayPath(worldPath, distro) {
259
- const parsed = uncToPosix(worldPath);
260
- if (parsed !== undefined && parsed.distro.toLowerCase() === String(distro).toLowerCase()) return parsed.linuxPath;
261
- return String(worldPath);
286
+ const parsed = uncToPosix(worldPath);
287
+ if (parsed !== undefined && parsed.distro.toLowerCase() === String(distro).toLowerCase())
288
+ return parsed.linuxPath;
289
+ return String(worldPath);
262
290
  }
263
-
264
291
  /** The provider root of the WSL UNC share — the level that holds one entry per distro. */
265
292
  export const UNC_PROVIDER_ROOT = "\\\\wsl.localhost";
266
-
267
293
  /**
268
294
  * The UNC root of one distro, which is the mount point of that distro's `/`.
269
295
  * This is a complete UNC path (`\\server\share`), which is what the directory
@@ -274,9 +300,8 @@ export const UNC_PROVIDER_ROOT = "\\\\wsl.localhost";
274
300
  * @returns e.g. `\\wsl.localhost\ubuntu`.
275
301
  */
276
302
  export function distroRoot(distro) {
277
- return `${UNC_PROVIDER_ROOT}\\${distro}`;
303
+ return `${UNC_PROVIDER_ROOT}\\${distro}`;
278
304
  }
279
-
280
305
  /**
281
306
  * Whether a path names the WSL share's root rather than a distro inside it.
282
307
  * Both share spellings and a trailing separator are accepted.
@@ -285,6 +310,6 @@ export function distroRoot(distro) {
285
310
  * @returns true for `\\wsl.localhost` and `\\wsl$` in any casing.
286
311
  */
287
312
  export function isProviderRoot(value) {
288
- const trimmed = String(value).replace(/[\\/]+$/, "").toLowerCase();
289
- return trimmed === UNC_PROVIDER_ROOT.toLowerCase() || trimmed === "\\\\wsl$";
313
+ const trimmed = String(value).replace(/[\\/]+$/, "").toLowerCase();
314
+ return trimmed === UNC_PROVIDER_ROOT.toLowerCase() || trimmed === "\\\\wsl$";
290
315
  }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * WSL-aware directory picker: the `ctx.directoryPicker` backend that makes the
3
+ * Web GUI's "Select Workspace Directory" dialog open inside a distro.
4
+ *
5
+ * ## Why this serves `kind: 'browse'` instead of declaring a `wsl` kind
6
+ *
7
+ * The seam's capability is a merge-extensible discriminated union, so adding a
8
+ * `wsl` kind looks like the natural move. It does not work, and the reason is
9
+ * worth stating plainly: the capability is **not** what crosses the wire.
10
+ * `dsh-api-workspace-controller` exposes exactly three Remote verbs and pins
11
+ * each to a literal kind —
12
+ *
13
+ * pick(signal) -> requireCapability('native', 'pick')
14
+ * list(path, signal) -> requireCapability('browse', 'list')
15
+ * createDirectory(...) -> requireCapability('browse', 'createDirectory')
16
+ *
17
+ * — and refuses anything else with `directory-picker/unavailable`. A backend
18
+ * reporting `{ kind: 'wsl', ... }` would therefore break the very verbs it
19
+ * needs, because `requireCapability('browse', ...)` would no longer match. The
20
+ * wire vocabulary lives in a shipped, versioned package inside `app.asar`, so
21
+ * adding a third kind means patching an asar package that the next app update
22
+ * overwrites.
23
+ *
24
+ * Serving `browse` is not a workaround, it is the fit: `browse`'s primitives are
25
+ * "list one absolute directory level" and "create one child directory", which is
26
+ * exactly what browsing a distro needs. The only thing the shipped browse
27
+ * backend lacks is *discoverability* of WSL — it opens at the Windows home
28
+ * directory. This backend closes that gap:
29
+ *
30
+ * - the root level lists the installed distros (`wsl.exe -l -q`), so WSL is
31
+ * the entry point rather than something you must know a UNC path for;
32
+ * - the breadcrumb "WSL" row jumps back to that distro list;
33
+ * - the dialog's Home affordance targets the preferred distro's Linux `$HOME`;
34
+ * - every other level inside a distro is listed BY THE DISTRO — the resident
35
+ * agent's `ls` when it is up, a one-shot `wsl.exe --exec ls` when it is
36
+ * out — because the host's own listing used to walk the 9p share in its
37
+ * slow direction, one metadata round trip per entry; a Windows path keeps
38
+ * the host's own walk, which is native there.
39
+ *
40
+ * Because the client surface is unchanged, this row composes with the **shipped**
41
+ * `@deepseek-ai/dsh-client-ui-directory-picker-browse` dialog: no client code,
42
+ * no wire change, no asar patch.
43
+ *
44
+ * This is a TypeScript source built to `lib/picker.js`; edit THIS file and run
45
+ * `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
46
+ * fails when it drifts.
47
+ *
48
+ * @module dsh-plugin-wsl/picker
49
+ */
50
+ import z from "@deepseek-ai/schemastery";
51
+ import { DirectoryPicker } from "@deepseek-ai/dsh-host-directory-picker";
52
+ import type { Context } from "@deepseek-ai/cordis";
53
+ import type { DirectoryEntry, DirectoryListing, DirectoryPickerBrowseCapability } from "@deepseek-ai/dsh-host-directory-picker";
54
+ /**
55
+ * The resolved service configuration, as {@link WslDirectoryPicker.Config} yields.
56
+ */
57
+ export interface PickerConfig {
58
+ /** Path to `wsl.exe`; overridable for a non-standard install. */
59
+ wslPath: string;
60
+ /**
61
+ * Distro listed first and used for the Home affordance; empty means WSL's
62
+ * own default distro.
63
+ */
64
+ preferredDistro: string;
65
+ /**
66
+ * Whether the Windows home is also offered at the root level.
67
+ */
68
+ includeHostHome: boolean;
69
+ /**
70
+ * Bound on the directory rows one `list` call may return; the level is
71
+ * flagged truncated beyond it.
72
+ */
73
+ maxEntries: number;
74
+ /** How long a resolved distro list stays fresh. */
75
+ distroCacheMs: number;
76
+ }
77
+ /**
78
+ * Await `operation`, but reject with the signal's reason the moment it aborts.
79
+ * Node's filesystem reads are not retractable, so the operation itself keeps
80
+ * running against a handle the caller then closes; its late settlement is
81
+ * swallowed so an abandoned read cannot surface as an unhandled rejection.
82
+ *
83
+ * @template T
84
+ * @param operation - the in-flight filesystem step.
85
+ * @param signal - caller lifetime; absent means plain awaiting.
86
+ * @returns the operation's value.
87
+ */
88
+ export declare function raceAbort<T>(operation: Promise<T>, signal?: AbortSignal): Promise<T>;
89
+ /**
90
+ * The `ctx.directoryPicker` WSL implementation.
91
+ *
92
+ * The capability object is built once per service lifetime because consumers may
93
+ * capture it across calls.
94
+ */
95
+ export declare class WslDirectoryPicker extends DirectoryPicker {
96
+ static Config: z<Schemastery.ObjectS<NoInfer<{
97
+ /** Path to `wsl.exe`; overridable for a non-standard install. */
98
+ wslPath: z<string, string, "defined">;
99
+ /**
100
+ * Distro listed first and used for the Home affordance. Empty means WSL's
101
+ * own default distro.
102
+ */
103
+ preferredDistro: z<string, string, "defined">;
104
+ /**
105
+ * Also offer the Windows home directory at the root level, so a normal
106
+ * host folder stays reachable without typing a path. The dialog's editable
107
+ * path zone accepts any Windows path either way.
108
+ */
109
+ includeHostHome: z<boolean, boolean, "defined">;
110
+ /** Bound on the directory rows one `list` call may return; the level is flagged truncated. */
111
+ maxEntries: z<number, number, "defined">;
112
+ /** How long a resolved distro list stays fresh. */
113
+ distroCacheMs: z<number, number, "defined">;
114
+ }>>, Schemastery.ObjectT<NoInfer<{
115
+ /** Path to `wsl.exe`; overridable for a non-standard install. */
116
+ wslPath: z<string, string, "defined">;
117
+ /**
118
+ * Distro listed first and used for the Home affordance. Empty means WSL's
119
+ * own default distro.
120
+ */
121
+ preferredDistro: z<string, string, "defined">;
122
+ /**
123
+ * Also offer the Windows home directory at the root level, so a normal
124
+ * host folder stays reachable without typing a path. The dialog's editable
125
+ * path zone accepts any Windows path either way.
126
+ */
127
+ includeHostHome: z<boolean, boolean, "defined">;
128
+ /** Bound on the directory rows one `list` call may return; the level is flagged truncated. */
129
+ maxEntries: z<number, number, "defined">;
130
+ /** How long a resolved distro list stays fresh. */
131
+ distroCacheMs: z<number, number, "defined">;
132
+ }>>, "plain">;
133
+ /** The resolved {@link WslDirectoryPicker.Config}. */
134
+ config: PickerConfig;
135
+ /** Last distro listing, so navigating to the root does not spawn `wsl.exe` every time. */
136
+ distroCache: {
137
+ at: number;
138
+ value: string[];
139
+ } | undefined;
140
+ /** Per-distro Linux home, resolved once (a distro's default user does not change under a running session). */
141
+ homeCache: Map<string, string>;
142
+ /**
143
+ * @param ctx - the owning context.
144
+ * @param config - the resolved {@link WslDirectoryPicker.Config}.
145
+ */
146
+ constructor(ctx: Context, config: PickerConfig);
147
+ /**
148
+ * The browse interaction capability.
149
+ *
150
+ * Deliberately `browse` and not a `wsl` kind: see the module comment. The
151
+ * distro listing is exposed as {@link WslDirectoryPicker.distros} for
152
+ * programmatic callers instead of through the capability union, because a
153
+ * capability kind the wire controller does not recognise would disable the
154
+ * verbs above rather than add a new one.
155
+ *
156
+ * @returns the stable `browse` capability object.
157
+ */
158
+ capability(): DirectoryPickerBrowseCapability;
159
+ browseCapability: DirectoryPickerBrowseCapability;
160
+ /**
161
+ * Installed distros, cached briefly.
162
+ *
163
+ * @param signal - optional cancellation.
164
+ * @returns distro names, default distro first.
165
+ */
166
+ distros(signal?: AbortSignal): Promise<string[]>;
167
+ /**
168
+ * The distro this picker leads with: the configured one when installed, else
169
+ * WSL's default, else the first installed.
170
+ *
171
+ * @param signal - optional cancellation.
172
+ * @returns a distro name, or empty when none is installed.
173
+ */
174
+ preferredDistro(signal?: AbortSignal): Promise<string>;
175
+ /**
176
+ * A distro's Linux home directory as a UNC path, best effort: a distro that
177
+ * cannot be queried (stopped, and unable to start) falls back to its root so
178
+ * the dialog still opens somewhere real.
179
+ *
180
+ * @param distro - distro name.
181
+ * @param signal - optional cancellation.
182
+ * @returns the UNC home path.
183
+ */
184
+ homeOf(distro: string, signal?: AbortSignal): Promise<string>;
185
+ /**
186
+ * The root level: one row per installed distro, plus the Windows home when
187
+ * configured. `path` is the preferred distro's root rather than the share
188
+ * root, so adopting the level with nothing selected still yields a real
189
+ * directory; the breadcrumb's "WSL" row carries the share root and returns
190
+ * here.
191
+ *
192
+ * @param signal - optional cancellation.
193
+ * @returns the root listing.
194
+ */
195
+ rootLevel(signal?: AbortSignal): Promise<DirectoryListing>;
196
+ /**
197
+ * Breadcrumb rows for a level, headed by the WSL row.
198
+ *
199
+ * @param target - the directory being listed.
200
+ * @returns breadcrumb rows.
201
+ */
202
+ crumbsFor(target: string): DirectoryEntry[];
203
+ /**
204
+ * List one directory level.
205
+ *
206
+ * @param path - absolute directory; absent or the share root lists the distros.
207
+ * @param signal - caller lifetime.
208
+ * @returns the level's listing with its ancestry.
209
+ * @throws DirectoryPickerError with code `directory-unreadable`.
210
+ */
211
+ list(path?: string, signal?: AbortSignal): Promise<DirectoryListing>;
212
+ /**
213
+ * List one level inside the distro, where the files live.
214
+ *
215
+ * The resident agent answers in one round trip; when it is out — distro
216
+ * stopped, or the resident's rebuild budget spent — the same `ls` runs
217
+ * through a one-shot `wsl.exe`. Neither path touches the 9p share, which is
218
+ * what the host walk it replaces used: one metadata round trip per entry,
219
+ * in the share's slow direction, plus one more per symlink.
220
+ *
221
+ * @param target - the absolute UNC directory to list.
222
+ * @param signal - caller lifetime.
223
+ * @returns the level's rows and truncation flag.
224
+ * @throws DirectoryPickerError with code `directory-unreadable`.
225
+ */
226
+ listDistroLevel(target: string, signal?: AbortSignal): Promise<{
227
+ rows: DirectoryEntry[];
228
+ truncated: boolean;
229
+ }>;
230
+ /**
231
+ * List one Windows level with the host's own walk — native I/O on a native
232
+ * filesystem, unchanged from the shipped browse behaviour.
233
+ *
234
+ * @param target - the absolute Windows directory to list.
235
+ * @param signal - caller lifetime.
236
+ * @returns the level's rows and truncation flag.
237
+ * @throws DirectoryPickerError with code `directory-unreadable`.
238
+ */
239
+ listHostLevel(target: string, signal?: AbortSignal): Promise<{
240
+ rows: DirectoryEntry[];
241
+ truncated: boolean;
242
+ }>;
243
+ /**
244
+ * Create one child directory inside the listed level.
245
+ *
246
+ * @param path - absolute existing parent directory.
247
+ * @param name - single non-blank path segment.
248
+ * @returns the created directory's absolute path.
249
+ * @throws DirectoryPickerError with code `directory-exists` or `directory-create-failed`.
250
+ */
251
+ createDirectory(path: string, name: string): Promise<string>;
252
+ /**
253
+ * Create one child directory inside the distro — the resident agent's
254
+ * `mkdir` when it is up, a one-shot `wsl.exe --exec mkdir` when it is out;
255
+ * the host's `mkdir` over the 9p share is what this replaces.
256
+ *
257
+ * @param parent - the absolute UNC parent directory.
258
+ * @param name - the validated single path segment to create.
259
+ * @param target - the UNC path the caller receives on success.
260
+ * @returns the created directory's absolute path.
261
+ * @throws DirectoryPickerError with code `directory-exists` or `directory-create-failed`.
262
+ */
263
+ createDistroDirectory(parent: string, name: string, target: string): Promise<string>;
264
+ }
265
+ export default WslDirectoryPicker;
266
+ export { ancestryCrumbs, boundedInsert, breadcrumbs, fullyQualified } from "./listing.js";