dsh-plugin-wsl-env 0.14.0 → 0.16.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 (57) hide show
  1. package/CHANGELOG.md +167 -14
  2. package/CONTRIBUTING.md +25 -33
  3. package/README.md +10 -10
  4. package/SECURITY.md +6 -4
  5. package/SUPPORT.md +8 -8
  6. package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
  7. package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
  8. package/agent/wsl-agent.sh +4 -5
  9. package/cordis.patch.yml +8 -1
  10. package/docs/ARCHITECTURE.md +42 -35
  11. package/docs/COMPATIBILITY.md +121 -0
  12. package/docs/CONFIGURATION.md +254 -272
  13. package/docs/LIMITATIONS.md +78 -35
  14. package/docs/MIGRATION.md +16 -13
  15. package/docs/PARITY.md +4 -3
  16. package/docs/PERFORMANCE.md +11 -9
  17. package/docs/QUICKSTART.md +60 -17
  18. package/docs/README.zh.md +7 -7
  19. package/docs/RELEASING.md +257 -0
  20. package/docs/REQUIREMENTS.zh.md +77 -0
  21. package/docs/ROADMAP-1.0.md +105 -61
  22. package/docs/ROADMAP.md +15 -9
  23. package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +3 -3
  24. package/docs/UPSTREAM-FSIO-EXPORT.md +7 -7
  25. package/docs/UPSTREAM-OPEN-IN-APP.md +57 -0
  26. package/docs/UPSTREAM-SESSION-NOTICE.md +4 -4
  27. package/docs/VALUE.zh.md +144 -0
  28. package/docs/adr/0002-native-agent.md +348 -0
  29. package/docs/root-fs-routing.md +186 -0
  30. package/docs/screenshots/picker.png +0 -0
  31. package/docs/screenshots/preview.png +0 -0
  32. package/docs/screenshots/sandbox.png +0 -0
  33. package/docs/screenshots/terminal.png +0 -0
  34. package/lib/agent-confined.d.ts +29 -23
  35. package/lib/agent-confined.js +58 -25
  36. package/lib/agent-exec.d.ts +2 -3
  37. package/lib/agent-shared.js +1 -0
  38. package/lib/agent.d.ts +19 -8
  39. package/lib/agent.js +73 -13
  40. package/lib/fs-routing.d.ts +2 -1
  41. package/lib/fs-routing.js +13 -1
  42. package/lib/fs-substrate.d.ts +11 -9
  43. package/lib/fs-substrate.js +6 -9
  44. package/lib/fsio-agent.d.ts +2 -3
  45. package/lib/fsio-agent.js +2 -3
  46. package/lib/index.d.ts +77 -13
  47. package/lib/index.js +63 -13
  48. package/lib/listing.d.ts +19 -12
  49. package/lib/listing.js +34 -19
  50. package/lib/picker.js +4 -4
  51. package/lib/session-runtime.d.ts +5 -3
  52. package/lib/session-runtime.js +5 -4
  53. package/locale/en.json +1 -1
  54. package/locale/zh.json +1 -1
  55. package/package.json +13 -4
  56. package/scripts/diagnose.sh +1 -1
  57. package/scripts/doctor.mjs +83 -9
@@ -36,6 +36,7 @@ import { fileURLToPath } from "node:url";
36
36
  import { parseDistroList } from "../lib/wsl.js";
37
37
  import { AGENT_NAME, PROTOCOL_VERSION } from "../lib/agent-protocol.js";
38
38
  import { sessionScopeSweepScript } from "../lib/session-scope.js";
39
+ import { parseListeningPorts, procNetScript } from "../lib/ports.js";
39
40
 
40
41
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
41
42
  const WSL = "wsl.exe";
@@ -63,10 +64,10 @@ const STALE_SCOPE_REPORT = [
63
64
  ' if [ -n "$cg" ] && [ -r "/sys/fs/cgroup$cg/cgroup.procs" ]; then',
64
65
  ' while read -r pid; do',
65
66
  ' [ -n "$pid" ] || continue',
66
- " if tr '\\0' ' ' < \"/proc/$pid/cmdline\" 2>/dev/null | grep -q -e 'wsl-agent.sh' -e 'dsh-wsl-agent'; then alive=1; break; fi",
67
+ " if tr '\\0' ' ' < \"/proc/$pid/cmdline\" 2>/dev/null | grep -q -e 'wsl-agent.sh' -e 'dsh-wsl-agent'; then alive=1; pids=\"$pids $pid\"; fi",
67
68
  ' done < "/sys/fs/cgroup$cg/cgroup.procs"',
68
69
  " fi",
69
- ' echo "$unit $([ "$alive" -eq 1 ] && echo active || echo stale)"',
70
+ ' echo "$unit $([ "$alive" -eq 1 ] && echo active || echo stale) pids:${pids:-none}"',
70
71
  "done",
71
72
  ].join("\n");
72
73
  /** The agent-cache inventory: one `state digest` line per cached binary. */
@@ -254,7 +255,7 @@ function installRemedy(distro, family) {
254
255
  * @returns {number} the exit status.
255
256
  */
256
257
  function rejectReadOnly() {
257
- console.error("doctor: --read-only refuses --fix/--install; run `pnpm run doctor -- --fix` instead");
258
+ console.error("doctor: --read-only refuses --fix/--install; run `pnpm run doctor --fix` instead");
258
259
  return 2;
259
260
  }
260
261
 
@@ -333,7 +334,7 @@ async function main(options) {
333
334
  const bwrapPath = (await inDistro(distro, "command -v bwrap")).stdout.trim();
334
335
  const install = installRemedy(distro, family);
335
336
  if (bwrapPath.length === 0) {
336
- check("sandbox.bwrap", "fail", "bubblewrap", "missing; every confined command fails closed without it", `${install}, or run "pnpm run bootstrap -- ${distro} --install"`);
337
+ check("sandbox.bwrap", "fail", "bubblewrap", "missing; every confined command fails closed without it", `${install}, or run "pnpm run bootstrap ${distro} --install"`);
337
338
  } else {
338
339
  check("sandbox.bwrap", "ok", "bubblewrap", bwrapPath);
339
340
  const userns = await inDistro(distro, "bwrap --ro-bind / / -- true");
@@ -385,6 +386,14 @@ async function main(options) {
385
386
  check("agent.cache", bad > 0 ? "warn" : "ok", "Agent cache", `${entries.length} cached binary(ies)${bad > 0 ? `, ${bad} not executable` : ""}`, bad > 0 ? `rm -rf ~/.cache/dsh-wsl/agent inside "${distro}"; the next command repopulates it` : undefined);
386
387
  }
387
388
 
389
+ const scopes = (await inDistro(distro, STALE_SCOPE_REPORT)).stdout.trim();
390
+ const scopeLines = scopes === "none" || scopes.length === 0 ? [] : scopes.split(/\r?\n/);
391
+ const stale = scopeLines.filter((line) => /\sstale(\s|$)/.test(line)).length;
392
+ const livePids = scopeLines.flatMap((line) => {
393
+ const m = /\spids:([0-9 ]+)$/.exec(line);
394
+ if (m === null) return [];
395
+ return (m[1] ?? "").trim().split(/\s+/).filter((p) => p.length > 0);
396
+ });
388
397
  // ---- resident handshake --------------------------------------------------
389
398
  const agentScript = join(ROOT, "agent", "wsl-agent.sh");
390
399
  if (!agentScript.startsWith("/")) {
@@ -394,8 +403,11 @@ async function main(options) {
394
403
  } else {
395
404
  const handshake = await probeResident(distro, agentScript);
396
405
  if (handshake.ok) {
406
+ const live = livePids.length > 0
407
+ ? `; a live resident (pid${livePids.length > 1 ? "s" : ""} ${livePids.join(", ")}) is already running in this distro — this check starts a fresh one, which cannot read its last error`
408
+ : "";
397
409
  check("agent.handshake", handshake.digestMatches ? "ok" : "fail", "Agent handshake",
398
- `pid ${handshake.pid}, protocol ${PROTOCOL_VERSION}, digest ${handshake.digestMatches ? "matches" : "MISMATCH — the distro reads a different script than this package"}`,
410
+ `pid ${handshake.pid}, protocol ${PROTOCOL_VERSION}, digest ${handshake.digestMatches ? "matches" : "MISMATCH — the distro reads a different script than this package"}${live}`,
399
411
  handshake.digestMatches ? undefined : "resync the runtime copy (pnpm run sync:windows), then restart the app");
400
412
  } else {
401
413
  check("agent.handshake", "fail", "Agent handshake", handshake.detail, "restart the DSH app; if it persists, delete ~/.cache/dsh-wsl/agent inside the distro and rerun the doctor");
@@ -403,13 +415,75 @@ async function main(options) {
403
415
  }
404
416
 
405
417
  // ---- stale session scopes ------------------------------------------------
406
- const scopes = (await inDistro(distro, STALE_SCOPE_REPORT)).stdout.trim();
407
- const scopeLines = scopes === "none" || scopes.length === 0 ? [] : scopes.split(/\r?\n/);
408
- const stale = scopeLines.filter((line) => line.endsWith(" stale")).length;
409
418
  if (scopeLines.length === 0) check("runtime.scopes", "info", "Session scopes", "none (no user manager, or none left behind)");
410
419
  else if (stale === 0) check("runtime.scopes", "ok", "Session scopes", `${scopeLines.length} live, none stale`);
411
420
  else check("runtime.scopes", "warn", "Session scopes", `${stale} stale of ${scopeLines.length}`, "--fix stops them (the same sweep the session preflight runs); they are safe to stop when no agent process is inside");
412
421
 
422
+ // ---- sessions across running distros (WSL-309) ----------------------------
423
+ // The single-distro checks above answer "is this distro healthy?"; this
424
+ // survey answers "what is running right now, everywhere?" — the concurrent
425
+ // sessions coordinate routing makes possible, made visible. Idle timers are
426
+ // the host process's private state and stay out: a scope's presence and its
427
+ // resident pids are the facts the distro side can attest.
428
+ // The distro lists both surveys share: `run` settles an object
429
+ // ({ ok, stdout, code }); `-q` prints plain names, and the running list's
430
+ // header is dropped by the intersection with them. The default distro
431
+ // carries a localized marker ("ubuntu (默认值)" / "ubuntu (default)") —
432
+ // strip a trailing parenthesized note before the intersection.
433
+ const distroNames = ((await run([WSL, "--list", "-q"])).stdout ?? "").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
434
+ const running = ((await run([WSL, "--list", "--running"])).stdout ?? "").split(/\r?\n/).map((line) => line.trim().replace(/\s+\([^)]*\)$/, "")).filter((line) => distroNames.includes(line));
435
+ if (!options.json) {
436
+ console.log("");
437
+ console.log("sessions across running distros");
438
+ if (running.length === 0) {
439
+ console.log(" (none)");
440
+ } else {
441
+ for (const d of running) {
442
+ const report = (await inDistro(d, STALE_SCOPE_REPORT)).stdout.trim();
443
+ const lines = report === "none" || report.length === 0 ? [] : report.split(/\r?\n/);
444
+ if (report === "none") {
445
+ console.log(` ${d} no user manager — scopes unavailable`);
446
+ continue;
447
+ }
448
+ if (lines.length === 0) {
449
+ console.log(` ${d} no session scopes`);
450
+ continue;
451
+ }
452
+ for (const line of lines) {
453
+ const m = /^(\S+) (active|stale) pids:(.*)$/.exec(line);
454
+ if (m === null) { console.log(` ${d} ${line}`); continue; }
455
+ const scope = m[1] ?? line;
456
+ const state = m[2] ?? "";
457
+ const pidNote = m[3] === "none" ? "" : ` resident pids:${(m[3] ?? "").trim()}`;
458
+ console.log(` ${d} ${scope} ${state}${pidNote}`);
459
+ }
460
+ }
461
+ }
462
+ }
463
+
464
+ // ---- listening ports across running distros (WSL-308) ----------------------
465
+ // The port poller serves the model's session (DSH_WSL_PORTS); this survey
466
+ // serves the human, with the two actions the facts imply: the localhost URL
467
+ // WSL2's forwarding already makes reachable, and the netsh line that would
468
+ // extend reach beyond the host (printed, never applied).
469
+ if (!options.json) {
470
+ const portScript = procNetScript();
471
+ let anyPorts = false;
472
+ for (const d of running) {
473
+ const answer = await inDistro(d, portScript);
474
+ const ports = parseListeningPorts(answer.stdout);
475
+ if (ports.length === 0) continue;
476
+ anyPorts = true;
477
+ const ip = ((await inDistro(d, "hostname -I")).stdout ?? "").trim().split(/\s+/)[0] ?? "";
478
+ console.log("");
479
+ console.log(`listening ports in ${d}`);
480
+ for (const port of ports) {
481
+ console.log(` :${port} http://localhost:${port} (LAN: netsh interface portproxy add v4tov4 listenport=${port} listenaddress=0.0.0.0 connectport=${port} connectaddress=${ip})`);
482
+ }
483
+ }
484
+ if (!anyPorts && running.length > 0) console.log("\nno listening ports in the running distros");
485
+ }
486
+
413
487
  // ---- peers ---------------------------------------------------------------
414
488
  const manifest = /** @type {{ peerDependencies?: Record<string, string> }} */ (JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")));
415
489
  const peers = Object.entries(manifest.peerDependencies ?? {});
@@ -442,7 +516,7 @@ async function main(options) {
442
516
  if (/^\s*-\s*id:\s*(wsl-shell|wsl-fs)\s*$/m.test(text)) trapped.push(entry.name);
443
517
  }
444
518
  if (trapped.length > 0) {
445
- check("config.preset-trap", "warn", "Profile patches", `nested rows patched by id in: ${trapped.join(", ")}`, "wsl-shell and wsl-fs live inside preset-wsl; an id patch matches nothing and is silently ignored. Run \"pnpm run preset -- --check <patch>\" to get the complete block, or see docs/CONFIGURATION.md#overriding-the-rows-inside-preset-wsl");
519
+ check("config.preset-trap", "warn", "Profile patches", `nested rows patched by id in: ${trapped.join(", ")}`, "wsl-shell and wsl-fs live inside preset-wsl; an id patch matches nothing and is ignored with a warning. Run \"pnpm run preset --check <patch>\" to get the complete block, or see docs/CONFIGURATION.md#configure-wsl-shell-and-wsl-fs");
446
520
  } else {
447
521
  check("config.profiles", "ok", "Profile patches", `${seen} profile patch(es) scanned`);
448
522
  }