dsh-plugin-wsl-env 0.11.0 → 0.13.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 (45) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/CONTRIBUTING.md +29 -7
  3. package/README.md +124 -138
  4. package/SECURITY.md +11 -1
  5. package/SUPPORT.md +29 -2
  6. package/cordis.patch.yml +17 -19
  7. package/docs/ARCHITECTURE.md +30 -29
  8. package/docs/CONFIGURATION.md +81 -21
  9. package/docs/LIMITATIONS.md +71 -6
  10. package/docs/MIGRATION.md +98 -0
  11. package/docs/PARITY.md +1 -1
  12. package/docs/PERFORMANCE.md +60 -0
  13. package/docs/QUICKSTART.md +125 -0
  14. package/docs/README.zh.md +117 -137
  15. package/docs/ROADMAP-1.0.md +780 -0
  16. package/docs/ROADMAP.md +4 -0
  17. package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +80 -0
  18. package/docs/UPSTREAM-LSP-SEAM.md +90 -0
  19. package/docs/UPSTREAM-SESSION-NOTICE.md +95 -0
  20. package/lib/agent-errors.d.ts +19 -0
  21. package/lib/agent-errors.js +22 -0
  22. package/lib/agent-exec.js +2 -2
  23. package/lib/agent.d.ts +68 -5
  24. package/lib/agent.js +129 -22
  25. package/lib/distro-route.d.ts +75 -0
  26. package/lib/distro-route.js +86 -0
  27. package/lib/fs-routing.d.ts +4 -1
  28. package/lib/index.d.ts +166 -37
  29. package/lib/index.js +353 -75
  30. package/lib/paths.d.ts +15 -0
  31. package/lib/paths.js +28 -0
  32. package/lib/preflight.d.ts +46 -3
  33. package/lib/preflight.js +83 -14
  34. package/lib/shell-env.d.ts +9 -3
  35. package/lib/shell-env.js +121 -43
  36. package/lib/subprocess.d.ts +12 -3
  37. package/lib/subprocess.js +21 -3
  38. package/package.json +23 -15
  39. package/scripts/diagnose.sh +9 -88
  40. package/scripts/doctor.mjs +548 -0
  41. package/scripts/profile-preset.mjs +254 -0
  42. package/lib/workspace-files-route.d.ts +0 -136
  43. package/lib/workspace-files-route.js +0 -225
  44. package/lib/workspace-files-wsl.d.ts +0 -257
  45. package/lib/workspace-files-wsl.js +0 -418
package/lib/agent.js CHANGED
@@ -10,11 +10,13 @@
10
10
  * command or a write that was in flight has UNKNOWN state — replaying it
11
11
  * would run non-idempotent work twice or report a create that actually
12
12
  * succeeded as a refusal — so it fails instead, and the caller re-reads
13
- * before retrying. A second failure marks the agent permanently dead and
14
- * every future call rejects with {@link AgentUnavailableError} — the
15
- * executor's cue to fall back to the one-shot `wsl.exe` path it shipped
16
- * with, so the worst case of the agent is "today's behaviour, plus one
17
- * failed round trip".
13
+ * before retrying. A failure that spends the rebuild leaves the agent
14
+ * `dead` with a bounded revival: the next call after a 2/4/8-second backoff
15
+ * gets a fresh transport and its own single rebuild; after three deaths the
16
+ * state is permanent and every call rejects with a restart remedy. While
17
+ * dead, calls reject with {@link AgentUnavailableError} — the executor's cue
18
+ * to fall back to the one-shot `wsl.exe` path it shipped with, so the worst
19
+ * case of the agent is "today's behaviour, plus one failed round trip".
18
20
  * 2. **The in-distro timeout is the real one.** The EXEC frame carries the
19
21
  * timeout in whole seconds and the agent SIGTERMs the child itself, so a
20
22
  * timed-out command dies where it lives instead of leaving an orphan
@@ -38,7 +40,7 @@
38
40
  * @module dsh-plugin-wsl/agent
39
41
  */
40
42
  import { spawn } from "node:child_process";
41
- import { AgentUnavailableError, CwdError } from "./agent-errors.js";
43
+ import { AgentUnavailableError, BASH_TIMEOUT_REASON, CwdError, SpawnError } from "./agent-errors.js";
42
44
  import { AGENT_NAME, MAX_FRAME_BYTES, PROTOCOL_VERSION, encodeExecFrame, encodeFsFrame, encodeKill, encodeSetEnv, parseAgentLine } from "./agent-protocol.js";
43
45
  /**
44
46
  * A zeroed per-kind metrics record.
@@ -58,6 +60,19 @@ function emptyAgentMetrics() {
58
60
  const HANDSHAKE_TIMEOUT_MS = 15000;
59
61
  /** Extra time the host watchdog waits past the request's own in-distro timeout. */
60
62
  const WATCHDOG_GRACE_MS = 13000;
63
+ /**
64
+ * How many times a resident that reached `dead` may be revived before the
65
+ * failure becomes permanent. Each revival gets the one permitted rebuild.
66
+ */
67
+ const MAX_REVIVALS = 3;
68
+ /** The first revival delay; doubles per death, capped at 30 s. */
69
+ const DEFAULT_REVIVAL_BACKOFF_MS = 2000;
70
+ /**
71
+ * How long a cancel waits before the out-of-band USR1. Long enough for the
72
+ * agent's own timeout kill to answer, short enough that a killed command still
73
+ * dies promptly.
74
+ */
75
+ const CANCEL_SIGNAL_GRACE_MS = 60;
61
76
  /**
62
77
  * How many requests one resident may hold in flight before refusing more. The
63
78
  * agent is serial, so a large queue only delays every entry; the cap keeps a
@@ -171,7 +186,11 @@ export class WslAgent {
171
186
  this.startSettler = null;
172
187
  this.nextRequestId = 0;
173
188
  this.pending = new Map();
189
+ this.lastAckedSeq = 0;
174
190
  this.rebuildUsed = false;
191
+ this.deaths = 0;
192
+ this.deadUntil = 0;
193
+ this.revivalBackoffMs = config.revivalBackoffMs ?? DEFAULT_REVIVAL_BACKOFF_MS;
175
194
  this.idleTimer = null;
176
195
  this.lastError = "";
177
196
  this.requestMetrics = emptyAgentMetrics();
@@ -189,6 +208,42 @@ export class WslAgent {
189
208
  get unavailableReason() {
190
209
  return this.lastError;
191
210
  }
211
+ /**
212
+ * Enter the dead state and schedule the next bounded revival attempt.
213
+ *
214
+ * The delay doubles per death (2 s, 4 s, 8 s, … capped at 30 s). After
215
+ * {@link MAX_REVIVALS} deaths the state stays permanent and the refusal
216
+ * carries the restart remedy.
217
+ */
218
+ markDead() {
219
+ this.deaths += 1;
220
+ this.state = "dead";
221
+ const delay = Math.min(this.revivalBackoffMs * 2 ** Math.max(0, this.deaths - 1), 30_000);
222
+ this.deadUntil = this.deaths > MAX_REVIVALS ? 0 : Date.now() + delay;
223
+ if (this.deaths > MAX_REVIVALS) {
224
+ this.lastError = `${this.lastError}; permanently out after ${this.deaths} deaths — restart the DSH app`;
225
+ }
226
+ else {
227
+ this.lastError = `${this.lastError}; reviving in ${Math.round(delay / 1000)}s (attempt ${this.deaths}/${MAX_REVIVALS})`;
228
+ }
229
+ }
230
+ /**
231
+ * A read-only lifecycle snapshot for diagnostics. Includes the live pid, the
232
+ * last failure and the remaining revival budget; it never starts or stops
233
+ * anything.
234
+ *
235
+ * @returns the status snapshot.
236
+ */
237
+ status() {
238
+ return {
239
+ distro: this.distro,
240
+ state: this.state,
241
+ pid: this.agentPid,
242
+ lastError: this.lastError,
243
+ deaths: this.deaths,
244
+ pending: this.pending.size,
245
+ };
246
+ }
192
247
  /**
193
248
  * Start the process and wait for the handshake. Re-entrant: concurrent
194
249
  * callers share the one start.
@@ -197,7 +252,19 @@ export class WslAgent {
197
252
  */
198
253
  start() {
199
254
  if (this.state === "dead") {
200
- return Promise.reject(new AgentUnavailableError(this.lastError));
255
+ if (this.deaths > MAX_REVIVALS) {
256
+ return Promise.reject(new AgentUnavailableError(`agent is permanently out after ${this.deaths} deaths (${this.lastError}); restart the DSH app, and delete ~/.cache/dsh-wsl/agent inside the distro if a stale binary is suspected`));
257
+ }
258
+ const wait = this.deadUntil - Date.now();
259
+ if (wait > 0) {
260
+ return Promise.reject(new AgentUnavailableError(`agent is down (${this.lastError}); reviving in ${Math.ceil(wait / 1000)}s, attempt ${this.deaths}/${MAX_REVIVALS}`));
261
+ }
262
+ // Bounded revival: the next call gets a fresh transport and its own
263
+ // single rebuild. Frames from the dead generation were settled in
264
+ // handleProcessExit; nothing is replayed here.
265
+ this.state = "idle";
266
+ this.rebuildUsed = false;
267
+ this.deadUntil = 0;
201
268
  }
202
269
  if (this.state === "ready")
203
270
  return Promise.resolve();
@@ -381,9 +448,10 @@ export class WslAgent {
381
448
  const pending = this.pending.get(message.id);
382
449
  if (pending) {
383
450
  pending.started = true;
451
+ this.lastAckedSeq = pending.seq;
384
452
  this.recordAck(pending);
385
453
  if (pending.cancelled)
386
- this.signalAgent("SIGUSR1");
454
+ this.armCancelSignal(pending);
387
455
  pending.armWatchdog?.();
388
456
  }
389
457
  break;
@@ -392,6 +460,7 @@ export class WslAgent {
392
460
  const pending = this.pending.get(message.id);
393
461
  if (!pending)
394
462
  break; // answered after its caller gave up — noise
463
+ pending.answered = true;
395
464
  this.clearPending(pending);
396
465
  this.pending.delete(message.id);
397
466
  this.rearmIdleTimer();
@@ -406,6 +475,7 @@ export class WslAgent {
406
475
  const pending = this.pending.get(message.id);
407
476
  if (!pending)
408
477
  break;
478
+ pending.answered = true;
409
479
  this.clearPending(pending);
410
480
  this.pending.delete(message.id);
411
481
  this.rearmIdleTimer();
@@ -414,10 +484,16 @@ export class WslAgent {
414
484
  pending.settled = true;
415
485
  // A cwd rejection is the command's own failure, not the agent's — it
416
486
  // becomes a CwdError so the execution layer can report it exactly like
417
- // the one-shot path's relay text.
487
+ // the one-shot path's relay text. A `spawn` rejection is the same
488
+ // class of failure on the native agent (it cannot distinguish a
489
+ // missing workdir), so it gets its own class and the executor retries
490
+ // it one-shot.
418
491
  if (message.reason === "cwd") {
419
492
  pending.reject(new CwdError(message.message));
420
493
  }
494
+ else if (message.reason === "spawn") {
495
+ pending.reject(new SpawnError(message.message));
496
+ }
421
497
  else {
422
498
  pending.reject(new AgentUnavailableError(`agent rejected request ${message.id} (${message.reason}): ${message.message}`));
423
499
  }
@@ -511,12 +587,12 @@ export class WslAgent {
511
587
  pending.settled = true;
512
588
  pending.reject(error);
513
589
  }
514
- this.state = "dead";
590
+ this.markDead();
515
591
  });
516
592
  return;
517
593
  }
518
- this.lastError = `agent exited again (code=${code} signal=${signalName ?? "none"}); falling back permanently`;
519
- this.state = "dead";
594
+ this.lastError = `agent exited again (code=${code} signal=${signalName ?? "none"})`;
595
+ this.markDead();
520
596
  for (const pending of this.pending.values()) {
521
597
  this.clearPending(pending);
522
598
  if (pending.settled)
@@ -544,6 +620,7 @@ export class WslAgent {
544
620
  }
545
621
  const pending = {
546
622
  id,
623
+ seq: Number(id.slice(1)),
547
624
  kind,
548
625
  sentAt: performance.now(),
549
626
  measured: false,
@@ -562,6 +639,8 @@ export class WslAgent {
562
639
  started: false,
563
640
  cancelled: false,
564
641
  settled: false,
642
+ answered: false,
643
+ signalTimer: null,
565
644
  armWatchdog: timeoutMs > 0
566
645
  ? () => {
567
646
  if (pending.watchdog)
@@ -598,8 +677,6 @@ export class WslAgent {
598
677
  // confined resident, a build without the USR1 trap); the signal is what
599
678
  // interrupts the command the agent is blocked on right now.
600
679
  this.writeLines([encodeKill(pending.id)]);
601
- if (pending.started)
602
- this.signalAgent("SIGUSR1");
603
680
  if (pending.settled)
604
681
  return;
605
682
  pending.settled = true;
@@ -608,6 +685,15 @@ export class WslAgent {
608
685
  // cleans the map.
609
686
  this.clearPending(pending);
610
687
  pending.reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
688
+ // Arm AFTER clearPending: the clear releases the per-request timers,
689
+ // and the signal is the one timer that must outlive this rejection.
690
+ // A host timeout is excluded: the distro-side watcher already kills
691
+ // that command, and a USR1 delivered after it settled would hit the
692
+ // request that took its place (observed as an exit-138 "completed").
693
+ const reason = pending.signal?.reason;
694
+ const timedOut = reason instanceof Error && reason.message === BASH_TIMEOUT_REASON;
695
+ if (pending.started && !timedOut)
696
+ this.armCancelSignal(pending);
611
697
  };
612
698
  signal.addEventListener("abort", pending.onAbort, { once: true });
613
699
  }
@@ -632,9 +718,6 @@ export class WslAgent {
632
718
  * @throws {AgentUnavailableError} when the agent is out for this process.
633
719
  */
634
720
  async exec({ cwd, argv, timeoutMs = 0, maxOutputBytes = 0, env, signal }) {
635
- if (this.state === "dead") {
636
- throw new AgentUnavailableError(this.lastError);
637
- }
638
721
  await this.start();
639
722
  const id = `r${this.nextRequestId++}`;
640
723
  const lines = [
@@ -657,9 +740,6 @@ export class WslAgent {
657
740
  * @throws {AgentUnavailableError} when the agent is out for this process.
658
741
  */
659
742
  async fs({ op, args, timeoutMs = 0, signal }) {
660
- if (this.state === "dead") {
661
- throw new AgentUnavailableError(this.lastError);
662
- }
663
743
  await this.start();
664
744
  const id = `r${this.nextRequestId++}`;
665
745
  const lines = encodeFsFrame({ id, op, args, timeoutMs });
@@ -674,8 +754,6 @@ export class WslAgent {
674
754
  * @returns resolved on PONG.
675
755
  */
676
756
  async ping(signal) {
677
- if (this.state === "dead")
678
- throw new AgentUnavailableError(this.lastError);
679
757
  await this.start();
680
758
  if (signal?.aborted)
681
759
  throw signal.reason instanceof Error ? signal.reason : new Error("aborted");
@@ -899,9 +977,38 @@ export class WslAgent {
899
977
  clearPending(pending) {
900
978
  if (pending.watchdog)
901
979
  clearTimeout(pending.watchdog);
980
+ if (pending.signalTimer)
981
+ clearTimeout(pending.signalTimer);
982
+ pending.signalTimer = null;
902
983
  if (pending.signal && pending.onAbort)
903
984
  pending.signal.removeEventListener("abort", pending.onAbort);
904
985
  }
986
+ /**
987
+ * Deliver the out-of-band cancel signal after a short grace, and only while
988
+ * the request it was meant for is still the agent's current one.
989
+ *
990
+ * The signal has no request id: it interrupts whatever command the agent is
991
+ * blocked in. A timeout request is usually already being killed by the
992
+ * in-distro watcher, so an immediate signal can land after the agent moved
993
+ * on — the next request then dies with exit 138 (SIGUSR1) and looks
994
+ * `completed`. A newer ACK means the agent has answered the old request, so
995
+ * the signal is dropped; an answered request is dropped too.
996
+ *
997
+ * @param pending - the cancelled request.
998
+ */
999
+ armCancelSignal(pending) {
1000
+ if (pending.signalTimer !== null)
1001
+ return;
1002
+ pending.signalTimer = setTimeout(() => {
1003
+ pending.signalTimer = null;
1004
+ if (pending.answered || this.pending.get(pending.id) !== pending)
1005
+ return;
1006
+ if (this.lastAckedSeq > pending.seq)
1007
+ return;
1008
+ this.signalAgent("SIGUSR1");
1009
+ }, CANCEL_SIGNAL_GRACE_MS);
1010
+ pending.signalTimer.unref?.();
1011
+ }
905
1012
  }
906
1013
  /** The read-only FS ops: replaying them across a rebuild changes nothing. */
907
1014
  const REPLAYABLE_FS_OPS = new Set(["stat", "lstat", "list", "realpath", "read"]);
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Which distro an operation belongs to.
3
+ *
4
+ * The pinned name has three spellings: an explicit distro wins, `"default"`
5
+ * forces WSL's own default, and the empty string or `"auto"` follows the
6
+ * workspace coordinate when the caller has one. The workspace coordinate is a
7
+ * `\\wsl.localhost\<distro>\...` path; a Linux, drive-letter or relative path
8
+ * carries no distro evidence and falls back to WSL's default.
9
+ *
10
+ * Pure by construction: the caller resolves the default distro once and the
11
+ * selection stays unit-testable without `wsl.exe`.
12
+ *
13
+ * This is a TypeScript source built to `lib/distro-route.js`; edit THIS file and
14
+ * run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
15
+ * fails when it drifts.
16
+ *
17
+ * @module dsh-plugin-wsl/distro-route
18
+ */
19
+ /** Why a distro was selected, for diagnostics and for the docs. */
20
+ export type DistroSource = "pinned" | "workspace" | "default";
21
+ /** One distro selection. */
22
+ export interface DistroChoice {
23
+ /** The distro name to use. */
24
+ distro: string;
25
+ /** Whether the name came from the config, the path, or WSL's default. */
26
+ source: DistroSource;
27
+ }
28
+ /**
29
+ * The distro a UNC path names.
30
+ *
31
+ * @param value - any path spelling.
32
+ * @returns the distro name, or undefined for a non-WSL path.
33
+ */
34
+ export declare function distroOfUnc(value: string | undefined): string | undefined;
35
+ /**
36
+ * The explicit `distro` config value, or undefined for `""`, `"auto"` and
37
+ * `"default"`.
38
+ *
39
+ * @param configured - the row's `distro` value.
40
+ * @returns the pinned distro, or undefined when the value is a mode.
41
+ */
42
+ export declare function pinnedDistro(configured: string | undefined): string | undefined;
43
+ /**
44
+ * Whether `distro: ""`/`"auto"` asks the workspace coordinate to decide.
45
+ *
46
+ * @param configured - the row's `distro` value.
47
+ * @returns true for the automatic modes.
48
+ */
49
+ export declare function isAutoDistro(configured: string | undefined): boolean;
50
+ /**
51
+ * Whether `distro: "default"` forces WSL's own default.
52
+ *
53
+ * @param configured - the row's `distro` value.
54
+ * @returns true for the explicit default mode.
55
+ */
56
+ export declare function isDefaultDistro(configured: string | undefined): boolean;
57
+ /**
58
+ * Select the distro for one operation.
59
+ *
60
+ * Order: an explicit distro wins; `"default"` takes the fallback; otherwise the
61
+ * first candidate that names a UNC distro wins, and the fallback closes the
62
+ * list. The candidate order matters — a request's workdir is stronger evidence
63
+ * than the configured default cwd.
64
+ *
65
+ * @param input - the configured value, the path candidates, and WSL's default.
66
+ * @returns the distro and why it was chosen.
67
+ */
68
+ export declare function selectDistro({ configured, candidates, fallback }: {
69
+ /** The row's `distro` value. */
70
+ configured: string | undefined;
71
+ /** Paths in any coordinate system, strongest evidence first. */
72
+ candidates: ReadonlyArray<string | undefined>;
73
+ /** WSL's default distro, already resolved by the caller. */
74
+ fallback: string;
75
+ }): DistroChoice;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Which distro an operation belongs to.
3
+ *
4
+ * The pinned name has three spellings: an explicit distro wins, `"default"`
5
+ * forces WSL's own default, and the empty string or `"auto"` follows the
6
+ * workspace coordinate when the caller has one. The workspace coordinate is a
7
+ * `\\wsl.localhost\<distro>\...` path; a Linux, drive-letter or relative path
8
+ * carries no distro evidence and falls back to WSL's default.
9
+ *
10
+ * Pure by construction: the caller resolves the default distro once and the
11
+ * selection stays unit-testable without `wsl.exe`.
12
+ *
13
+ * This is a TypeScript source built to `lib/distro-route.js`; edit THIS file and
14
+ * run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
15
+ * fails when it drifts.
16
+ *
17
+ * @module dsh-plugin-wsl/distro-route
18
+ */
19
+ import { uncToPosix } from "./paths.js";
20
+ /**
21
+ * The distro a UNC path names.
22
+ *
23
+ * @param value - any path spelling.
24
+ * @returns the distro name, or undefined for a non-WSL path.
25
+ */
26
+ export function distroOfUnc(value) {
27
+ const text = String(value ?? "").trim();
28
+ return text.length === 0 ? undefined : uncToPosix(text)?.distro;
29
+ }
30
+ /**
31
+ * The explicit `distro` config value, or undefined for `""`, `"auto"` and
32
+ * `"default"`.
33
+ *
34
+ * @param configured - the row's `distro` value.
35
+ * @returns the pinned distro, or undefined when the value is a mode.
36
+ */
37
+ export function pinnedDistro(configured) {
38
+ const text = String(configured ?? "").trim();
39
+ if (text.length === 0)
40
+ return undefined;
41
+ const mode = text.toLowerCase();
42
+ return mode === "auto" || mode === "default" ? undefined : text;
43
+ }
44
+ /**
45
+ * Whether `distro: ""`/`"auto"` asks the workspace coordinate to decide.
46
+ *
47
+ * @param configured - the row's `distro` value.
48
+ * @returns true for the automatic modes.
49
+ */
50
+ export function isAutoDistro(configured) {
51
+ const text = String(configured ?? "").trim().toLowerCase();
52
+ return text.length === 0 || text === "auto";
53
+ }
54
+ /**
55
+ * Whether `distro: "default"` forces WSL's own default.
56
+ *
57
+ * @param configured - the row's `distro` value.
58
+ * @returns true for the explicit default mode.
59
+ */
60
+ export function isDefaultDistro(configured) {
61
+ return String(configured ?? "").trim().toLowerCase() === "default";
62
+ }
63
+ /**
64
+ * Select the distro for one operation.
65
+ *
66
+ * Order: an explicit distro wins; `"default"` takes the fallback; otherwise the
67
+ * first candidate that names a UNC distro wins, and the fallback closes the
68
+ * list. The candidate order matters — a request's workdir is stronger evidence
69
+ * than the configured default cwd.
70
+ *
71
+ * @param input - the configured value, the path candidates, and WSL's default.
72
+ * @returns the distro and why it was chosen.
73
+ */
74
+ export function selectDistro({ configured, candidates, fallback }) {
75
+ const pinned = pinnedDistro(configured);
76
+ if (pinned !== undefined)
77
+ return { distro: pinned, source: "pinned" };
78
+ if (isDefaultDistro(configured))
79
+ return { distro: fallback, source: "default" };
80
+ for (const candidate of candidates) {
81
+ const named = distroOfUnc(candidate);
82
+ if (named !== undefined)
83
+ return { distro: named, source: "workspace" };
84
+ }
85
+ return { distro: fallback, source: "default" };
86
+ }
@@ -45,7 +45,10 @@ export interface RoutingConfig {
45
45
  cwd?: string;
46
46
  /** The host backend's overwrite-diff byte bound. */
47
47
  diffBasisMaxBytes?: number;
48
- /** The pinned distro for routed targets; empty resolves WSL's default. */
48
+ /**
49
+ * The distro mode for routed targets: empty/`"auto"` follows each target's
50
+ * UNC distro, `"default"` forces WSL's default, and an explicit name pins.
51
+ */
49
52
  distro?: string;
50
53
  /** The `wsl.exe` path, used only to resolve the default distro. */
51
54
  wslPath?: string;