dsh-plugin-wsl-env 0.7.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +345 -8
- package/CONTRIBUTING.md +221 -68
- package/README.md +50 -35
- package/SECURITY.md +18 -2
- package/SUPPORT.md +23 -7
- package/cordis.patch.yml +9 -0
- package/docs/ARCHITECTURE.md +200 -57
- package/docs/CONFIGURATION.md +180 -16
- package/docs/LIMITATIONS.md +91 -33
- package/docs/PARITY.md +66 -38
- package/docs/PEER-PARITY.md +95 -32
- package/docs/README.zh.md +49 -31
- package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
- package/docs/UPSTREAM-TERMINAL-TITLE.md +49 -14
- package/lib/agent-confined.d.ts +42 -0
- package/lib/agent-confined.js +25 -41
- package/lib/agent-errors.d.ts +31 -0
- package/lib/agent-errors.js +19 -16
- package/lib/agent-exec.d.ts +159 -0
- package/lib/agent-exec.js +151 -165
- package/lib/agent-protocol.d.ts +192 -0
- package/lib/agent-protocol.js +76 -100
- package/lib/agent-shared.d.ts +27 -0
- package/lib/agent-shared.js +12 -12
- package/lib/agent.d.ts +353 -0
- package/lib/agent.js +536 -552
- package/lib/auto-preset.d.ts +110 -0
- package/lib/auto-preset.js +157 -141
- package/lib/bwrap.d.ts +90 -0
- package/lib/bwrap.js +18 -18
- package/lib/file-reference-wsl.d.ts +160 -0
- package/lib/file-reference-wsl.js +192 -173
- package/lib/fs-decisions.d.ts +107 -0
- package/lib/fs-decisions.js +64 -60
- package/lib/fs-routing.d.ts +182 -0
- package/lib/fs-routing.js +173 -116
- package/lib/fs-substrate.d.ts +258 -0
- package/lib/fs-substrate.js +248 -248
- package/lib/fs.d.ts +10 -0
- package/lib/fs.js +5 -0
- package/lib/fsio-agent.d.ts +307 -0
- package/lib/fsio-agent.js +414 -410
- package/lib/fsio-text.d.ts +115 -0
- package/lib/fsio-text.js +104 -84
- package/lib/git-route.d.ts +100 -0
- package/lib/git-route.js +46 -47
- package/lib/index.d.ts +1003 -0
- package/lib/index.js +1106 -1003
- package/lib/listing.d.ts +114 -0
- package/lib/listing.js +68 -55
- package/lib/paths.d.ts +182 -0
- package/lib/paths.js +134 -109
- package/lib/picker.d.ts +266 -0
- package/lib/picker.js +371 -351
- package/lib/ports.d.ts +44 -0
- package/lib/ports.js +39 -29
- package/lib/preflight.d.ts +91 -0
- package/lib/preflight.js +94 -0
- package/lib/preset-choice.d.ts +33 -0
- package/lib/preset-choice.js +15 -7
- package/lib/probe-cache.d.ts +51 -0
- package/lib/probe-cache.js +43 -29
- package/lib/sandbox-core.d.ts +149 -0
- package/lib/sandbox-core.js +239 -87
- package/lib/sandbox.d.ts +48 -0
- package/lib/sandbox.js +4 -4
- package/lib/search-exec.d.ts +96 -0
- package/lib/search-exec.js +52 -79
- package/lib/search-route.d.ts +75 -0
- package/lib/search-route.js +30 -31
- package/lib/shell-env.d.ts +88 -0
- package/lib/shell-env.js +78 -74
- package/lib/shell.d.ts +10 -0
- package/lib/shell.js +5 -0
- package/lib/subprocess.d.ts +313 -0
- package/lib/subprocess.js +267 -262
- package/lib/terminal-activity.d.ts +98 -0
- package/lib/terminal-activity.js +29 -39
- package/lib/terminal-route.d.ts +27 -0
- package/lib/terminal-route.js +16 -8
- package/lib/watcher.d.ts +77 -0
- package/lib/watcher.js +120 -126
- package/lib/workspace-files-route.d.ts +132 -0
- package/lib/workspace-files-route.js +73 -81
- package/lib/workspace-files-wsl.d.ts +257 -0
- package/lib/workspace-files-wsl.js +311 -251
- package/lib/wsl.d.ts +266 -0
- package/lib/wsl.js +150 -155
- package/locale/preflight/en.json +6 -0
- package/locale/preflight/zh.json +6 -0
- package/package.json +78 -22
- package/scripts/diagnose.sh +92 -0
package/lib/picker.js
CHANGED
|
@@ -41,9 +41,12 @@
|
|
|
41
41
|
* `@deepseek-ai/dsh-client-ui-directory-picker-browse` dialog: no client code,
|
|
42
42
|
* no wire change, no asar patch.
|
|
43
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
|
+
*
|
|
44
48
|
* @module dsh-plugin-wsl/picker
|
|
45
49
|
*/
|
|
46
|
-
|
|
47
50
|
import { mkdir, opendir, stat } from "node:fs/promises";
|
|
48
51
|
import { homedir } from "node:os";
|
|
49
52
|
import { join, resolve } from "node:path";
|
|
@@ -54,69 +57,78 @@ import { breadcrumbs, boundedInsert, fullyQualified, lsListingArgv, parseLsListi
|
|
|
54
57
|
import { listDistros, linuxHome, runCapture } from "./wsl.js";
|
|
55
58
|
import { sharedAgent } from "./agent-shared.js";
|
|
56
59
|
import { AgentUnavailableError } from "./agent-errors.js";
|
|
57
|
-
|
|
58
60
|
/** How long a resolved distro list stays fresh before it is queried again. */
|
|
59
61
|
const DEFAULT_DISTRO_CACHE_MS = 5_000;
|
|
60
|
-
|
|
61
62
|
/**
|
|
62
63
|
* Await `operation`, but reject with the signal's reason the moment it aborts.
|
|
63
64
|
* Node's filesystem reads are not retractable, so the operation itself keeps
|
|
64
65
|
* running against a handle the caller then closes; its late settlement is
|
|
65
66
|
* swallowed so an abandoned read cannot surface as an unhandled rejection.
|
|
66
67
|
*
|
|
68
|
+
* @template T
|
|
67
69
|
* @param operation - the in-flight filesystem step.
|
|
68
70
|
* @param signal - caller lifetime; absent means plain awaiting.
|
|
69
71
|
* @returns the operation's value.
|
|
70
72
|
*/
|
|
71
73
|
export function raceAbort(operation, signal) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
);
|
|
93
|
-
});
|
|
74
|
+
if (signal === void 0)
|
|
75
|
+
return operation;
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const onAbort = () => {
|
|
78
|
+
operation.catch(() => { });
|
|
79
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
|
|
80
|
+
};
|
|
81
|
+
if (signal.aborted) {
|
|
82
|
+
onAbort();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
86
|
+
operation.then((value) => {
|
|
87
|
+
signal.removeEventListener("abort", onAbort);
|
|
88
|
+
resolve(value);
|
|
89
|
+
}, (reason) => {
|
|
90
|
+
signal.removeEventListener("abort", onAbort);
|
|
91
|
+
reject(reason instanceof Error ? reason : new Error(String(reason)));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Message text of an unknown thrown value.
|
|
97
|
+
*
|
|
98
|
+
* @param error - the thrown value.
|
|
99
|
+
* @returns its message text.
|
|
100
|
+
*/
|
|
97
101
|
function messageOf(error) {
|
|
98
|
-
|
|
102
|
+
return error instanceof Error ? error.message : String(error);
|
|
99
103
|
}
|
|
100
|
-
|
|
101
104
|
/**
|
|
102
105
|
* One listing row for a dirent, following symlinks to directories; null for
|
|
103
106
|
* non-directories and broken or cyclic links, which cannot be entered.
|
|
107
|
+
*
|
|
108
|
+
* @param parent - the directory being listed.
|
|
109
|
+
* @param name - the dirent's base name.
|
|
110
|
+
* @param isDirectory - whether the dirent reports a directory.
|
|
111
|
+
* @param isSymbolicLink - whether the dirent is a symlink.
|
|
112
|
+
* @param signal - caller lifetime.
|
|
113
|
+
* @returns the row, or null when unenterable.
|
|
104
114
|
*/
|
|
105
115
|
async function directoryRow(parent, name, isDirectory, isSymbolicLink, signal) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
const path = join(parent, name);
|
|
117
|
+
let enterable = isDirectory;
|
|
118
|
+
if (!enterable && isSymbolicLink) {
|
|
119
|
+
try {
|
|
120
|
+
enterable = (await raceAbort(stat(path), signal)).isDirectory();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
if (signal?.aborted)
|
|
124
|
+
throw signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason));
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
114
127
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
if (!enterable)
|
|
129
|
+
return null;
|
|
130
|
+
return { name, path, hidden: name.startsWith(".") };
|
|
118
131
|
}
|
|
119
|
-
|
|
120
132
|
/**
|
|
121
133
|
* The `ctx.directoryPicker` WSL implementation.
|
|
122
134
|
*
|
|
@@ -124,331 +136,339 @@ async function directoryRow(parent, name, isDirectory, isSymbolicLink, signal) {
|
|
|
124
136
|
* capture it across calls.
|
|
125
137
|
*/
|
|
126
138
|
export class WslDirectoryPicker extends DirectoryPicker {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
139
|
+
static Config = z.object({
|
|
140
|
+
/** Path to `wsl.exe`; overridable for a non-standard install. */
|
|
141
|
+
wslPath: z.string().default("wsl.exe"),
|
|
142
|
+
/**
|
|
143
|
+
* Distro listed first and used for the Home affordance. Empty means WSL's
|
|
144
|
+
* own default distro.
|
|
145
|
+
*/
|
|
146
|
+
preferredDistro: z.string().default(""),
|
|
147
|
+
/**
|
|
148
|
+
* Also offer the Windows home directory at the root level, so a normal
|
|
149
|
+
* host folder stays reachable without typing a path. The dialog's editable
|
|
150
|
+
* path zone accepts any Windows path either way.
|
|
151
|
+
*/
|
|
152
|
+
includeHostHome: z.boolean().default(true),
|
|
153
|
+
/** Bound on the directory rows one `list` call may return; the level is flagged truncated. */
|
|
154
|
+
maxEntries: z.natural().min(1).default(1_000),
|
|
155
|
+
/** How long a resolved distro list stays fresh. */
|
|
156
|
+
distroCacheMs: z.natural().default(DEFAULT_DISTRO_CACHE_MS),
|
|
157
|
+
});
|
|
130
158
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
159
|
+
* @param ctx - the owning context.
|
|
160
|
+
* @param config - the resolved {@link WslDirectoryPicker.Config}.
|
|
133
161
|
*/
|
|
134
|
-
|
|
162
|
+
constructor(ctx, config) {
|
|
163
|
+
super(ctx);
|
|
164
|
+
this.config = config;
|
|
165
|
+
this.distroCache = undefined;
|
|
166
|
+
this.homeCache = new Map();
|
|
167
|
+
}
|
|
135
168
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
169
|
+
* The browse interaction capability.
|
|
170
|
+
*
|
|
171
|
+
* Deliberately `browse` and not a `wsl` kind: see the module comment. The
|
|
172
|
+
* distro listing is exposed as {@link WslDirectoryPicker.distros} for
|
|
173
|
+
* programmatic callers instead of through the capability union, because a
|
|
174
|
+
* capability kind the wire controller does not recognise would disable the
|
|
175
|
+
* verbs above rather than add a new one.
|
|
176
|
+
*
|
|
177
|
+
* @returns the stable `browse` capability object.
|
|
139
178
|
*/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
maxEntries: z.natural().min(1).default(1_000),
|
|
143
|
-
/** How long a resolved distro list stays fresh. */
|
|
144
|
-
distroCacheMs: z.natural().default(DEFAULT_DISTRO_CACHE_MS),
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
constructor(ctx, config) {
|
|
148
|
-
super(ctx);
|
|
149
|
-
this.config = config;
|
|
150
|
-
/** Last distro listing, so navigating to the root does not spawn `wsl.exe` every time. */
|
|
151
|
-
this.distroCache = undefined;
|
|
152
|
-
/** Per-distro Linux home, resolved once (a distro's default user does not change under a running session). */
|
|
153
|
-
this.homeCache = new Map();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* The browse interaction capability.
|
|
158
|
-
*
|
|
159
|
-
* Deliberately `browse` and not a `wsl` kind: see the module comment. The
|
|
160
|
-
* distro listing is exposed as {@link WslDirectoryPicker.distros} for
|
|
161
|
-
* programmatic callers instead of through the capability union, because a
|
|
162
|
-
* capability kind the wire controller does not recognise would disable the
|
|
163
|
-
* verbs above rather than add a new one.
|
|
164
|
-
*
|
|
165
|
-
* @returns the stable `browse` capability object.
|
|
166
|
-
*/
|
|
167
|
-
capability() {
|
|
168
|
-
return this.browseCapability;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
browseCapability = {
|
|
172
|
-
kind: "browse",
|
|
173
|
-
list: (path, signal) => this.list(path, signal),
|
|
174
|
-
createDirectory: (path, name) => this.createDirectory(path, name),
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Installed distros, cached briefly.
|
|
179
|
-
* @param signal - optional cancellation.
|
|
180
|
-
* @returns distro names, default distro first.
|
|
181
|
-
*/
|
|
182
|
-
async distros(signal) {
|
|
183
|
-
const ttl = this.config.distroCacheMs;
|
|
184
|
-
const now = Date.now();
|
|
185
|
-
if (this.distroCache !== undefined && now - this.distroCache.at < ttl) return this.distroCache.value;
|
|
186
|
-
const value = await listDistros({ wslPath: this.config.wslPath, signal });
|
|
187
|
-
this.distroCache = { at: now, value };
|
|
188
|
-
return value;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* The distro this picker leads with: the configured one when installed, else
|
|
193
|
-
* WSL's default, else the first installed.
|
|
194
|
-
* @param signal - optional cancellation.
|
|
195
|
-
* @returns a distro name, or empty when none is installed.
|
|
196
|
-
*/
|
|
197
|
-
async preferredDistro(signal) {
|
|
198
|
-
const distros = await this.distros(signal);
|
|
199
|
-
const configured = this.config.preferredDistro;
|
|
200
|
-
if (configured.length > 0 && distros.includes(configured)) return configured;
|
|
201
|
-
return distros[0] ?? "";
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* A distro's Linux home directory as a UNC path, best effort: a distro that
|
|
206
|
-
* cannot be queried (stopped, and unable to start) falls back to its root so
|
|
207
|
-
* the dialog still opens somewhere real.
|
|
208
|
-
*
|
|
209
|
-
* @param distro - distro name.
|
|
210
|
-
* @param signal - optional cancellation.
|
|
211
|
-
* @returns the UNC home path.
|
|
212
|
-
*/
|
|
213
|
-
async homeOf(distro, signal) {
|
|
214
|
-
const cached = this.homeCache.get(distro);
|
|
215
|
-
if (cached !== undefined) return cached;
|
|
216
|
-
let home;
|
|
217
|
-
try {
|
|
218
|
-
home = await linuxHome(distro, { wslPath: this.config.wslPath, signal });
|
|
219
|
-
} catch {
|
|
220
|
-
home = distroRoot(distro);
|
|
221
|
-
}
|
|
222
|
-
this.homeCache.set(distro, home);
|
|
223
|
-
return home;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* The root level: one row per installed distro, plus the Windows home when
|
|
228
|
-
* configured. `path` is the preferred distro's root rather than the share
|
|
229
|
-
* root, so adopting the level with nothing selected still yields a real
|
|
230
|
-
* directory; the breadcrumb's "WSL" row carries the share root and returns
|
|
231
|
-
* here.
|
|
232
|
-
*
|
|
233
|
-
* @param signal - optional cancellation.
|
|
234
|
-
* @returns the root listing.
|
|
235
|
-
*/
|
|
236
|
-
async rootLevel(signal) {
|
|
237
|
-
const distros = await this.distros(signal);
|
|
238
|
-
const preferred = await this.preferredDistro(signal);
|
|
239
|
-
const entries = [];
|
|
240
|
-
if (this.config.includeHostHome) {
|
|
241
|
-
const hostHome = homedir();
|
|
242
|
-
entries.push({ name: hostHome, path: hostHome, hidden: false });
|
|
179
|
+
capability() {
|
|
180
|
+
return this.browseCapability;
|
|
243
181
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
home,
|
|
249
|
-
crumbs: [{ name: "WSL", path: UNC_PROVIDER_ROOT, hidden: false }],
|
|
250
|
-
entries,
|
|
251
|
-
truncated: false,
|
|
182
|
+
browseCapability = {
|
|
183
|
+
kind: "browse",
|
|
184
|
+
list: (path, signal) => this.list(path, signal),
|
|
185
|
+
createDirectory: (path, name) => this.createDirectory(path, name),
|
|
252
186
|
};
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
* @param signal - caller lifetime.
|
|
268
|
-
* @returns the level's listing with its ancestry.
|
|
269
|
-
* @throws DirectoryPickerError with code `directory-unreadable`.
|
|
270
|
-
*/
|
|
271
|
-
async list(path, signal) {
|
|
272
|
-
if (path === void 0 || isProviderRoot(path)) return this.rootLevel(signal);
|
|
273
|
-
if (!fullyQualified(path)) {
|
|
274
|
-
throw new DirectoryPickerError("directory-unreadable", path, `cannot list "${path}": not a fully qualified path`);
|
|
187
|
+
/**
|
|
188
|
+
* Installed distros, cached briefly.
|
|
189
|
+
*
|
|
190
|
+
* @param signal - optional cancellation.
|
|
191
|
+
* @returns distro names, default distro first.
|
|
192
|
+
*/
|
|
193
|
+
async distros(signal) {
|
|
194
|
+
const ttl = this.config.distroCacheMs;
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
if (this.distroCache !== undefined && now - this.distroCache.at < ttl)
|
|
197
|
+
return this.distroCache.value;
|
|
198
|
+
const value = await listDistros({ wslPath: this.config.wslPath, signal });
|
|
199
|
+
this.distroCache = { at: now, value };
|
|
200
|
+
return value;
|
|
275
201
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
});
|
|
313
|
-
if (result.exitCode !== 0) {
|
|
314
|
-
const detail = result.stderr.toString("utf8").trim();
|
|
315
|
-
throw new DirectoryPickerError(
|
|
316
|
-
"directory-unreadable",
|
|
317
|
-
target,
|
|
318
|
-
`cannot list ${target}: ${detail.length > 0 ? detail : `ls exited ${result.exitCode}`}`,
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
stdout = result.stdout;
|
|
322
|
-
} catch (error) {
|
|
323
|
-
if (!(error instanceof AgentUnavailableError)) throw error;
|
|
324
|
-
try {
|
|
325
|
-
stdout = await runCapture(
|
|
326
|
-
[this.config.wslPath, "-d", parsed.distro, "--cd", linuxDir, "--exec", ...lsListingArgv(linuxDir)],
|
|
327
|
-
signal,
|
|
328
|
-
);
|
|
329
|
-
} catch (fallbackError) {
|
|
330
|
-
throw new DirectoryPickerError("directory-unreadable", target, `cannot list ${target}: ${messageOf(fallbackError)}`);
|
|
331
|
-
}
|
|
202
|
+
/**
|
|
203
|
+
* The distro this picker leads with: the configured one when installed, else
|
|
204
|
+
* WSL's default, else the first installed.
|
|
205
|
+
*
|
|
206
|
+
* @param signal - optional cancellation.
|
|
207
|
+
* @returns a distro name, or empty when none is installed.
|
|
208
|
+
*/
|
|
209
|
+
async preferredDistro(signal) {
|
|
210
|
+
const distros = await this.distros(signal);
|
|
211
|
+
const configured = this.config.preferredDistro;
|
|
212
|
+
if (configured.length > 0 && distros.includes(configured))
|
|
213
|
+
return configured;
|
|
214
|
+
return distros[0] ?? "";
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* A distro's Linux home directory as a UNC path, best effort: a distro that
|
|
218
|
+
* cannot be queried (stopped, and unable to start) falls back to its root so
|
|
219
|
+
* the dialog still opens somewhere real.
|
|
220
|
+
*
|
|
221
|
+
* @param distro - distro name.
|
|
222
|
+
* @param signal - optional cancellation.
|
|
223
|
+
* @returns the UNC home path.
|
|
224
|
+
*/
|
|
225
|
+
async homeOf(distro, signal) {
|
|
226
|
+
const cached = this.homeCache.get(distro);
|
|
227
|
+
if (cached !== undefined)
|
|
228
|
+
return cached;
|
|
229
|
+
let home;
|
|
230
|
+
try {
|
|
231
|
+
home = await linuxHome(distro, { wslPath: this.config.wslPath, signal });
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
home = distroRoot(distro);
|
|
235
|
+
}
|
|
236
|
+
this.homeCache.set(distro, home);
|
|
237
|
+
return home;
|
|
332
238
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
const opening = opendir(target);
|
|
352
|
-
level = await raceAbort(opening, signal).catch((error) => {
|
|
353
|
-
opening.then((dir) => dir.close().catch(() => {}), () => {});
|
|
354
|
-
throw error;
|
|
355
|
-
});
|
|
356
|
-
for (;;) {
|
|
357
|
-
const dirent = await raceAbort(level.read(), signal);
|
|
358
|
-
if (dirent === null) break;
|
|
359
|
-
if (!dirent.isDirectory() && !dirent.isSymbolicLink()) continue;
|
|
360
|
-
if (boundedInsert(window, { name: dirent.name, isDirectory: dirent.isDirectory(), isSymbolicLink: dirent.isSymbolicLink() }, keep)) {
|
|
361
|
-
evicted = true;
|
|
239
|
+
/**
|
|
240
|
+
* The root level: one row per installed distro, plus the Windows home when
|
|
241
|
+
* configured. `path` is the preferred distro's root rather than the share
|
|
242
|
+
* root, so adopting the level with nothing selected still yields a real
|
|
243
|
+
* directory; the breadcrumb's "WSL" row carries the share root and returns
|
|
244
|
+
* here.
|
|
245
|
+
*
|
|
246
|
+
* @param signal - optional cancellation.
|
|
247
|
+
* @returns the root listing.
|
|
248
|
+
*/
|
|
249
|
+
async rootLevel(signal) {
|
|
250
|
+
const distros = await this.distros(signal);
|
|
251
|
+
const preferred = await this.preferredDistro(signal);
|
|
252
|
+
const entries = [];
|
|
253
|
+
if (this.config.includeHostHome) {
|
|
254
|
+
const hostHome = homedir();
|
|
255
|
+
entries.push({ name: hostHome, path: hostHome, hidden: false });
|
|
362
256
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
257
|
+
for (const distro of distros)
|
|
258
|
+
entries.push({ name: distro, path: distroRoot(distro), hidden: false });
|
|
259
|
+
const home = preferred.length > 0 ? await this.homeOf(preferred, signal) : homedir();
|
|
260
|
+
return {
|
|
261
|
+
path: preferred.length > 0 ? distroRoot(preferred) : homedir(),
|
|
262
|
+
home,
|
|
263
|
+
crumbs: [{ name: "WSL", path: UNC_PROVIDER_ROOT, hidden: false }],
|
|
264
|
+
entries,
|
|
265
|
+
truncated: false,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Breadcrumb rows for a level, headed by the WSL row.
|
|
270
|
+
*
|
|
271
|
+
* @param target - the directory being listed.
|
|
272
|
+
* @returns breadcrumb rows.
|
|
273
|
+
*/
|
|
274
|
+
crumbsFor(target) {
|
|
275
|
+
return breadcrumbs(target, UNC_PROVIDER_ROOT);
|
|
369
276
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
277
|
+
/**
|
|
278
|
+
* List one directory level.
|
|
279
|
+
*
|
|
280
|
+
* @param path - absolute directory; absent or the share root lists the distros.
|
|
281
|
+
* @param signal - caller lifetime.
|
|
282
|
+
* @returns the level's listing with its ancestry.
|
|
283
|
+
* @throws DirectoryPickerError with code `directory-unreadable`.
|
|
284
|
+
*/
|
|
285
|
+
async list(path, signal) {
|
|
286
|
+
if (path === void 0 || isProviderRoot(path))
|
|
287
|
+
return this.rootLevel(signal);
|
|
288
|
+
if (!fullyQualified(path)) {
|
|
289
|
+
throw new DirectoryPickerError("directory-unreadable", path, `cannot list "${path}": not a fully qualified path`);
|
|
290
|
+
}
|
|
291
|
+
const target = resolve(path);
|
|
292
|
+
const preferred = await this.preferredDistro(signal);
|
|
293
|
+
const level = isWslUnc(target) ? await this.listDistroLevel(target, signal) : await this.listHostLevel(target, signal);
|
|
294
|
+
return {
|
|
295
|
+
path: target,
|
|
296
|
+
home: preferred.length > 0 ? await this.homeOf(preferred, signal) : homedir(),
|
|
297
|
+
crumbs: this.crumbsFor(target),
|
|
298
|
+
entries: level.rows,
|
|
299
|
+
truncated: level.truncated,
|
|
300
|
+
};
|
|
382
301
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
302
|
+
/**
|
|
303
|
+
* List one level inside the distro, where the files live.
|
|
304
|
+
*
|
|
305
|
+
* The resident agent answers in one round trip; when it is out — distro
|
|
306
|
+
* stopped, or the resident's rebuild budget spent — the same `ls` runs
|
|
307
|
+
* through a one-shot `wsl.exe`. Neither path touches the 9p share, which is
|
|
308
|
+
* what the host walk it replaces used: one metadata round trip per entry,
|
|
309
|
+
* in the share's slow direction, plus one more per symlink.
|
|
310
|
+
*
|
|
311
|
+
* @param target - the absolute UNC directory to list.
|
|
312
|
+
* @param signal - caller lifetime.
|
|
313
|
+
* @returns the level's rows and truncation flag.
|
|
314
|
+
* @throws DirectoryPickerError with code `directory-unreadable`.
|
|
315
|
+
*/
|
|
316
|
+
async listDistroLevel(target, signal) {
|
|
317
|
+
// The only caller gates on `isWslUnc`, so the parse cannot miss.
|
|
318
|
+
const parsed = uncToPosix(target);
|
|
319
|
+
const linuxDir = parsed.linuxPath.length > 0 ? parsed.linuxPath : "/";
|
|
320
|
+
let stdout;
|
|
321
|
+
try {
|
|
322
|
+
const result = await sharedAgent(parsed.distro).exec({
|
|
323
|
+
cwd: linuxDir,
|
|
324
|
+
argv: lsListingArgv(linuxDir),
|
|
325
|
+
timeoutMs: 15_000,
|
|
326
|
+
signal,
|
|
327
|
+
});
|
|
328
|
+
if (result.exitCode !== 0) {
|
|
329
|
+
const detail = result.stderr.toString("utf8").trim();
|
|
330
|
+
throw new DirectoryPickerError("directory-unreadable", target, `cannot list ${target}: ${detail.length > 0 ? detail : `ls exited ${result.exitCode}`}`);
|
|
331
|
+
}
|
|
332
|
+
stdout = result.stdout;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (!(error instanceof AgentUnavailableError))
|
|
336
|
+
throw error;
|
|
337
|
+
try {
|
|
338
|
+
stdout = await runCapture([this.config.wslPath, "-d", parsed.distro, "--cd", linuxDir, "--exec", ...lsListingArgv(linuxDir)], signal);
|
|
339
|
+
}
|
|
340
|
+
catch (fallbackError) {
|
|
341
|
+
throw new DirectoryPickerError("directory-unreadable", target, `cannot list ${target}: ${messageOf(fallbackError)}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return parseLsListing(stdout, target, this.config.maxEntries);
|
|
396
345
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
346
|
+
/**
|
|
347
|
+
* List one Windows level with the host's own walk — native I/O on a native
|
|
348
|
+
* filesystem, unchanged from the shipped browse behaviour.
|
|
349
|
+
*
|
|
350
|
+
* @param target - the absolute Windows directory to list.
|
|
351
|
+
* @param signal - caller lifetime.
|
|
352
|
+
* @returns the level's rows and truncation flag.
|
|
353
|
+
* @throws DirectoryPickerError with code `directory-unreadable`.
|
|
354
|
+
*/
|
|
355
|
+
async listHostLevel(target, signal) {
|
|
356
|
+
const maxEntries = this.config.maxEntries;
|
|
357
|
+
const keep = maxEntries + 1;
|
|
358
|
+
const window = [];
|
|
359
|
+
let evicted = false;
|
|
360
|
+
let level;
|
|
361
|
+
try {
|
|
362
|
+
const opening = opendir(target);
|
|
363
|
+
level = await raceAbort(opening, signal).catch((error) => {
|
|
364
|
+
opening.then((dir) => dir.close().catch(() => { }), () => { });
|
|
365
|
+
throw error;
|
|
366
|
+
});
|
|
367
|
+
for (;;) {
|
|
368
|
+
const dirent = await raceAbort(level.read(), signal);
|
|
369
|
+
if (dirent === null)
|
|
370
|
+
break;
|
|
371
|
+
if (!dirent.isDirectory() && !dirent.isSymbolicLink())
|
|
372
|
+
continue;
|
|
373
|
+
if (boundedInsert(window, { name: dirent.name, isDirectory: dirent.isDirectory(), isSymbolicLink: dirent.isSymbolicLink() }, keep)) {
|
|
374
|
+
evicted = true;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
signal?.throwIfAborted();
|
|
380
|
+
throw new DirectoryPickerError("directory-unreadable", target, `cannot list ${target}: ${messageOf(error)}`);
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
if (level !== undefined)
|
|
384
|
+
await level.close().catch(() => { });
|
|
385
|
+
}
|
|
386
|
+
const rows = [];
|
|
387
|
+
let truncated = evicted;
|
|
388
|
+
for (const candidate of window) {
|
|
389
|
+
signal?.throwIfAborted();
|
|
390
|
+
const row = await directoryRow(target, candidate.name, candidate.isDirectory, candidate.isSymbolicLink, signal);
|
|
391
|
+
if (row === null)
|
|
392
|
+
continue;
|
|
393
|
+
if (rows.length === maxEntries) {
|
|
394
|
+
truncated = true;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
rows.push(row);
|
|
398
|
+
}
|
|
399
|
+
return { rows, truncated };
|
|
400
400
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Create one child directory inside the listed level.
|
|
403
|
+
*
|
|
404
|
+
* @param path - absolute existing parent directory.
|
|
405
|
+
* @param name - single non-blank path segment.
|
|
406
|
+
* @returns the created directory's absolute path.
|
|
407
|
+
* @throws DirectoryPickerError with code `directory-exists` or `directory-create-failed`.
|
|
408
|
+
*/
|
|
409
|
+
async createDirectory(path, name) {
|
|
410
|
+
if (!fullyQualified(path)) {
|
|
411
|
+
throw new DirectoryPickerError("directory-create-failed", path, `cannot create under "${path}": not a fully qualified parent path`);
|
|
412
|
+
}
|
|
413
|
+
const parent = resolve(path);
|
|
414
|
+
if (name.trim() === "" || name === "." || name === ".." || /[/\\]/.test(name)) {
|
|
415
|
+
throw new DirectoryPickerError("directory-create-failed", join(parent, name), `"${name}" is not a single path segment`);
|
|
416
|
+
}
|
|
417
|
+
const target = join(parent, name);
|
|
418
|
+
if (isWslUnc(parent))
|
|
419
|
+
return this.createDistroDirectory(parent, name, target);
|
|
420
|
+
try {
|
|
421
|
+
await mkdir(target);
|
|
422
|
+
return target;
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") {
|
|
426
|
+
throw new DirectoryPickerError("directory-exists", target, `${target} already exists`);
|
|
427
|
+
}
|
|
428
|
+
throw new DirectoryPickerError("directory-create-failed", target, `cannot create ${target}: ${messageOf(error)}`);
|
|
429
|
+
}
|
|
411
430
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
431
|
+
/**
|
|
432
|
+
* Create one child directory inside the distro — the resident agent's
|
|
433
|
+
* `mkdir` when it is up, a one-shot `wsl.exe --exec mkdir` when it is out;
|
|
434
|
+
* the host's `mkdir` over the 9p share is what this replaces.
|
|
435
|
+
*
|
|
436
|
+
* @param parent - the absolute UNC parent directory.
|
|
437
|
+
* @param name - the validated single path segment to create.
|
|
438
|
+
* @param target - the UNC path the caller receives on success.
|
|
439
|
+
* @returns the created directory's absolute path.
|
|
440
|
+
* @throws DirectoryPickerError with code `directory-exists` or `directory-create-failed`.
|
|
441
|
+
*/
|
|
442
|
+
async createDistroDirectory(parent, name, target) {
|
|
443
|
+
// The only caller gates on `isWslUnc`, so the parse cannot miss.
|
|
444
|
+
const parsed = uncToPosix(parent);
|
|
445
|
+
const linuxParent = parsed.linuxPath.length > 0 ? parsed.linuxPath : "/";
|
|
446
|
+
try {
|
|
447
|
+
const result = await sharedAgent(parsed.distro).exec({ cwd: linuxParent, argv: ["mkdir", "--", name], timeoutMs: 10_000 });
|
|
448
|
+
if (result.exitCode === 0)
|
|
449
|
+
return target;
|
|
450
|
+
const detail = result.stderr.toString("utf8");
|
|
451
|
+
if (/File exists/i.test(detail))
|
|
452
|
+
throw new DirectoryPickerError("directory-exists", target, `${target} already exists`);
|
|
453
|
+
throw new DirectoryPickerError("directory-create-failed", target, `cannot create ${target}: ${detail.trim().length > 0 ? detail.trim() : `mkdir exited ${result.exitCode}`}`);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
if (error instanceof DirectoryPickerError)
|
|
457
|
+
throw error;
|
|
458
|
+
if (!(error instanceof AgentUnavailableError))
|
|
459
|
+
throw error;
|
|
460
|
+
try {
|
|
461
|
+
await runCapture([this.config.wslPath, "-d", parsed.distro, "--cd", linuxParent, "--exec", "mkdir", "--", name]);
|
|
462
|
+
return target;
|
|
463
|
+
}
|
|
464
|
+
catch (fallbackError) {
|
|
465
|
+
const message = messageOf(fallbackError);
|
|
466
|
+
if (/File exists/i.test(message))
|
|
467
|
+
throw new DirectoryPickerError("directory-exists", target, `${target} already exists`);
|
|
468
|
+
throw new DirectoryPickerError("directory-create-failed", target, `cannot create ${target}: ${message}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
449
471
|
}
|
|
450
|
-
}
|
|
451
472
|
}
|
|
452
|
-
|
|
453
473
|
export default WslDirectoryPicker;
|
|
454
474
|
export { ancestryCrumbs, boundedInsert, breadcrumbs, fullyQualified } from "./listing.js";
|