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.
Files changed (92) hide show
  1. package/CHANGELOG.md +345 -8
  2. package/CONTRIBUTING.md +221 -68
  3. package/README.md +50 -35
  4. package/SECURITY.md +18 -2
  5. package/SUPPORT.md +23 -7
  6. package/cordis.patch.yml +9 -0
  7. package/docs/ARCHITECTURE.md +200 -57
  8. package/docs/CONFIGURATION.md +180 -16
  9. package/docs/LIMITATIONS.md +91 -33
  10. package/docs/PARITY.md +66 -38
  11. package/docs/PEER-PARITY.md +95 -32
  12. package/docs/README.zh.md +49 -31
  13. package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
  14. package/docs/UPSTREAM-TERMINAL-TITLE.md +49 -14
  15. package/lib/agent-confined.d.ts +42 -0
  16. package/lib/agent-confined.js +25 -41
  17. package/lib/agent-errors.d.ts +31 -0
  18. package/lib/agent-errors.js +19 -16
  19. package/lib/agent-exec.d.ts +159 -0
  20. package/lib/agent-exec.js +151 -165
  21. package/lib/agent-protocol.d.ts +192 -0
  22. package/lib/agent-protocol.js +76 -100
  23. package/lib/agent-shared.d.ts +27 -0
  24. package/lib/agent-shared.js +12 -12
  25. package/lib/agent.d.ts +353 -0
  26. package/lib/agent.js +536 -552
  27. package/lib/auto-preset.d.ts +110 -0
  28. package/lib/auto-preset.js +157 -141
  29. package/lib/bwrap.d.ts +90 -0
  30. package/lib/bwrap.js +18 -18
  31. package/lib/file-reference-wsl.d.ts +160 -0
  32. package/lib/file-reference-wsl.js +192 -173
  33. package/lib/fs-decisions.d.ts +107 -0
  34. package/lib/fs-decisions.js +64 -60
  35. package/lib/fs-routing.d.ts +182 -0
  36. package/lib/fs-routing.js +173 -116
  37. package/lib/fs-substrate.d.ts +258 -0
  38. package/lib/fs-substrate.js +248 -248
  39. package/lib/fs.d.ts +10 -0
  40. package/lib/fs.js +5 -0
  41. package/lib/fsio-agent.d.ts +307 -0
  42. package/lib/fsio-agent.js +414 -410
  43. package/lib/fsio-text.d.ts +115 -0
  44. package/lib/fsio-text.js +104 -84
  45. package/lib/git-route.d.ts +100 -0
  46. package/lib/git-route.js +46 -47
  47. package/lib/index.d.ts +1003 -0
  48. package/lib/index.js +1106 -1003
  49. package/lib/listing.d.ts +114 -0
  50. package/lib/listing.js +68 -55
  51. package/lib/paths.d.ts +182 -0
  52. package/lib/paths.js +134 -109
  53. package/lib/picker.d.ts +266 -0
  54. package/lib/picker.js +371 -351
  55. package/lib/ports.d.ts +44 -0
  56. package/lib/ports.js +39 -29
  57. package/lib/preflight.d.ts +91 -0
  58. package/lib/preflight.js +94 -0
  59. package/lib/preset-choice.d.ts +33 -0
  60. package/lib/preset-choice.js +15 -7
  61. package/lib/probe-cache.d.ts +51 -0
  62. package/lib/probe-cache.js +43 -29
  63. package/lib/sandbox-core.d.ts +149 -0
  64. package/lib/sandbox-core.js +239 -87
  65. package/lib/sandbox.d.ts +48 -0
  66. package/lib/sandbox.js +4 -4
  67. package/lib/search-exec.d.ts +96 -0
  68. package/lib/search-exec.js +52 -79
  69. package/lib/search-route.d.ts +75 -0
  70. package/lib/search-route.js +30 -31
  71. package/lib/shell-env.d.ts +88 -0
  72. package/lib/shell-env.js +78 -74
  73. package/lib/shell.d.ts +10 -0
  74. package/lib/shell.js +5 -0
  75. package/lib/subprocess.d.ts +313 -0
  76. package/lib/subprocess.js +267 -262
  77. package/lib/terminal-activity.d.ts +98 -0
  78. package/lib/terminal-activity.js +29 -39
  79. package/lib/terminal-route.d.ts +27 -0
  80. package/lib/terminal-route.js +16 -8
  81. package/lib/watcher.d.ts +77 -0
  82. package/lib/watcher.js +120 -126
  83. package/lib/workspace-files-route.d.ts +132 -0
  84. package/lib/workspace-files-route.js +73 -81
  85. package/lib/workspace-files-wsl.d.ts +257 -0
  86. package/lib/workspace-files-wsl.js +311 -251
  87. package/lib/wsl.d.ts +266 -0
  88. package/lib/wsl.js +150 -155
  89. package/locale/preflight/en.json +6 -0
  90. package/locale/preflight/zh.json +6 -0
  91. package/package.json +78 -22
  92. package/scripts/diagnose.sh +92 -0
@@ -23,92 +23,65 @@
23
23
  * Pure shaping over injected collaborators, so the handle semantics are
24
24
  * unit-testable without a distro.
25
25
  *
26
+ * This is a TypeScript source built to `lib/search-exec.js`; edit THIS file and
27
+ * run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
28
+ * fails when it drifts.
29
+ *
26
30
  * @module dsh-plugin-wsl/search-exec
27
31
  */
28
-
29
32
  import { AgentUnavailableError } from "./agent-errors.js";
30
-
31
- /**
32
- * The output-stream reader the search tool consumes: `handle.collected.stdout`
33
- * / `stderr` expose `readFrom(offset)` with the seam's standard shape.
34
- *
35
- * @typedef {Object} SearchCollectedReader
36
- * @property {(from: number) => {text: string, lossy: boolean, nextOffset: number}} readFrom
37
- */
38
-
39
- /**
40
- * The slice of the subprocess handle the file-search tool consumes
41
- * (`dsh-tool-fs-search` `runRipgrep`): settle on `done`, then read the
42
- * collected streams. Nothing else of the handle is contract.
43
- *
44
- * @typedef {Object} SearchExecutionHandle
45
- * @property {Promise<{exitCode: number|null, signal: string|null}>} done
46
- * @property {{stdout: SearchCollectedReader, stderr: SearchCollectedReader}} collected
47
- * @property {() => boolean|void} [kill] - optional cooperative kill; the tool
48
- * relies on its `signal` for cancellation instead.
49
- */
50
-
51
33
  /**
52
34
  * Run one search command through the agent and present it as a search handle.
53
35
  *
54
36
  * @param object - the handle's collaborators.
55
- * @param {object} object.agent - the {@link WslAgent}-like runner: `exec`
56
- * accepting `{cwd, argv, maxOutputBytes, signal}` and resolving
57
- * `{exitCode, stdout, stderr}`; throws `AgentUnavailableError` when out.
58
- * @param {string} object.cwd - the Linux working directory to search from.
59
- * @param {string[]} object.argv - the distro-side argv (`rg` and its arguments).
60
- * @param {number} [object.maxOutputBytes] - per-stream capture ceiling the
61
- * agent enforces; the tool's own raw-output budget rides here.
62
- * @param {AbortSignal} [object.signal] - the caller's cancellation.
63
- * @param {() => object} object.spawnFallback - builds the one-shot handle the
64
- * facade delegates to when the agent is out; called at most once, lazily.
65
- * @returns {object} the search handle (`done`, `collected`, `kill`).
37
+ * @returns the search handle (`done`, `collected`, `kill`).
66
38
  */
67
39
  export function searchExecutionHandle({ agent, cwd, argv, maxOutputBytes = 0, signal, spawnFallback }) {
68
- const state = { delegate: undefined, stdout: undefined, stderr: undefined };
69
- const controller = new AbortController();
70
- if (signal?.aborted) controller.abort(signal.reason);
71
- else signal?.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
72
-
73
- const reader = (slot) => ({
74
- readFrom(from) {
75
- const buffer = state[slot] ?? Buffer.alloc(0);
76
- const start = Math.min(from, buffer.length);
77
- return { text: buffer.subarray(start).toString("utf8"), lossy: false, nextOffset: buffer.length };
78
- },
79
- });
80
-
81
- /** @type {object} */
82
- const handle = {
83
- get collected() {
84
- if (state.delegate !== undefined) return state.delegate.collected;
85
- return { stdout: reader("stdout"), stderr: reader("stderr") };
86
- },
87
- done: (async () => {
88
- let result;
89
- try {
90
- result = await agent.exec({ cwd, argv, maxOutputBytes, signal: controller.signal });
91
- state.stdout = result.stdout;
92
- state.stderr = result.stderr;
93
- } catch (error) {
94
- if (!(error instanceof AgentUnavailableError)) {
95
- if (controller.signal.aborted) {
96
- // Our kill() or the caller's cancellation: the killed outcome the
97
- // one-shot path reports, not a failure.
98
- return { exitCode: null, signal: "SIGTERM" };
99
- }
100
- throw error;
101
- }
102
- const delegate = spawnFallback();
103
- state.delegate = delegate;
104
- return delegate.done;
105
- }
106
- return { exitCode: result.exitCode, signal: null };
107
- })(),
108
- kill() {
109
- controller.abort(new Error("killed"));
110
- state.delegate?.kill?.();
111
- },
112
- };
113
- return handle;
40
+ const state = { delegate: undefined, stdout: undefined, stderr: undefined };
41
+ const controller = new AbortController();
42
+ if (signal?.aborted)
43
+ controller.abort(signal.reason);
44
+ else
45
+ signal?.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
46
+ const reader = (slot) => ({
47
+ readFrom(from) {
48
+ const buffer = state[slot] ?? Buffer.alloc(0);
49
+ const start = Math.min(from, buffer.length);
50
+ return { text: buffer.subarray(start).toString("utf8"), lossy: false, nextOffset: buffer.length };
51
+ },
52
+ });
53
+ const handle = {
54
+ get collected() {
55
+ if (state.delegate !== undefined)
56
+ return state.delegate.collected;
57
+ return { stdout: reader("stdout"), stderr: reader("stderr") };
58
+ },
59
+ done: (async () => {
60
+ let result;
61
+ try {
62
+ result = await agent.exec({ cwd, argv, maxOutputBytes, signal: controller.signal });
63
+ state.stdout = result.stdout;
64
+ state.stderr = result.stderr;
65
+ }
66
+ catch (error) {
67
+ if (!(error instanceof AgentUnavailableError)) {
68
+ if (controller.signal.aborted) {
69
+ // Our kill() or the caller's cancellation: the killed outcome the
70
+ // one-shot path reports, not a failure.
71
+ return { exitCode: null, signal: "SIGTERM" };
72
+ }
73
+ throw error;
74
+ }
75
+ const delegate = spawnFallback();
76
+ state.delegate = delegate;
77
+ return delegate.done;
78
+ }
79
+ return { exitCode: result.exitCode, signal: null };
80
+ })(),
81
+ kill() {
82
+ controller.abort(new Error("killed"));
83
+ state.delegate?.kill?.();
84
+ },
85
+ };
86
+ return handle;
114
87
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Which execution world a file-search spawn belongs to, and the `wsl.exe`
3
+ * argv it becomes there.
4
+ *
5
+ * `dsh-tool-fs-search` spawns the packaged ripgrep binary through
6
+ * `ctx.subprocess` with the session workspace as the working directory. Left
7
+ * alone, that spawn runs the Windows binary over the distro's 9p share — the
8
+ * one model-facing I/O path that still crossed the share, and in the slow
9
+ * direction: measured at ~17 ms per file of metadata round trips where the
10
+ * distro's own ext4 answers in microseconds. The interception point is the
11
+ * subprocess provider's `spawn`, and the decision lives here — pure,
12
+ * peer-free, and unit-testable, mirroring `terminal-route.js`.
13
+ *
14
+ * The rewrite is deliberately narrow. A spawn is intercepted only when the
15
+ * working directory NAMES the distro outright — a `\\wsl.localhost\<distro>`
16
+ * UNC — because that is the form a distro session's search carries and the
17
+ * only one that resolves without an async distro query: `spawn` hands back
18
+ * its handle synchronously, so the override must stay synchronous. A Windows
19
+ * drive directory keeps the host binary, which is native there and touches
20
+ * no share; anything else passes through untouched, exactly as before.
21
+ *
22
+ * The rewritten argv forwards every rg argument verbatim, so the tool reads
23
+ * the output format and exit codes of the distro's own rg. One addition is
24
+ * made: when the tool named no search path — its pattern rides `--regexp=`,
25
+ * and a bare `--` separator is exactly how it spells an explicit path — the
26
+ * rewrite appends `-- .`. Reason: `wsl.exe --exec` hands the distro-side rg a
27
+ * relay FIFO for stdin, and rg's readable-stdin heuristic then searches stdin
28
+ * (instantly empty) instead of walking the working directory; the packaged
29
+ * binary under the old path saw a non-readable NUL stdin and walked it. An
30
+ * explicit path makes the search deterministic on either transport.
31
+ *
32
+ * The distro-side binary must therefore exist — `scripts/bootstrap.sh` reports
33
+ * it — and a distro without one surfaces rg's own "command not found" instead
34
+ * of a silent fall-back to the share.
35
+ *
36
+ * This is a TypeScript source built to `lib/search-route.js`; edit THIS file
37
+ * and run `pnpm run build` — the artifact under `lib/` is generated, and
38
+ * `pnpm test` fails when it drifts.
39
+ *
40
+ * @module dsh-plugin-wsl/search-route
41
+ */
42
+ /**
43
+ * Whether a spawn's program is the ripgrep binary.
44
+ *
45
+ * A bare `argv[0]` is judged by its own name, so the packaged binary's
46
+ * absolute path and any other spelling of `rg` match alike.
47
+ *
48
+ * @param program - the spawn's `argv[0]`.
49
+ * @returns true when the spawn is a ripgrep launch.
50
+ */
51
+ export declare function isSearchProgram(program: unknown): boolean;
52
+ /**
53
+ * The distro-side rewrite of one search spawn, when it belongs in the distro.
54
+ *
55
+ * Two argv forms come back for one decision. `rgArgv` is the distro command
56
+ * itself (`rg` and its arguments) — what the resident agent execs. `wslArgv`
57
+ * is the same command wrapped for a one-shot `wsl.exe --exec` launch — the
58
+ * fallback transport for when the agent is out. The working directory becomes
59
+ * the `--cd` the distro starts in; `wsl.exe` itself must start in a Windows
60
+ * directory, so the caller supplies its own host cwd when applying `wslArgv`.
61
+ *
62
+ * @param object - the spawn's inputs.
63
+ * @returns the rewrite, or undefined when the spawn is not a distro search and
64
+ * must run exactly as the shipped implementation would run it.
65
+ */
66
+ export declare function searchSpawnRewrite({ argv, cwd, wslPath }: {
67
+ argv: readonly string[];
68
+ cwd: string;
69
+ wslPath: string;
70
+ }): {
71
+ distro: string;
72
+ linuxCwd: string;
73
+ rgArgv: string[];
74
+ wslArgv: string[];
75
+ } | undefined;
@@ -33,29 +33,30 @@
33
33
  * it — and a distro without one surfaces rg's own "command not found" instead
34
34
  * of a silent fall-back to the share.
35
35
  *
36
+ * This is a TypeScript source built to `lib/search-route.js`; edit THIS file
37
+ * and run `pnpm run build` — the artifact under `lib/` is generated, and
38
+ * `pnpm test` fails when it drifts.
39
+ *
36
40
  * @module dsh-plugin-wsl/search-route
37
41
  */
38
-
39
42
  import { uncToPosix } from "./paths.js";
40
-
41
43
  /** The one program this rewrite passes through to the distro. */
42
44
  const SEARCH_PROGRAM = "rg";
43
-
44
45
  /**
45
46
  * Whether a spawn's program is the ripgrep binary.
46
47
  *
47
48
  * A bare `argv[0]` is judged by its own name, so the packaged binary's
48
49
  * absolute path and any other spelling of `rg` match alike.
49
50
  *
50
- * @param unknown - the spawn's `argv[0]`.
51
+ * @param program - the spawn's `argv[0]`.
51
52
  * @returns true when the spawn is a ripgrep launch.
52
53
  */
53
54
  export function isSearchProgram(program) {
54
- if (typeof program !== "string" || program.length === 0) return false;
55
- const base = program.slice(Math.max(program.lastIndexOf("/"), program.lastIndexOf("\\")) + 1);
56
- return base.toLowerCase().replace(/\.exe$/u, "") === SEARCH_PROGRAM;
55
+ if (typeof program !== "string" || program.length === 0)
56
+ return false;
57
+ const base = program.slice(Math.max(program.lastIndexOf("/"), program.lastIndexOf("\\")) + 1);
58
+ return base.toLowerCase().replace(/\.exe$/u, "") === SEARCH_PROGRAM;
57
59
  }
58
-
59
60
  /**
60
61
  * The distro-side rewrite of one search spawn, when it belongs in the distro.
61
62
  *
@@ -67,29 +68,27 @@ export function isSearchProgram(program) {
67
68
  * directory, so the caller supplies its own host cwd when applying `wslArgv`.
68
69
  *
69
70
  * @param object - the spawn's inputs.
70
- * @param {unknown[]} object.argv - the spawn's argv; `argv[0]` is the program.
71
- * @param {unknown} object.cwd - the spawn's working directory, any coordinate system.
72
- * @param {string} object.wslPath - path to `wsl.exe`.
73
- * @returns {{distro: string, linuxCwd: string, rgArgv: string[], wslArgv: string[]}|undefined} the
74
- * rewrite, or undefined when the spawn is not a distro search and must run
75
- * exactly as the shipped implementation would run it.
71
+ * @returns the rewrite, or undefined when the spawn is not a distro search and
72
+ * must run exactly as the shipped implementation would run it.
76
73
  */
77
74
  export function searchSpawnRewrite({ argv, cwd, wslPath }) {
78
- if (!Array.isArray(argv) || !isSearchProgram(argv[0])) return undefined;
79
- const named = uncToPosix(cwd);
80
- if (named === undefined) return undefined;
81
- const linuxCwd = named.linuxPath.length > 0 ? named.linuxPath : "/";
82
- // No bare `--` means the tool named no search path; without one, distro-side
83
- // rg reads the relay fifo on stdin (rg's readable-stdin heuristic) instead of
84
- // walking the directory. `.` is the working directory — the same directory
85
- // the packaged binary walked when the host spawned it directly.
86
- const forwarded = argv.slice(1);
87
- const tool = forwarded.includes("--") ? forwarded : [...forwarded, "--", "."];
88
- const rgArgv = [SEARCH_PROGRAM, ...tool];
89
- return {
90
- distro: named.distro,
91
- linuxCwd,
92
- rgArgv,
93
- wslArgv: [wslPath, "-d", named.distro, "--cd", linuxCwd, "--exec", ...rgArgv],
94
- };
75
+ if (!Array.isArray(argv) || !isSearchProgram(argv[0]))
76
+ return undefined;
77
+ const named = uncToPosix(cwd);
78
+ if (named === undefined)
79
+ return undefined;
80
+ const linuxCwd = named.linuxPath.length > 0 ? named.linuxPath : "/";
81
+ // No bare `--` means the tool named no search path; without one, distro-side
82
+ // rg reads the relay fifo on stdin (rg's readable-stdin heuristic) instead of
83
+ // walking the directory. `.` is the working directory — the same directory
84
+ // the packaged binary walked when the host spawned it directly.
85
+ const forwarded = argv.slice(1);
86
+ const tool = forwarded.includes("--") ? forwarded : [...forwarded, "--", "."];
87
+ const rgArgv = [SEARCH_PROGRAM, ...tool];
88
+ return {
89
+ distro: named.distro,
90
+ linuxCwd,
91
+ rgArgv,
92
+ wslArgv: [wslPath, "-d", named.distro, "--cd", linuxCwd, "--exec", ...rgArgv],
93
+ };
95
94
  }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Contribute the WSL environment's facts to the managed `DSH_*` namespace.
3
+ *
4
+ * `dsh-shell-env` owns `ctx.shellEnv`, the registry that rebuilds the trusted
5
+ * `DSH_*` variables injected into every model shell call. That is DSH's sanctioned
6
+ * channel for telling a model what its shell environment actually is — the shell
7
+ * tool's own description points the model at it ("Managed `$DSH_*` variables
8
+ * expose current harness environment facts").
9
+ *
10
+ * It has one built-in key that looks like it should carry this, and does not:
11
+ *
12
+ * collect() { const values = { [DSH_HOME_ENV]: this.dshHome, [DSH_SHELL_KEY]: "1" } }
13
+ *
14
+ * `DSH_SHELL` is a marker FLAG (`=1`) meaning "a DSH-managed shell call", not the
15
+ * shell path, and it is in `RESERVED_BASH_ENV_KEYS`, so a contributor may not own
16
+ * it. The supported way to convey the real shell is a `DSH_`-prefixed key of our
17
+ * own, declared with a description and resolved synchronously.
18
+ *
19
+ * Scoping uses the same test as `auto-preset`: the agent's recorded workspace
20
+ * directory. A session whose workspace is inside a distro is exactly a session
21
+ * whose shell calls run in that distro — so a host session sees none of these.
22
+ *
23
+ * This is a TypeScript source built to `lib/shell-env.js`; edit THIS file and
24
+ * run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
25
+ * fails when it drifts.
26
+ *
27
+ * @module dsh-plugin-wsl/shell-env
28
+ */
29
+ import z from "@deepseek-ai/schemastery";
30
+ import type { Context } from "@deepseek-ai/cordis";
31
+ export declare const name = "wsl-shell-env";
32
+ export declare const inject: string[];
33
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
34
+ /** Distro these facts describe; empty means this machine's default distro. */
35
+ distro: z<string, string, "defined">;
36
+ /** How often the listening-port snapshot refreshes, in milliseconds. */
37
+ portsRefreshMs: z<number, number, "defined">;
38
+ }>>, Schemastery.ObjectT<NoInfer<{
39
+ /** Distro these facts describe; empty means this machine's default distro. */
40
+ distro: z<string, string, "defined">;
41
+ /** How often the listening-port snapshot refreshes, in milliseconds. */
42
+ portsRefreshMs: z<number, number, "defined">;
43
+ }>>, "plain">;
44
+ /** The config schema's validated output. */
45
+ export interface ShellEnvConfig {
46
+ /** Distro these facts describe; empty means this machine's default distro. */
47
+ distro?: string;
48
+ /** How often the listening-port snapshot refreshes, in milliseconds. */
49
+ portsRefreshMs?: number;
50
+ }
51
+ /**
52
+ * The execution slice the shell-env registry hands a contributor's `resolve`;
53
+ * the harness's execution object is structural here.
54
+ */
55
+ export interface ShellEnvExecution {
56
+ /** The executing session's agent stub. */
57
+ agent?: {
58
+ session?: {
59
+ header?: {
60
+ cwd?: string | undefined;
61
+ } | undefined;
62
+ } | undefined;
63
+ } | undefined;
64
+ }
65
+ /**
66
+ * The cordis context extended with the `shellEnv` registry slice this plugin
67
+ * contributes through. The registry belongs to the harness and its
68
+ * declaration is not visible to this plugin, so it is spelled structurally on
69
+ * top of the framework's {@link Context}.
70
+ */
71
+ export type ShellEnvContext = Context & {
72
+ shellEnv: {
73
+ register: (contributor: {
74
+ name: string;
75
+ variables: Record<string, {
76
+ description: string;
77
+ }>;
78
+ resolve: (execution: ShellEnvExecution | undefined) => Record<string, string>;
79
+ }) => () => void;
80
+ };
81
+ };
82
+ /**
83
+ * Contribute the WSL environment's facts to the shell-env registry.
84
+ *
85
+ * @param ctx - the cordis context.
86
+ * @param config - the schema-validated plugin config.
87
+ */
88
+ export declare function apply(ctx: ShellEnvContext, config?: ShellEnvConfig): Promise<void>;
package/lib/shell-env.js CHANGED
@@ -20,90 +20,94 @@
20
20
  * directory. A session whose workspace is inside a distro is exactly a session
21
21
  * whose shell calls run in that distro — so a host session sees none of these.
22
22
  *
23
+ * This is a TypeScript source built to `lib/shell-env.js`; edit THIS file and
24
+ * run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
25
+ * fails when it drifts.
26
+ *
23
27
  * @module dsh-plugin-wsl/shell-env
24
28
  */
25
-
26
29
  import z from "@deepseek-ai/schemastery";
27
30
  import { isAnotherDistrosUnc, isWslUnc } from "./paths.js";
28
31
  import { defaultDistro, defaultShell, linuxHomePath } from "./wsl.js";
29
32
  import { sharedAgent } from "./agent-shared.js";
30
33
  import { listeningPorts } from "./ports.js";
31
-
32
34
  export const name = "wsl-shell-env";
33
-
34
35
  export const inject = ["shellEnv"];
35
-
36
36
  export const Config = z.object({
37
- /** Distro these facts describe; empty means this machine's default distro. */
38
- distro: z.string().default(""),
39
- /** How often the listening-port snapshot refreshes, in milliseconds. */
40
- portsRefreshMs: z.number().default(10 * 1000),
37
+ /** Distro these facts describe; empty means this machine's default distro. */
38
+ distro: z.string().default(""),
39
+ /** How often the listening-port snapshot refreshes, in milliseconds. */
40
+ portsRefreshMs: z.number().default(10 * 1000),
41
41
  });
42
-
42
+ /**
43
+ * Contribute the WSL environment's facts to the shell-env registry.
44
+ *
45
+ * @param ctx - the cordis context.
46
+ * @param config - the schema-validated plugin config.
47
+ */
43
48
  export async function apply(ctx, config = {}) {
44
- // `resolve` is called synchronously by the registry (`Object.entries` of its
45
- // result), so every fact has to be resolved up front, not per call.
46
- const configured = config.distro ?? "";
47
- const distro = configured.length > 0 ? configured : await defaultDistro();
48
- // undefined when both probes failed (a cold distro): the fact is then
49
- // simply absent, which beats presenting the "bash" placeholder as the
50
- // distro's login shell for this process's lifetime.
51
- const shell = await defaultShell(distro);
52
- // The consumer is a shell running INSIDE the distro, so the fact is the POSIX
53
- // path, not the host-side world path.
54
- const home = await linuxHomePath(distro);
55
-
56
- const variables = {
57
- DSH_WSL_DISTRO: {
58
- description: `Name of the WSL distro this workspace lives in (${distro}).`,
59
- },
60
- DSH_WSL_SHELL: {
61
- description: `Absolute path of the login shell that model shell calls run in inside ${distro}. Commands are executed by this shell, not by bash.`,
62
- },
63
- DSH_WSL_HOME: {
64
- description: `Home directory of the user model shell calls run as inside ${distro}.`,
65
- },
66
- DSH_WSL_PORTS: {
67
- description: `Ports with a listener inside ${distro} (comma-separated, ascending), as of the last snapshot — refreshed every ${Math.round(config.portsRefreshMs / 1000)}s, so a dev server started moments ago may not be listed yet. Empty means none were listening at the last scan.`,
68
- },
69
- };
70
-
71
- // The port snapshot refreshes on a timer and is served synchronously: the
72
- // registry's resolve is sync by contract, so live reads are impossible —
73
- // the timer bounds staleness instead (see lib/ports.js).
74
- let ports = [];
75
- const refreshPorts = async () => {
76
- ports = await listeningPorts(sharedAgent(distro), 8000);
77
- };
78
- void refreshPorts();
79
- const portsTimer = setInterval(() => void refreshPorts(), config.portsRefreshMs);
80
- portsTimer.unref?.();
81
-
82
- const dispose = ctx.shellEnv.register({
83
- name: "dsh-plugin-wsl-env",
84
- variables,
85
- resolve: (execution) => {
86
- const cwd = execution?.agent?.session?.header?.cwd;
87
- if (!isWslUnc(cwd)) return {};
88
- // The facts describe ONE distro. A workspace under another distro's UNC
89
- // is not that distro — the shell refuses its commands outright (the
90
- // cross-distro guard) — so the honest resolution is no facts at all,
91
- // not another distro's home and shell presented as this session's.
92
- if (isAnotherDistrosUnc(cwd, distro)) return {};
93
- const facts = {
94
- DSH_WSL_DISTRO: distro,
95
- DSH_WSL_HOME: home,
96
- DSH_WSL_PORTS: ports.join(","),
97
- };
98
- if (shell !== undefined) facts.DSH_WSL_SHELL = shell;
99
- return facts;
100
- },
101
- });
102
-
103
- // Disposing matters: a config hot-reload re-runs apply(), and the registry
104
- // rejects a second contributor claiming the same keys.
105
- ctx.effect(() => {
106
- clearInterval(portsTimer);
107
- dispose();
108
- });
49
+ // `resolve` is called synchronously by the registry (`Object.entries` of its
50
+ // result), so every fact has to be resolved up front, not per call.
51
+ const configured = config.distro ?? "";
52
+ const distro = configured.length > 0 ? configured : await defaultDistro();
53
+ // undefined when both probes failed (a cold distro): the fact is then
54
+ // simply absent, which beats presenting the "bash" placeholder as the
55
+ // distro's login shell for this process's lifetime.
56
+ const shell = await defaultShell(distro);
57
+ // The consumer is a shell running INSIDE the distro, so the fact is the POSIX
58
+ // path, not the host-side world path.
59
+ const home = await linuxHomePath(distro);
60
+ const variables = {
61
+ DSH_WSL_DISTRO: {
62
+ description: `Name of the WSL distro this workspace lives in (${distro}).`,
63
+ },
64
+ DSH_WSL_SHELL: {
65
+ description: `Absolute path of the login shell that model shell calls run in inside ${distro}. Commands are executed by this shell, not by bash.`,
66
+ },
67
+ DSH_WSL_HOME: {
68
+ description: `Home directory of the user model shell calls run as inside ${distro}.`,
69
+ },
70
+ DSH_WSL_PORTS: {
71
+ description: `Ports with a listener inside ${distro} (comma-separated, ascending), as of the last snapshot — refreshed every ${Math.round(config.portsRefreshMs / 1000)}s, so a dev server started moments ago may not be listed yet. Empty means none were listening at the last scan.`,
72
+ },
73
+ };
74
+ // The port snapshot refreshes on a timer and is served synchronously: the
75
+ // registry's resolve is sync by contract, so live reads are impossible —
76
+ // the timer bounds staleness instead (see lib/ports.js).
77
+ let ports = [];
78
+ const refreshPorts = async () => {
79
+ ports = await listeningPorts(sharedAgent(distro), 8000);
80
+ };
81
+ void refreshPorts();
82
+ const portsTimer = setInterval(() => void refreshPorts(), config.portsRefreshMs);
83
+ portsTimer.unref?.();
84
+ const dispose = ctx.shellEnv.register({
85
+ name: "dsh-plugin-wsl-env",
86
+ variables,
87
+ resolve: (execution) => {
88
+ const cwd = execution?.agent?.session?.header?.cwd;
89
+ if (cwd === undefined || !isWslUnc(cwd))
90
+ return {};
91
+ // The facts describe ONE distro. A workspace under another distro's UNC
92
+ // is not that distro — the shell refuses its commands outright (the
93
+ // cross-distro guard) — so the honest resolution is no facts at all,
94
+ // not another distro's home and shell presented as this session's.
95
+ if (isAnotherDistrosUnc(cwd, distro))
96
+ return {};
97
+ const facts = {
98
+ DSH_WSL_DISTRO: distro,
99
+ DSH_WSL_HOME: home,
100
+ DSH_WSL_PORTS: ports.join(","),
101
+ };
102
+ if (shell !== undefined)
103
+ facts.DSH_WSL_SHELL = shell;
104
+ return facts;
105
+ },
106
+ });
107
+ // Disposing matters: a config hot-reload re-runs apply(), and the registry
108
+ // rejects a second contributor claiming the same keys.
109
+ ctx.effect(() => () => {
110
+ clearInterval(portsTimer);
111
+ dispose();
112
+ });
109
113
  }
package/lib/shell.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `ctx.shell` entrypoint — load as `dsh-plugin-wsl` in a profile patch.
3
+ *
4
+ * This is a TypeScript source built to `lib/shell.js`; edit THIS file and run
5
+ * `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
6
+ * fails when it drifts.
7
+ *
8
+ * @module dsh-plugin-wsl/shell
9
+ */
10
+ export { WslShellExecutor as default, WslShellExecutor } from "./index.js";
package/lib/shell.js CHANGED
@@ -1,5 +1,10 @@
1
1
  /**
2
2
  * `ctx.shell` entrypoint — load as `dsh-plugin-wsl` in a profile patch.
3
+ *
4
+ * This is a TypeScript source built to `lib/shell.js`; edit THIS file and run
5
+ * `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
6
+ * fails when it drifts.
7
+ *
3
8
  * @module dsh-plugin-wsl/shell
4
9
  */
5
10
  export { WslShellExecutor as default, WslShellExecutor } from "./index.js";