dsh-plugin-wsl-env 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/listing.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Pure listing helpers for the WSL directory picker, kept dependency-free so
3
+ * they can be unit-tested without installing the DSH peers the service itself
4
+ * imports. Ported from `@deepseek-ai/dsh-host-directory-picker-browse` so the
5
+ * picker behaves exactly like the shipped browse backend on ordinary paths.
6
+ *
7
+ * @module dsh-plugin-wsl/listing
8
+ */
9
+
10
+ import { win32 } from "node:path";
11
+ import { isWslUnc, uncToPosix } from "./paths.js";
12
+
13
+ /**
14
+ * Whether a path names one fixed filesystem location regardless of process
15
+ * state. On Windows only drive-qualified (`C:\…`) or complete UNC
16
+ * (`\\server\share…`) forms qualify — which is what lets a distro root like
17
+ * `\\wsl.localhost\ubuntu` be adopted as a workspace, while the bare share root
18
+ * `\\wsl.localhost` is correctly rejected as a directory.
19
+ *
20
+ * @param path - candidate path.
21
+ * @returns whether the path is fully qualified.
22
+ */
23
+ export function fullyQualified(path) {
24
+ return win32.isAbsolute(path) && /^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/]+[^\\/]+)/.test(path);
25
+ }
26
+
27
+ /**
28
+ * Ancestor chain from the filesystem root to `target` inclusive — the
29
+ * breadcrumb rows of a listing, every one a jump target. On a UNC path the walk
30
+ * terminates at the share root, whose `dirname` is itself.
31
+ *
32
+ * The `win32` flavour is explicit, not the platform default: these helpers
33
+ * serve Windows paths only, and the bare `node:path` functions are POSIX on
34
+ * Linux, where a UNC path parses as a single relative segment and the chain
35
+ * collapses to `['.', <whole path>]`. Walking up from inside a distro also
36
+ * yields the share root WITH its trailing separator (`\\wsl.localhost\ubuntu\`)
37
+ * — canonicalized by {@link breadcrumbs}, and asserted by the tests.
38
+ *
39
+ * `win32.basename` of a drive root (`C:\`) is empty, so the root crumb falls
40
+ * back to its own path rather than rendering blank.
41
+ *
42
+ * @param target - an absolute Windows path.
43
+ * @returns the ancestry, outermost first.
44
+ */
45
+ export function ancestryCrumbs(target) {
46
+ const crumbs = [];
47
+ let current = target;
48
+ for (;;) {
49
+ const parent = win32.dirname(current);
50
+ // `basename` of a volume root is empty (`C:\`), so the root crumb falls
51
+ // back to its own path rather than rendering blank.
52
+ crumbs.unshift({ name: win32.basename(current) || current, path: current, hidden: false });
53
+ if (parent === current) return crumbs;
54
+ current = parent;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Breadcrumb rows for a level, with the WSL share head prepended for anything
60
+ * inside a distro.
61
+ *
62
+ * Two Windows path quirks make this more than a relabel. `win32.dirname` treats
63
+ * `\\server\share` as a volume root and never yields the share itself, so the
64
+ * WSL row has to be synthesized; and `win32.basename` of that same share root is
65
+ * the empty string, so the distro row is named from the parsed distro instead.
66
+ * Without both fixes, browsing into a distro would render a blank breadcrumb and
67
+ * offer no way back to the distro list.
68
+ *
69
+ * @param target - the directory being listed.
70
+ * @param providerRoot - the share root acting as the WSL row's jump target.
71
+ * @returns breadcrumb rows, outermost first.
72
+ */
73
+ export function breadcrumbs(target, providerRoot) {
74
+ const crumbs = ancestryCrumbs(target);
75
+ const head = crumbs[0];
76
+ if (head === undefined) return [{ name: "WSL", path: providerRoot, hidden: false }];
77
+ const parsed = isWslUnc(head.path) ? uncToPosix(head.path) : undefined;
78
+ if (parsed !== undefined && parsed.linuxPath === "/") {
79
+ // `win32.dirname` reports the share root with a trailing separator
80
+ // (`\\wsl.localhost\ubuntu\`); rewrite the row to the canonical distro root
81
+ // so crumb paths match what distroRoot() and the picker produce elsewhere.
82
+ crumbs[0] = { name: parsed.distro, path: `${providerRoot}\\${parsed.distro}`, hidden: false };
83
+ crumbs.unshift({ name: "WSL", path: providerRoot, hidden: false });
84
+ }
85
+ return crumbs;
86
+ }
87
+
88
+ /**
89
+ * Insert a streamed candidate into the name-sorted bounded window, evicting the
90
+ * name-largest candidate when the window exceeds `keep`. Keeps memory O(keep)
91
+ * regardless of how many children a level holds, so a pathological directory
92
+ * cannot make the picker allocate one row per entry.
93
+ *
94
+ * @param window - the name-ascending window, mutated in place.
95
+ * @param candidate - the streamed candidate to place.
96
+ * @param keep - the window bound.
97
+ * @returns true when an eviction happened (the level has more candidates).
98
+ */
99
+ export function boundedInsert(window, candidate, keep) {
100
+ if (window.length === keep && candidate.name.localeCompare(window[window.length - 1].name) >= 0) return true;
101
+ let lo = 0;
102
+ let hi = window.length;
103
+ while (lo < hi) {
104
+ const mid = (lo + hi) >>> 1;
105
+ if (candidate.name.localeCompare(window[mid].name) < 0) hi = mid;
106
+ else lo = mid + 1;
107
+ }
108
+ window.splice(lo, 0, candidate);
109
+ if (window.length <= keep) return false;
110
+ window.pop();
111
+ return true;
112
+ }
package/lib/paths.js ADDED
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Path translation between the three coordinate systems a WSL-backed session
3
+ * touches:
4
+ *
5
+ * 1. Linux/POSIX paths inside the distro /home/andy/proj/src/a.ts
6
+ * 2. WSL's Windows UNC share \\wsl.localhost\ubuntu\home\andy\proj\src\a.ts
7
+ * 3. The Windows filesystem itself C:\Users\andyz\... -> /mnt/c/Users/andyz/...
8
+ *
9
+ * The harness host can reach (2) directly with Node's fs, which is what makes
10
+ * reads and ripgrep searches work without any in-distro helper. Commands must
11
+ * run inside the distro, so the shell executor speaks (1).
12
+ *
13
+ * These helpers are pure and dependency-free so they can be unit-tested without
14
+ * booting Cordis.
15
+ *
16
+ * @module dsh-plugin-wsl/paths
17
+ */
18
+
19
+ /** The WSL UNC share prefix Node can read. `\\wsl$\<distro>\...` is the legacy spelling. */
20
+ const UNC_RE = /^\\\\wsl(?:\.localhost|\$)\\([^\\]+)(?:\\([\s\S]*))?$/i;
21
+ const DRIVE_RE = /^([A-Za-z]):[\\/]([\s\S]*)$/;
22
+
23
+ /** Normalize Windows separators without touching a POSIX path. */
24
+ function toWindowsSlashes(value) {
25
+ return String(value).replace(/\//g, "\\");
26
+ }
27
+
28
+ /**
29
+ * Whether a string is the WSL UNC share rather than an ordinary Windows path.
30
+ * @param value - candidate path.
31
+ * @returns true for `\\wsl.localhost\...` and `\\wsl$\...`.
32
+ */
33
+ export function isWslUnc(value) {
34
+ return UNC_RE.test(String(value));
35
+ }
36
+
37
+ /**
38
+ * Whether a string is POSIX-absolute. On Windows `path.isAbsolute('/home/x')`
39
+ * is also true, so the WSL mapper must test this before falling back to the
40
+ * host helpers.
41
+ * @param value - candidate path.
42
+ * @returns true when the path starts with a single forward slash.
43
+ */
44
+ export function isPosixAbsolute(value) {
45
+ return /^\/(?!\/)/.test(String(value));
46
+ }
47
+
48
+ /**
49
+ * Whether a path is relative — the only input for which {@link toWorldPath} and
50
+ * {@link toLinuxPath} consult their `cwd` fallback. A provider uses this to skip
51
+ * resolving a default workdir for input that can never need one.
52
+ * @param value - candidate path.
53
+ * @returns true when the path names no absolute location.
54
+ */
55
+ export function isRelativeWorldPath(value) {
56
+ const text = String(value ?? "");
57
+ if (text.length === 0) return false;
58
+ return !isWslUnc(text) && !isPosixAbsolute(text) && !DRIVE_RE.test(text);
59
+ }
60
+
61
+ /** Drop trailing separators, keeping a bare root (`C:\`, `\\wsl.localhost\x\`) meaningful. */
62
+ function trimTrailingSeparators(value) {
63
+ return value.replace(/[\\/]+$/, "") || value;
64
+ }
65
+
66
+ /**
67
+ * A UNC, drive, or (with a distro) Linux path as a comparable Windows key.
68
+ * Windows semantics are case-insensitive and separator-tolerant, so the key is
69
+ * lowercased and backslashed; a Linux path maps onto the distro's UNC share so
70
+ * it can be compared with a target reached through that share.
71
+ * @param value - the path to key.
72
+ * @param distro - the distro whose share a Linux path maps onto, if any.
73
+ * @returns the key, or undefined when the path is not in that vocabulary.
74
+ */
75
+ function toWindowsKey(value, distro) {
76
+ const text = String(value ?? "");
77
+ if (text.length === 0) return undefined;
78
+ if (DRIVE_RE.test(text) || isWslUnc(text)) {
79
+ const key = trimTrailingSeparators(toWindowsSlashes(text)).toLowerCase();
80
+ // A drive root trims to `c:`; without its separator back the prefix test
81
+ // would accept a sibling like `C:\ish` as a child of `C:\`.
82
+ return /^[a-z]:$/.test(key) ? `${key}\\` : key;
83
+ }
84
+ if (distro !== undefined && distro !== "" && isPosixAbsolute(text)) {
85
+ return trimTrailingSeparators(toWindowsSlashes(posixToUnc(distro, text))).toLowerCase();
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ /** A POSIX-absolute path as a comparable key, without its trailing separators. */
91
+ function toPosixKey(value) {
92
+ const text = String(value ?? "");
93
+ if (!isPosixAbsolute(text)) return undefined;
94
+ const trimmed = text.replace(/\/+$/, "");
95
+ return trimmed.length === 0 ? "/" : trimmed;
96
+ }
97
+
98
+ /**
99
+ * Whether a world path is a granted root or lies beneath it, compared in the
100
+ * coordinate system the two spellings share.
101
+ *
102
+ * A grant is written in the vocabulary of the world it governs: the Windows
103
+ * sandbox hands out drive and UNC paths, while a distro-side grant is a Linux
104
+ * path such as `/tmp`. Passing the distro maps a Linux grant onto the same UNC
105
+ * share as the target, so `\\wsl.localhost\ubuntu\home\andy` can be compared
106
+ * with `/home/andy`. Mixed pairs that share no vocabulary report no containment
107
+ * — the conservative answer, never a textual guess.
108
+ *
109
+ * @param target - the path being tested, in any coordinate system.
110
+ * @param root - the granted root, in any coordinate system.
111
+ * @param options - the distro that maps a Linux root onto its UNC share.
112
+ * @returns true when target is root itself or a descendant of it.
113
+ */
114
+ export function isWorldPathUnder(target, root, options = {}) {
115
+ const winTarget = toWindowsKey(target, options.distro);
116
+ const winRoot = toWindowsKey(root, options.distro);
117
+ if (winTarget !== undefined && winRoot !== undefined) {
118
+ // A drive root keeps its separator in the key, so the child boundary is
119
+ // "root already ends with one" rather than "append another".
120
+ const prefix = winRoot.endsWith("\\") ? winRoot : `${winRoot}\\`;
121
+ return winTarget === winRoot || winTarget.startsWith(prefix);
122
+ }
123
+ const posixTarget = toPosixKey(target);
124
+ const posixRoot = toPosixKey(root);
125
+ if (posixTarget !== undefined && posixRoot !== undefined) {
126
+ return posixRoot === "/" || posixTarget === posixRoot || posixTarget.startsWith(`${posixRoot}/`);
127
+ }
128
+ return false;
129
+ }
130
+
131
+ /**
132
+ * Map a Linux path inside a distro onto the UNC path the host can read.
133
+ * @param distro - distro name, e.g. `ubuntu`.
134
+ * @param linuxPath - absolute Linux path, e.g. `/home/andy/proj`.
135
+ * @returns the equivalent `\\wsl.localhost\<distro>\...` path.
136
+ */
137
+ export function posixToUnc(distro, linuxPath) {
138
+ const rest = String(linuxPath).replace(/^\/+/, "").replace(/\//g, "\\");
139
+ return `\\\\wsl.localhost\\${distro}\\${rest}`.replace(/\\+$/, "\\");
140
+ }
141
+
142
+ /**
143
+ * Map a WSL UNC path back onto the distro and Linux path it names.
144
+ * @param uncPath - a `\\wsl.localhost\<distro>\...` path.
145
+ * @returns `{ distro, linuxPath }`, or undefined for any other path.
146
+ */
147
+ export function uncToPosix(uncPath) {
148
+ const match = UNC_RE.exec(String(uncPath));
149
+ if (!match?.[1]) return undefined;
150
+ const rest = (match[2] ?? "").replace(/\\/g, "/");
151
+ return { distro: match[1], linuxPath: rest.length > 0 ? `/${rest}` : "/" };
152
+ }
153
+
154
+ /**
155
+ * Whether a UNC path belongs to the named distro. Used to confine the WSL
156
+ * filesystem backend to one distro instead of silently serving every mounted
157
+ * share.
158
+ * @param uncPath - candidate UNC path.
159
+ * @param distro - the distro this session is pinned to.
160
+ * @returns true when the path is the same distro (case-insensitive).
161
+ */
162
+ export function isUnderDistro(uncPath, distro) {
163
+ const parsed = uncToPosix(uncPath);
164
+ return parsed !== undefined && parsed.distro.toLowerCase() === String(distro).toLowerCase();
165
+ }
166
+
167
+ /**
168
+ * Translate a Windows drive path into its WSL interop mount.
169
+ * @param winPath - e.g. `C:\Users\andyz\Documents`.
170
+ * @returns e.g. `/mnt/c/Users/andyz/Documents`, or undefined for a non-drive path.
171
+ */
172
+ export function windowsToLinuxMount(winPath) {
173
+ const match = DRIVE_RE.exec(String(winPath));
174
+ if (!match?.[1]) return undefined;
175
+ return `/mnt/${match[1].toLowerCase()}/${match[2].replace(/\\/g, "/")}`;
176
+ }
177
+
178
+ /**
179
+ * Resolve a caller-supplied workdir into a path `wsl.exe --cd` accepts.
180
+ *
181
+ * Accepts every coordinate system, because the tool layer passes whatever the
182
+ * session cwd is (a UNC path once the workspace is a WSL folder) while a human
183
+ * editing the profile is likely to write a plain Linux path.
184
+ *
185
+ * @param value - the path to translate.
186
+ * @param options - the pinned distro and the fallback directory for relative input.
187
+ * @returns an absolute Linux path, or the input unchanged when it cannot be mapped.
188
+ */
189
+ export function toLinuxPath(value, options) {
190
+ const text = String(value ?? "");
191
+ if (text.length === 0) return text;
192
+ const parsed = uncToPosix(text);
193
+ if (parsed !== undefined) return parsed.linuxPath;
194
+ if (isPosixAbsolute(text)) return text;
195
+ const mounted = windowsToLinuxMount(text);
196
+ if (mounted !== undefined) return mounted;
197
+ // Relative input is resolved against the configured default workdir.
198
+ const base = options?.cwd ?? "/";
199
+ const baseLinux = toLinuxPath(base, { distro: options?.distro });
200
+ return `${baseLinux.replace(/\/+$/, "")}/${text.replace(/^\.\//, "")}`;
201
+ }
202
+
203
+ /**
204
+ * Resolve a caller-supplied path into the UNC coordinate system the host's fs
205
+ * calls use. Linux paths from the model, UNC paths from the harness, and
206
+ * Windows paths from the host all have to land somewhere real.
207
+ *
208
+ * @param value - the path to translate.
209
+ * @param options - the pinned distro and the fallback directory for relative input.
210
+ * @returns an absolute host path (UNC for anything inside the distro).
211
+ */
212
+ export function toWorldPath(value, options) {
213
+ const text = String(value ?? "");
214
+ if (text.length === 0) return text;
215
+ if (isWslUnc(text)) return toWindowsSlashes(text);
216
+ if (isPosixAbsolute(text)) return posixToUnc(options?.distro ?? "Ubuntu", text);
217
+ if (DRIVE_RE.test(text)) return toWindowsSlashes(text);
218
+ const base = options?.cwd ?? process.cwd();
219
+ const baseWorld = toWorldPath(base, { distro: options?.distro });
220
+ return `${baseWorld.replace(/[\\/]+$/, "")}\\${toWindowsSlashes(text).replace(/^\.\\/, "")}`;
221
+ }
222
+
223
+ /**
224
+ * Render a world path back into the form the model and the UI should see: a
225
+ * Linux path inside the distro, and the ordinary Windows path everywhere else.
226
+ * `FsTarget.targetKey` stays the UNC path (the contract treats it as opaque);
227
+ * only `displayPath` is rewritten.
228
+ *
229
+ * @param worldPath - an absolute host path.
230
+ * @param distro - the distro whose paths should be shown as Linux paths.
231
+ * @returns the display path.
232
+ */
233
+ export function toDisplayPath(worldPath, distro) {
234
+ const parsed = uncToPosix(worldPath);
235
+ if (parsed !== undefined && parsed.distro.toLowerCase() === String(distro).toLowerCase()) return parsed.linuxPath;
236
+ return String(worldPath);
237
+ }
238
+
239
+ /** The provider root of the WSL UNC share — the level that holds one entry per distro. */
240
+ export const UNC_PROVIDER_ROOT = "\\\\wsl.localhost";
241
+
242
+ /**
243
+ * The UNC root of one distro, which is the mount point of that distro's `/`.
244
+ * This is a complete UNC path (`\\server\share`), which is what the directory
245
+ * picker's fully-qualified check requires and what the workspace registry
246
+ * accepts as a project directory.
247
+ *
248
+ * @param distro - distro name.
249
+ * @returns e.g. `\\wsl.localhost\ubuntu`.
250
+ */
251
+ export function distroRoot(distro) {
252
+ return `${UNC_PROVIDER_ROOT}\\${distro}`;
253
+ }
254
+
255
+ /**
256
+ * Whether a path names the WSL share's root rather than a distro inside it.
257
+ * Both share spellings and a trailing separator are accepted.
258
+ *
259
+ * @param value - candidate path.
260
+ * @returns true for `\\wsl.localhost` and `\\wsl$` in any casing.
261
+ */
262
+ export function isProviderRoot(value) {
263
+ const trimmed = String(value).replace(/[\\/]+$/, "").toLowerCase();
264
+ return trimmed === UNC_PROVIDER_ROOT.toLowerCase() || trimmed === "\\\\wsl$";
265
+ }