dsh-plugin-wsl-env 0.7.4 → 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 +298 -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 -997
  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 +136 -105
  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 -70
  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 -151
  79. package/package.json +68 -20
@@ -0,0 +1,160 @@
1
+ /**
2
+ * The distro-side traversal strategy for `@` completion — the plugin's
3
+ * ready answer to RFC addendum ① (docs/upstream/rfc-wsl-workspaces.md).
4
+ *
5
+ * ## The seam this consumes
6
+ *
7
+ * `@deepseek-ai/dsh-file-reference-local` funnels every workspace traversal
8
+ * through three module-internal functions: `readWorkspaceRoot` and
9
+ * `readDirectory` (both `node:fs/promises` readdir with `withFileTypes`) and
10
+ * `resolveDisplayDirectory` (a lexical resolve plus a per-segment `lstat`
11
+ * walk that refuses symlink and non-directory components). Everything else —
12
+ * the fuzzy ranking, the bounded index, the generation/invalidation dance,
13
+ * the excluded-directory list — is pure logic over the entries those three
14
+ * return. Addendum ① proposes exposing the three as an injectable strategy
15
+ * with the current implementations as defaults; this module is the consumer
16
+ * side, built and tested against that interface so the takeover lands the
17
+ * day the seam does.
18
+ *
19
+ * It is deliberately NOT wired into cordis.patch.yml yet: with the seam
20
+ * absent, reaching the funnels would mean subclassing past module-internal
21
+ * functions or re-implementing the ranking engine — the behavior-regression
22
+ * path the RFC rules out. The module ships, tested, until upstream speaks.
23
+ *
24
+ * ## Semantics, held faithful where the seam allows
25
+ *
26
+ * - Entries keep upstream's Dirent shape: `{name, isDirectory(), isFile()}`.
27
+ * The distro listing uses `find -printf '%y\t%f'` rather than `ls -1ALp`
28
+ * precisely for this: `%y` reports `l` for symlinks, which the parse skips
29
+ * the way a readdir Dirent is neither file nor directory. `ls -1ALp` would
30
+ * resolve links through and change which candidates exist.
31
+ * - `resolveDisplayDirectory` refuses a symlink or non-directory component
32
+ * with one agent exec that walks the (lexically normalized) segments —
33
+ * the same verdict upstream reaches with N sequential lstats, and unlike
34
+ * `realpath`, refusing is the point, not resolving.
35
+ * - A directory the traversal cannot read yields `[]` (upstream's readdir
36
+ * catch); an aborted caller propagates the abort instead.
37
+ * - The returned absolutes stay in the root's own coordinate system (UNC for
38
+ * a distro workspace), because upstream joins children with the host
39
+ * `node:path` — a POSIX path from this layer would be mangled by it.
40
+ *
41
+ * This is a TypeScript source built to `lib/file-reference-wsl.js`; edit THIS
42
+ * file and run `pnpm run build` — the artifact under `lib/` is generated, and
43
+ * `pnpm test` fails when it drifts.
44
+ *
45
+ * @module dsh-plugin-wsl/file-reference-wsl
46
+ */
47
+ import type { WslAgent } from "./agent.js";
48
+ /**
49
+ * Upstream's Dirent shape the ranking engine reads: what a `withFileTypes`
50
+ * readdir yields and what the distro listing parses into.
51
+ */
52
+ export interface TraversalDirent {
53
+ /** The child's name. */
54
+ name: string;
55
+ /** The directory verdict. */
56
+ isDirectory: () => boolean;
57
+ /** The regular-file verdict. */
58
+ isFile: () => boolean;
59
+ }
60
+ /**
61
+ * The traversal seam `WorkspaceFileSearch` consumes — the three module-internal
62
+ * funnels of `@deepseek-ai/dsh-file-reference-local`, per RFC addendum ①.
63
+ */
64
+ export interface FileReferenceTraversal {
65
+ /** The root's entries. */
66
+ readWorkspaceRoot: (absolute: string, signal: AbortSignal) => Promise<TraversalDirent[]>;
67
+ /** One directory's entries; an unreadable one yields []. */
68
+ readDirectory: (absolute: string, signal: AbortSignal) => Promise<TraversalDirent[]>;
69
+ /** The resolved display directory, or undefined when refused. */
70
+ resolveDisplayDirectory: (root: string, displayDirectory: string, signal: AbortSignal) => Promise<string | undefined>;
71
+ }
72
+ /**
73
+ * Lexically normalize a `/`-separated display directory against its root.
74
+ * Upstream resolves `resolve(root, displayDirectory)` and refuses a result
75
+ * outside the root, so `a/../b` lands on `b` while `..` past the root is
76
+ * refused outright — the walk below never sees a `..` segment.
77
+ *
78
+ * @param displayDirectory - the query text before the fragment, possibly "".
79
+ * @returns the normalized segments, or undefined when the display directory
80
+ * escapes the root.
81
+ */
82
+ export declare function normalizeDisplaySegments(displayDirectory: string): string[] | undefined;
83
+ /**
84
+ * The distro argv for one directory listing: `find -printf '%y\t%f'` over the
85
+ * contained directory. The containment guard is the same shape the picker and
86
+ * workspace-files use — `realpath` the requested directory, refuse anything
87
+ * the root does not contain — and the `%y` type char is what keeps symlink
88
+ * semantics identical to a readdir Dirent (skipped, not followed).
89
+ *
90
+ * @param linuxRoot - the workspace root, POSIX spelling.
91
+ * @param linuxDir - the directory to list, POSIX spelling.
92
+ * @param maxEntries - the listing cap (head -n bound).
93
+ * @returns the agent exec argv.
94
+ */
95
+ export declare function traversalArgv(linuxRoot: string, linuxDir: string, maxEntries: number): string[];
96
+ /**
97
+ * Parse one `%y\t%f` listing into upstream's Dirent-like entries.
98
+ * `d` is a directory and `f` a file; every other type char — `l` for
99
+ * symlinks, devices, sockets — is skipped, matching a readdir Dirent.
100
+ *
101
+ * @param stdout - the raw listing.
102
+ * @returns the parsed entries.
103
+ */
104
+ export declare function parseTraversalListing(stdout: string | Buffer): Array<{
105
+ name: string;
106
+ isDirectory: () => boolean;
107
+ isFile: () => boolean;
108
+ }>;
109
+ /**
110
+ * The argv for the per-segment walk behind `resolveDisplayDirectory`: each
111
+ * segment must exist, be a directory, and not be a symlink — upstream's
112
+ * lstat verdicts, checked in one exec. Segments ride as argv elements, never
113
+ * interpolated into the script.
114
+ *
115
+ * @param linuxRoot - the workspace root, POSIX spelling.
116
+ * @param segments - the normalized display segments below the root.
117
+ * @returns the agent exec argv.
118
+ */
119
+ export declare function resolveDirectoryArgv(linuxRoot: string, segments: string[]): string[];
120
+ /**
121
+ * The host-default traversal — the node:fs implementations the seam keeps
122
+ * as upstream's defaults, replicated here so the prototype answers host
123
+ * workspaces without the seam and so tests can inject substitutes for it.
124
+ * (Upstream owns the real defaults; this copy exists only until the takeover.)
125
+ *
126
+ * @param deps - the node:fs/promises functions, injectable for tests.
127
+ * @returns the strategy object with the upstream default semantics.
128
+ */
129
+ export declare function hostFileReferenceTraversal(deps: {
130
+ /** The readdir implementation. */
131
+ readdir: typeof import("node:fs/promises").readdir;
132
+ /** The lstat implementation. */
133
+ lstat: typeof import("node:fs/promises").lstat;
134
+ }): FileReferenceTraversal;
135
+ /**
136
+ * The distro-routed traversal strategy. Each method routes by coordinate:
137
+ * a `\\wsl.localhost\<distro>` UNC rides the resident agent (the search
138
+ * root and every absolute upstream passes stay in the UNC form it joined
139
+ * them with); anything else defers to the default traversal. The search
140
+ * root doubles as the containment root for the distro listings' guard, so
141
+ * the seam wires one strategy per `WorkspaceFileSearch` — exactly where
142
+ * `WorkspaceFileSearch(root, config, traversal)` would take it.
143
+ *
144
+ * @param options - the strategy's collaborators.
145
+ * @returns the strategy object the seam's `WorkspaceFileSearch` would take.
146
+ */
147
+ export declare function wslFileReferenceTraversal({ root, runnerFor, defaultTraversal, maxEntries }: {
148
+ /** The search root, the UNC form upstream passes. */
149
+ root: string;
150
+ /**
151
+ * The agent runner for one distro; tests substitute a fake (a real wiring
152
+ * passes sharedAgent).
153
+ */
154
+ runnerFor: (distro: string) => Pick<WslAgent, "exec">;
155
+ /** The host-default strategy. */
156
+ defaultTraversal: FileReferenceTraversal;
157
+ /** The listing cap, upstream's index budget. */
158
+ maxEntries?: number;
159
+ }): FileReferenceTraversal;
160
+ export default wslFileReferenceTraversal;
@@ -38,39 +38,41 @@
38
38
  * a distro workspace), because upstream joins children with the host
39
39
  * `node:path` — a POSIX path from this layer would be mangled by it.
40
40
  *
41
+ * This is a TypeScript source built to `lib/file-reference-wsl.js`; edit THIS
42
+ * file and run `pnpm run build` — the artifact under `lib/` is generated, and
43
+ * `pnpm test` fails when it drifts.
44
+ *
41
45
  * @module dsh-plugin-wsl/file-reference-wsl
42
46
  */
43
-
44
47
  import { isWslUnc, posixToUnc, uncToPosix } from "./paths.js";
45
-
46
48
  /** Upstream's default cap on one workspace index; the per-directory listing
47
49
  * rides the same bound, since one level can't usefully exceed the budget. */
48
50
  const DEFAULT_MAX_ENTRIES = 50000;
49
-
50
51
  /**
51
52
  * Lexically normalize a `/`-separated display directory against its root.
52
53
  * Upstream resolves `resolve(root, displayDirectory)` and refuses a result
53
54
  * outside the root, so `a/../b` lands on `b` while `..` past the root is
54
55
  * refused outright — the walk below never sees a `..` segment.
55
56
  *
56
- * @param {string} displayDirectory - the query text before the fragment, possibly "".
57
- * @returns {string[] | undefined} the normalized segments, or undefined when
58
- * the display directory escapes the root.
57
+ * @param displayDirectory - the query text before the fragment, possibly "".
58
+ * @returns the normalized segments, or undefined when the display directory
59
+ * escapes the root.
59
60
  */
60
61
  export function normalizeDisplaySegments(displayDirectory) {
61
- const segments = [];
62
- for (const segment of String(displayDirectory).split("/")) {
63
- if (segment === "" || segment === ".") continue;
64
- if (segment === "..") {
65
- if (segments.length === 0) return undefined;
66
- segments.pop();
67
- continue;
62
+ const segments = [];
63
+ for (const segment of String(displayDirectory).split("/")) {
64
+ if (segment === "" || segment === ".")
65
+ continue;
66
+ if (segment === "..") {
67
+ if (segments.length === 0)
68
+ return undefined;
69
+ segments.pop();
70
+ continue;
71
+ }
72
+ segments.push(segment);
68
73
  }
69
- segments.push(segment);
70
- }
71
- return segments;
74
+ return segments;
72
75
  }
73
-
74
76
  /**
75
77
  * The distro argv for one directory listing: `find -printf '%y\t%f'` over the
76
78
  * contained directory. The containment guard is the same shape the picker and
@@ -78,127 +80,131 @@ export function normalizeDisplaySegments(displayDirectory) {
78
80
  * the root does not contain — and the `%y` type char is what keeps symlink
79
81
  * semantics identical to a readdir Dirent (skipped, not followed).
80
82
  *
81
- * @param {string} linuxRoot - the workspace root, POSIX spelling.
82
- * @param {string} linuxDir - the directory to list, POSIX spelling.
83
- * @param {number} maxEntries - the listing cap (head -n bound).
84
- * @returns {string[]} the agent exec argv.
83
+ * @param linuxRoot - the workspace root, POSIX spelling.
84
+ * @param linuxDir - the directory to list, POSIX spelling.
85
+ * @param maxEntries - the listing cap (head -n bound).
86
+ * @returns the agent exec argv.
85
87
  */
86
88
  export function traversalArgv(linuxRoot, linuxDir, maxEntries) {
87
- const script = [
88
- "root=$1 dir=$2 max=$3",
89
- 'real=$(realpath -L -- "$dir") || exit 2',
90
- 'case "$real" in "$root") ;; "$root"/*) ;; *) echo "OUTSIDE:$real" >&2; exit 3;; esac',
91
- '[ -d "$real" ] || { echo "NOTDIR:$real" >&2; exit 4; }',
92
- 'find "$real" -mindepth 1 -maxdepth 1 -printf \'%y\\t%f\\n\' | head -n "$max"',
93
- ].join("\n");
94
- return ["sh", "-c", script, "sh", linuxRoot, linuxDir, maxEntries];
89
+ const script = [
90
+ "root=$1 dir=$2 max=$3",
91
+ 'real=$(realpath -L -- "$dir") || exit 2',
92
+ 'case "$real" in "$root") ;; "$root"/*) ;; *) echo "OUTSIDE:$real" >&2; exit 3;; esac',
93
+ '[ -d "$real" ] || { echo "NOTDIR:$real" >&2; exit 4; }',
94
+ 'find "$real" -mindepth 1 -maxdepth 1 -printf \'%y\\t%f\\n\' | head -n "$max"',
95
+ ].join("\n");
96
+ return ["sh", "-c", script, "sh", linuxRoot, linuxDir, String(maxEntries)];
95
97
  }
96
-
97
98
  /**
98
99
  * Parse one `%y\t%f` listing into upstream's Dirent-like entries.
99
100
  * `d` is a directory and `f` a file; every other type char — `l` for
100
101
  * symlinks, devices, sockets — is skipped, matching a readdir Dirent.
101
102
  *
102
- * @param {string} stdout - the raw listing.
103
- * @returns {{name: string, isDirectory: () => boolean, isFile: () => boolean}[]}
103
+ * @param stdout - the raw listing.
104
+ * @returns the parsed entries.
104
105
  */
105
106
  export function parseTraversalListing(stdout) {
106
- const entries = [];
107
- for (const line of String(stdout).split("\n")) {
108
- const tab = line.indexOf("\t");
109
- if (tab <= 0) continue;
110
- const kind = line.slice(0, tab);
111
- const name = line.slice(tab + 1);
112
- if (name.length === 0) continue;
113
- if (kind !== "d" && kind !== "f") continue;
114
- entries.push({
115
- name,
116
- isDirectory: () => kind === "d",
117
- isFile: () => kind === "f",
118
- });
119
- }
120
- return entries;
107
+ const entries = [];
108
+ for (const line of String(stdout).split("\n")) {
109
+ const tab = line.indexOf("\t");
110
+ if (tab <= 0)
111
+ continue;
112
+ const kind = line.slice(0, tab);
113
+ const name = line.slice(tab + 1);
114
+ if (name.length === 0)
115
+ continue;
116
+ if (kind !== "d" && kind !== "f")
117
+ continue;
118
+ entries.push({
119
+ name,
120
+ isDirectory: () => kind === "d",
121
+ isFile: () => kind === "f",
122
+ });
123
+ }
124
+ return entries;
121
125
  }
122
-
123
126
  /**
124
127
  * The argv for the per-segment walk behind `resolveDisplayDirectory`: each
125
128
  * segment must exist, be a directory, and not be a symlink — upstream's
126
129
  * lstat verdicts, checked in one exec. Segments ride as argv elements, never
127
130
  * interpolated into the script.
128
131
  *
129
- * @param {string} linuxRoot - the workspace root, POSIX spelling.
130
- * @param {string[]} segments - the normalized display segments below the root.
131
- * @returns {string[]} the agent exec argv.
132
+ * @param linuxRoot - the workspace root, POSIX spelling.
133
+ * @param segments - the normalized display segments below the root.
134
+ * @returns the agent exec argv.
132
135
  */
133
136
  export function resolveDirectoryArgv(linuxRoot, segments) {
134
- const script = [
135
- "root=$1",
136
- 'cur="$root"',
137
- 'for seg in "${@:2}"; do',
138
- ' cur="$cur/$seg"',
139
- ' if [ -L "$cur" ]; then echo "SYMLINK:$cur" >&2; exit 5; fi',
140
- ' [ -d "$cur" ] || { echo "NOTDIR:$cur" >&2; exit 4; }',
141
- "done",
142
- 'printf \'%s\\n\' "$cur"',
143
- ].join("\n");
144
- return ["sh", "-c", script, "sh", linuxRoot, ...segments];
137
+ const script = [
138
+ "root=$1",
139
+ 'cur="$root"',
140
+ 'for seg in "${@:2}"; do',
141
+ ' cur="$cur/$seg"',
142
+ ' if [ -L "$cur" ]; then echo "SYMLINK:$cur" >&2; exit 5; fi',
143
+ ' [ -d "$cur" ] || { echo "NOTDIR:$cur" >&2; exit 4; }',
144
+ "done",
145
+ 'printf \'%s\\n\' "$cur"',
146
+ ].join("\n");
147
+ return ["sh", "-c", script, "sh", linuxRoot, ...segments];
145
148
  }
146
-
147
149
  /**
148
150
  * The host-default traversal — the node:fs implementations the seam keeps
149
151
  * as upstream's defaults, replicated here so the prototype answers host
150
152
  * workspaces without the seam and so tests can inject substitutes for it.
151
153
  * (Upstream owns the real defaults; this copy exists only until the takeover.)
152
154
  *
153
- * @param {object} deps - the node:fs/promises functions, injectable for tests.
155
+ * @param deps - the node:fs/promises functions, injectable for tests.
154
156
  * @returns the strategy object with the upstream default semantics.
155
157
  */
156
158
  export function hostFileReferenceTraversal(deps) {
157
- const read = async (absolute, signal) => {
158
- signal.throwIfAborted();
159
- try {
160
- const entries = await deps.readdir(absolute, { withFileTypes: true });
161
- signal.throwIfAborted();
162
- return entries;
163
- } catch (error) {
164
- signal.throwIfAborted();
165
- throw error;
166
- }
167
- };
168
- return {
169
- readWorkspaceRoot: read,
170
- readDirectory: async (absolute, signal) => {
171
- signal.throwIfAborted();
172
- try {
173
- const entries = await deps.readdir(absolute, { withFileTypes: true });
174
- signal.throwIfAborted();
175
- return entries;
176
- } catch {
177
- signal.throwIfAborted();
178
- return [];
179
- }
180
- },
181
- resolveDisplayDirectory: async (root, displayDirectory, signal) => {
182
- const segments = normalizeDisplaySegments(displayDirectory);
183
- if (segments === undefined) return undefined;
184
- let current = root;
185
- for (const segment of segments) {
159
+ const read = async (absolute, signal) => {
186
160
  signal.throwIfAborted();
187
- current = `${current}${current.endsWith("/") ? "" : "/"}${segment}`;
188
161
  try {
189
- const status = await deps.lstat(current);
190
- signal.throwIfAborted();
191
- if (status.isSymbolicLink() || !status.isDirectory()) return undefined;
192
- } catch (error) {
193
- signal.throwIfAborted();
194
- return undefined;
162
+ const entries = await deps.readdir(absolute, { withFileTypes: true });
163
+ signal.throwIfAborted();
164
+ return entries;
195
165
  }
196
- }
197
- return current;
198
- },
199
- };
166
+ catch (error) {
167
+ signal.throwIfAborted();
168
+ throw error;
169
+ }
170
+ };
171
+ return {
172
+ readWorkspaceRoot: read,
173
+ readDirectory: async (absolute, signal) => {
174
+ signal.throwIfAborted();
175
+ try {
176
+ const entries = await deps.readdir(absolute, { withFileTypes: true });
177
+ signal.throwIfAborted();
178
+ return entries;
179
+ }
180
+ catch {
181
+ signal.throwIfAborted();
182
+ return [];
183
+ }
184
+ },
185
+ resolveDisplayDirectory: async (root, displayDirectory, signal) => {
186
+ const segments = normalizeDisplaySegments(displayDirectory);
187
+ if (segments === undefined)
188
+ return undefined;
189
+ let current = root;
190
+ for (const segment of segments) {
191
+ signal.throwIfAborted();
192
+ current = `${current}${current.endsWith("/") ? "" : "/"}${segment}`;
193
+ try {
194
+ const status = await deps.lstat(current);
195
+ signal.throwIfAborted();
196
+ if (status.isSymbolicLink() || !status.isDirectory())
197
+ return undefined;
198
+ }
199
+ catch {
200
+ signal.throwIfAborted();
201
+ return undefined;
202
+ }
203
+ }
204
+ return current;
205
+ },
206
+ };
200
207
  }
201
-
202
208
  /**
203
209
  * The distro-routed traversal strategy. Each method routes by coordinate:
204
210
  * a `\\wsl.localhost\<distro>` UNC rides the resident agent (the search
@@ -208,77 +214,90 @@ export function hostFileReferenceTraversal(deps) {
208
214
  * the seam wires one strategy per `WorkspaceFileSearch` — exactly where
209
215
  * `WorkspaceFileSearch(root, config, traversal)` would take it.
210
216
  *
211
- * @param {object} options
212
- * @param {string} options.root - the search root, the UNC form upstream passes.
213
- * @param {(distro: string) => object} options.runnerFor - the agent runner
214
- * for one distro; tests substitute a fake (a real wiring passes sharedAgent).
215
- * @param {object} options.defaultTraversal - the host-default strategy.
216
- * @param {number} [options.maxEntries] - the listing cap, upstream's index budget.
217
+ * @param options - the strategy's collaborators.
217
218
  * @returns the strategy object the seam's `WorkspaceFileSearch` would take.
218
219
  */
219
220
  export function wslFileReferenceTraversal({ root, runnerFor, defaultTraversal, maxEntries = DEFAULT_MAX_ENTRIES }) {
220
- if (!isWslUnc(root)) return defaultTraversal;
221
- const rootCoords = uncToPosix(root);
222
- const rootLinux = rootCoords.linuxPath;
223
-
224
- const listDistro = async (absolute, signal, onRefusal) => {
225
- signal.throwIfAborted();
226
- const { linuxPath } = uncToPosix(absolute);
227
- try {
228
- const result = await runnerFor(rootCoords.distro).exec({
229
- cwd: "/",
230
- argv: traversalArgv(rootLinux, linuxPath, maxEntries),
231
- maxOutputBytes: 4_000_000,
232
- timeoutMs: 30_000,
233
- signal,
234
- });
235
- signal.throwIfAborted();
236
- if (result.exitCode !== 0) {
237
- return onRefusal(new Error(`listing "${linuxPath}" failed: ${String(result.stderr).trim() || `exit ${result.exitCode}`}`));
238
- }
239
- return parseTraversalListing(result.stdout);
240
- } catch (error) {
241
- signal.throwIfAborted();
242
- return onRefusal(error);
243
- }
244
- };
245
-
246
- return {
247
- readWorkspaceRoot(absolute, signal) {
248
- if (!isWslUnc(absolute)) return defaultTraversal.readWorkspaceRoot(absolute, signal);
249
- return listDistro(absolute, signal, (error) => {
250
- throw error;
251
- });
252
- },
253
-
254
- readDirectory(absolute, signal) {
255
- if (!isWslUnc(absolute)) return defaultTraversal.readDirectory(absolute, signal);
256
- return listDistro(absolute, signal, () => []);
257
- },
258
-
259
- async resolveDisplayDirectory(rootOf, displayDirectory, signal) {
260
- signal.throwIfAborted();
261
- if (!isWslUnc(rootOf)) return defaultTraversal.resolveDisplayDirectory(rootOf, displayDirectory, signal);
262
- const segments = normalizeDisplaySegments(displayDirectory);
263
- if (segments === undefined) return undefined;
264
- if (segments.length === 0) return root;
265
- try {
266
- const result = await runnerFor(rootCoords.distro).exec({
267
- cwd: "/",
268
- argv: resolveDirectoryArgv(rootLinux, segments),
269
- maxOutputBytes: 4_000_000,
270
- timeoutMs: 30_000,
271
- signal,
272
- });
273
- signal.throwIfAborted();
274
- if (result.exitCode !== 0) return undefined;
275
- return posixToUnc(rootCoords.distro, result.stdout.trim());
276
- } catch {
221
+ if (!isWslUnc(root))
222
+ return defaultTraversal;
223
+ const rootCoords = uncToPosix(root);
224
+ if (!rootCoords)
225
+ return defaultTraversal;
226
+ const rootLinux = rootCoords.linuxPath;
227
+ /**
228
+ * List one distro directory through the resident agent's guarded `find`.
229
+ *
230
+ * @param absolute - the UNC directory to list.
231
+ * @param signal - the caller's cancellation.
232
+ * @param onRefusal - the refusal policy: rethrow the error, or degrade to
233
+ * an empty listing.
234
+ * @returns the parsed entries.
235
+ */
236
+ const listDistro = async (absolute, signal, onRefusal) => {
277
237
  signal.throwIfAborted();
278
- return undefined;
279
- }
280
- },
281
- };
238
+ const coords = uncToPosix(absolute);
239
+ if (!coords)
240
+ return onRefusal(new Error(`not a distro path: ${absolute}`));
241
+ const { linuxPath } = coords;
242
+ try {
243
+ const result = await runnerFor(rootCoords.distro).exec({
244
+ cwd: "/",
245
+ argv: traversalArgv(rootLinux, linuxPath, maxEntries),
246
+ maxOutputBytes: 4_000_000,
247
+ timeoutMs: 30_000,
248
+ signal,
249
+ });
250
+ signal.throwIfAborted();
251
+ if (result.exitCode !== 0) {
252
+ return onRefusal(new Error(`listing "${linuxPath}" failed: ${String(result.stderr).trim() || `exit ${result.exitCode}`}`));
253
+ }
254
+ return parseTraversalListing(result.stdout);
255
+ }
256
+ catch (error) {
257
+ signal.throwIfAborted();
258
+ return onRefusal(error);
259
+ }
260
+ };
261
+ return {
262
+ readWorkspaceRoot(absolute, signal) {
263
+ if (!isWslUnc(absolute))
264
+ return defaultTraversal.readWorkspaceRoot(absolute, signal);
265
+ return listDistro(absolute, signal, (error) => {
266
+ throw error;
267
+ });
268
+ },
269
+ readDirectory(absolute, signal) {
270
+ if (!isWslUnc(absolute))
271
+ return defaultTraversal.readDirectory(absolute, signal);
272
+ return listDistro(absolute, signal, () => []);
273
+ },
274
+ async resolveDisplayDirectory(rootOf, displayDirectory, signal) {
275
+ signal.throwIfAborted();
276
+ if (!isWslUnc(rootOf))
277
+ return defaultTraversal.resolveDisplayDirectory(rootOf, displayDirectory, signal);
278
+ const segments = normalizeDisplaySegments(displayDirectory);
279
+ if (segments === undefined)
280
+ return undefined;
281
+ if (segments.length === 0)
282
+ return root;
283
+ try {
284
+ const result = await runnerFor(rootCoords.distro).exec({
285
+ cwd: "/",
286
+ argv: resolveDirectoryArgv(rootLinux, segments),
287
+ maxOutputBytes: 4_000_000,
288
+ timeoutMs: 30_000,
289
+ signal,
290
+ });
291
+ signal.throwIfAborted();
292
+ if (result.exitCode !== 0)
293
+ return undefined;
294
+ return posixToUnc(rootCoords.distro, String(result.stdout).trim());
295
+ }
296
+ catch {
297
+ signal.throwIfAborted();
298
+ return undefined;
299
+ }
300
+ },
301
+ };
282
302
  }
283
-
284
303
  export default wslFileReferenceTraversal;