dsh-plugin-wsl-env 0.8.0 → 0.10.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 (54) hide show
  1. package/CHANGELOG.md +488 -12
  2. package/CONTRIBUTING.md +125 -62
  3. package/README.md +99 -68
  4. package/SECURITY.md +18 -2
  5. package/SUPPORT.md +26 -10
  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 +83 -16
  9. package/cordis.patch.yml +50 -35
  10. package/docs/ARCHITECTURE.md +203 -55
  11. package/docs/CONFIGURATION.md +185 -18
  12. package/docs/LIMITATIONS.md +125 -45
  13. package/docs/PARITY.md +68 -38
  14. package/docs/PEER-PARITY.md +103 -33
  15. package/docs/README.zh.md +105 -64
  16. package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
  17. package/docs/UPSTREAM-TERMINAL-TITLE.md +56 -14
  18. package/examples/preset-wsl-session-sandbox.cordis.patch.yml +178 -0
  19. package/lib/agent-protocol.d.ts +10 -4
  20. package/lib/agent-protocol.js +10 -3
  21. package/lib/agent.d.ts +84 -0
  22. package/lib/agent.js +167 -15
  23. package/lib/bwrap.d.ts +8 -0
  24. package/lib/bwrap.js +4 -1
  25. package/lib/fs-routing.d.ts +40 -4
  26. package/lib/fs-routing.js +80 -12
  27. package/lib/fsio-agent.js +1 -1
  28. package/lib/index.d.ts +134 -0
  29. package/lib/index.js +202 -9
  30. package/lib/native-agent.d.ts +124 -0
  31. package/lib/native-agent.js +198 -0
  32. package/lib/ports.d.ts +13 -2
  33. package/lib/ports.js +18 -5
  34. package/lib/preflight.d.ts +91 -0
  35. package/lib/preflight.js +117 -0
  36. package/lib/probe-cache.d.ts +12 -1
  37. package/lib/probe-cache.js +11 -3
  38. package/lib/sandbox-core.d.ts +70 -0
  39. package/lib/sandbox-core.js +187 -18
  40. package/lib/session-runtime.d.ts +161 -0
  41. package/lib/session-runtime.js +220 -0
  42. package/lib/session-scope.d.ts +124 -0
  43. package/lib/session-scope.js +191 -0
  44. package/lib/shell-env.js +10 -3
  45. package/lib/watcher.d.ts +25 -12
  46. package/lib/watcher.js +120 -32
  47. package/lib/workspace-files-route.d.ts +8 -4
  48. package/lib/workspace-files-route.js +42 -14
  49. package/lib/workspace-files-wsl.js +25 -8
  50. package/lib/wsl.js +10 -4
  51. package/locale/preflight/en.json +6 -0
  52. package/locale/preflight/zh.json +6 -0
  53. package/package.json +44 -27
  54. package/scripts/diagnose.sh +92 -0
@@ -162,11 +162,13 @@ export async function distroStat({ runner, distro, linuxRoot, path, distroWorksp
162
162
  const linuxFile = linuxJoin(linuxRoot, path);
163
163
  const result = await runner.exec({
164
164
  cwd: "/",
165
- argv: statRecordArgv(linuxFile),
165
+ argv: statRecordArgv(linuxFile, linuxRoot),
166
166
  maxOutputBytes: 4096,
167
167
  timeoutMs: 30_000,
168
168
  signal,
169
169
  });
170
+ if (result.exitCode === 3)
171
+ throw outsideWorkspace(path);
170
172
  if (result.exitCode !== 0)
171
173
  throw notFound(path);
172
174
  const record = parseStatRecord(result.stdout);
@@ -186,11 +188,13 @@ export async function distroRead({ runner, distro, linuxRoot, path, offset, limi
186
188
  const linuxFile = linuxJoin(linuxRoot, path);
187
189
  const result = await runner.exec({
188
190
  cwd: "/",
189
- argv: pageArgv(linuxFile, offset, limit),
191
+ argv: pageArgv(linuxFile, offset, limit, linuxRoot),
190
192
  maxOutputBytes: (maxBytes + 1) * 4,
191
193
  timeoutMs: 60_000,
192
194
  signal,
193
195
  });
196
+ if (result.exitCode === 3)
197
+ throw outsideWorkspace(path);
194
198
  if (result.exitCode === 2)
195
199
  throw notFound(path);
196
200
  if (result.exitCode !== 0 && result.exitCode !== 1) {
@@ -213,16 +217,27 @@ export async function distroRead({ runner, distro, linuxRoot, path, offset, limi
213
217
  catch {
214
218
  throw notText(path);
215
219
  }
216
- const lines = text.length === 0 ? 0 : text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
217
- const bytes = Buffer.byteLength(text, "utf8") + lines;
220
+ // `pageArgv` reads one line past the page so EOF is decidable from the
221
+ // yield; that line is a probe, not part of the answer. Split it off before
222
+ // counting, and count bytes on the page the caller actually receives (a
223
+ // CRLF file's line count is not a byte budget).
224
+ const parts = text.length === 0 ? [] : text.split("\n");
225
+ if (text.endsWith("\n"))
226
+ parts.pop();
227
+ const count = parts.length;
228
+ const truncated = count > limit;
229
+ const kept = truncated ? parts.slice(0, limit) : parts;
230
+ const pageText = truncated ? `${kept.join("\n")}\n` : text;
231
+ const lines = kept.length;
232
+ const bytes = Buffer.byteLength(pageText, "utf8");
218
233
  if (bytes > maxBytes) {
219
234
  throw new RemoteError("workspace-file/too-large", `lines ${offset}-${offset + limit - 1} of "${path}" exceed the ${maxBytes} byte cap`, { path, limit: maxBytes });
220
235
  }
221
- if (text.includes(String.fromCharCode(0)))
236
+ if (pageText.includes(String.fromCharCode(0)))
222
237
  throw notText(path);
223
- const eof = lines < limit;
238
+ const eof = !truncated;
224
239
  const stat = { absolutePath: posixToUnc(distro, linuxFile), version: record.version };
225
- return { ...stat, offset, text, lines, eof };
240
+ return { ...stat, offset, text: pageText, lines, eof };
226
241
  }
227
242
  /**
228
243
  * Read one byte window (or the capped whole file) through the runner.
@@ -248,11 +263,13 @@ export async function distroReadBytes({ runner, distro, linuxRoot, path, offset,
248
263
  throw windowTooLarge(path, window.length, maxBytes);
249
264
  const result = await runner.exec({
250
265
  cwd: "/",
251
- argv: bytesArgv(linuxFile, window.offset, window.length),
266
+ argv: bytesArgv(linuxFile, window.offset, window.length, linuxRoot),
252
267
  maxOutputBytes: window.length + 1024,
253
268
  timeoutMs: 60_000,
254
269
  signal,
255
270
  });
271
+ if (result.exitCode === 3)
272
+ throw outsideWorkspace(path);
256
273
  const record = parseStatRecordFromStderr(result.stderr);
257
274
  if (record === undefined)
258
275
  throw notFound(path);
package/lib/wsl.js CHANGED
@@ -104,8 +104,11 @@ export function parseDistroList(stdout) {
104
104
  */
105
105
  export async function defaultDistro(options = {}) {
106
106
  const [first] = await listDistros(options);
107
- if (first === undefined)
108
- throw new Error("dsh-plugin-wsl: no WSL distro is installed (`wsl.exe -l -q` returned nothing)");
107
+ if (first === undefined) {
108
+ throw new Error("dsh-plugin-wsl: no WSL distro is installed (`wsl.exe -l -q` returned nothing). " +
109
+ "Install one: `wsl.exe --list --online` lists the available names, then `wsl --install -d <name>`, " +
110
+ "or point this row's `distro` at an existing one.");
111
+ }
109
112
  return first;
110
113
  }
111
114
  /**
@@ -134,7 +137,8 @@ export async function linuxHomePath(distro, options = {}) {
134
137
  export function parseHomePath(distro, stdout) {
135
138
  const home = String(stdout).trim();
136
139
  if (home.length === 0 || !home.startsWith("/")) {
137
- throw new Error(`dsh-plugin-wsl: distro "${distro}" reported no usable $HOME (got "${home}")`);
140
+ throw new Error(`dsh-plugin-wsl: distro "${distro}" reported no usable $HOME (got "${home}"). ` +
141
+ `Check the distro's default user in /etc/wsl.conf, and that the user's /etc/passwd entry carries a home directory.`);
138
142
  }
139
143
  return home;
140
144
  }
@@ -337,7 +341,9 @@ export async function defaultShell(distro, options = {}) {
337
341
  export async function runInDistro(distro, command, options = {}) {
338
342
  const shell = options.shell ?? (await defaultShell(distro, options));
339
343
  if (shell === undefined) {
340
- throw new Error(`dsh-plugin-wsl: distro "${distro}" named no usable shell (passwd probe and $SHELL both failed)`);
344
+ throw new Error(`dsh-plugin-wsl: distro "${distro}" named no usable shell (passwd probe and $SHELL both failed). ` +
345
+ `Check the user's login shell in /etc/passwd (\`getent passwd "$(id -u)"\` inside the distro), ` +
346
+ `or point this row's \`shell\` at an existing absolute path.`);
341
347
  }
342
348
  return runCapture([options.wslPath ?? DEFAULT_WSL_PATH, "-d", distro, "--exec", shell, ...shellArgs(shell, options.loginShell !== false), command], options.signal);
343
349
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "WSL sandbox preflight",
4
+ "description": "Probes bubblewrap when a distro session opens and surfaces a missing or unusable bwrap as a warning with the exact remedy — instead of the first command's fail-closed error being the discovery moment. Advisory only; never installs anything."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "WSL 沙箱预检",
4
+ "description": "发行版会话打开时探测 bubblewrap,把缺失或不可用的 bwrap 连同确切的修复指引作为警告提前给出——而不是让第一条命令的 fail-closed 报错充当发现时刻。纯建议性,绝不安装任何东西。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-plugin-wsl-env",
3
- "version": "0.8.0",
4
- "description": "让 DeepSeek Harness 以 WSL 子系统作为会话的执行环境:shell、文件读写与命令执行均在子系统内完成,目录选择器与 GUI 终端同样定位到子系统路径,并通过子系统内的 bubblewrap 对命令进行沙箱隔离",
3
+ "version": "0.10.0",
4
+ "description": "\u8ba9 DeepSeek Harness \u4ee5 WSL \u5b50\u7cfb\u7edf\u4f5c\u4e3a\u4f1a\u8bdd\u7684\u6267\u884c\u73af\u5883\uff1ashell\u3001\u6587\u4ef6\u8bfb\u5199\u4e0e\u547d\u4ee4\u6267\u884c\u5747\u5728\u5b50\u7cfb\u7edf\u5185\u5b8c\u6210\uff0c\u76ee\u5f55\u9009\u62e9\u5668\u4e0e GUI \u7ec8\u7aef\u540c\u6837\u5b9a\u4f4d\u5230\u5b50\u7cfb\u7edf\u8def\u5f84\uff0c\u5e76\u901a\u8fc7\u5b50\u7cfb\u7edf\u5185\u7684 bubblewrap \u5bf9\u547d\u4ee4\u8fdb\u884c\u6c99\u7bb1\u9694\u79bb",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -47,12 +47,18 @@
47
47
  "types": "./lib/paths.d.ts",
48
48
  "default": "./lib/paths.js"
49
49
  },
50
+ "./preflight": {
51
+ "types": "./lib/preflight.d.ts",
52
+ "default": "./lib/preflight.js"
53
+ },
50
54
  "./picker": {
51
55
  "types": "./lib/picker.d.ts",
52
56
  "default": "./lib/picker.js"
53
57
  },
54
58
  "./picker/locale/en.json": "./locale/picker/en.json",
55
59
  "./picker/locale/zh.json": "./locale/picker/zh.json",
60
+ "./preflight/locale/en.json": "./locale/preflight/en.json",
61
+ "./preflight/locale/zh.json": "./locale/preflight/zh.json",
56
62
  "./shell": {
57
63
  "types": "./lib/shell.d.ts",
58
64
  "default": "./lib/shell.js"
@@ -87,6 +93,7 @@
87
93
  "lib/**/*.js",
88
94
  "lib/**/*.d.ts",
89
95
  "agent/*.sh",
96
+ "agent/native/linux-*/dsh-wsl-agent",
90
97
  "locale/**/*.json",
91
98
  "cordis.patch.yml",
92
99
  "examples/**/*.yml",
@@ -104,16 +111,17 @@
104
111
  "SECURITY.md",
105
112
  "SUPPORT.md",
106
113
  "LICENSE",
107
- "scripts/bootstrap.sh"
114
+ "scripts/bootstrap.sh",
115
+ "scripts/diagnose.sh"
108
116
  ],
109
117
  "scripts": {
110
118
  "test": "pnpm run lint:style && pnpm run test:syntax && pnpm run lint:build && pnpm run lint:types && pnpm run test:unit",
111
119
  "lint:types": "tsc --noEmit",
112
120
  "build": "node scripts/build.mjs",
113
121
  "lint:build": "node test/build-freshness.mjs",
114
- "test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 test/agent-exec.test.mjs test/agent-fs-script.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/fs-substrate.test.mjs test/fs.boot.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/fsio-agent.test.mjs test/fsio-text.test.mjs test/git-route.test.mjs test/listing.test.mjs test/paths.test.mjs test/ports.test.mjs test/preset-choice.test.mjs test/probe-cache.test.mjs test/sandbox.test.mjs test/search-exec.test.mjs test/search-route.test.mjs test/fs.boot.test.mjs test/shell.test.mjs test/terminal-activity.test.mjs test/terminal.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs",
122
+ "test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 test/agent-exec.test.mjs test/agent-fs-script.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/fs-substrate.test.mjs test/fs.boot.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/fsio-agent.test.mjs test/fsio-text.test.mjs test/git-route.test.mjs test/listing.test.mjs test/paths.test.mjs test/ports.test.mjs test/preset-choice.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/sandbox.test.mjs test/search-exec.test.mjs test/search-route.test.mjs test/shell.test.mjs test/terminal-activity.test.mjs test/terminal.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs",
115
123
  "test:syntax": "node test/syntax.mjs",
116
- "test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preset-choice.test.mjs test/sandbox.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/agent-fs-script.test.mjs test/fsio-agent.test.mjs test/fs-substrate.test.mjs test/agent-exec.test.mjs test/ports.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/git-route.test.mjs test/fsio-text.test.mjs test/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs",
124
+ "test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/preset-choice.test.mjs test/sandbox.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/agent-fs-script.test.mjs test/fsio-agent.test.mjs test/fs-substrate.test.mjs test/agent-exec.test.mjs test/ports.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/git-route.test.mjs test/fsio-text.test.mjs test/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs",
117
125
  "lint:style": "node test/style.mjs",
118
126
  "prepublishOnly": "pnpm test",
119
127
  "probe": "bash test/probe/run.sh",
@@ -129,7 +137,8 @@
129
137
  "probe:agent": "bash test/probe/agent.sh",
130
138
  "probe:substrate": "bash test/probe/substrate.sh",
131
139
  "probe:exec": "bash test/probe/exec.sh",
132
- "bootstrap": "bash scripts/bootstrap.sh"
140
+ "bootstrap": "bash scripts/bootstrap.sh",
141
+ "diagnose": "bash scripts/diagnose.sh"
133
142
  },
134
143
  "packageManager": "pnpm@11.7.0",
135
144
  "engines": {
@@ -153,40 +162,45 @@
153
162
  "url": "https://github.com/Big-Dao/dsh-plugin-wsl-env/issues"
154
163
  },
155
164
  "devDependencies": {
156
- "@deepseek-ai/cordis": "4.0.4",
165
+ "@deepseek-ai/cordis": "4.0.5",
157
166
  "@deepseek-ai/cordis-plugin-include": "*",
158
167
  "@deepseek-ai/cordis-plugin-loader": "*",
159
- "@deepseek-ai/dsh-api-workspace-files": "0.2.0-rc.2",
160
- "@deepseek-ai/dsh-bash-local": "0.2.0-rc.2",
161
- "@deepseek-ai/dsh-fs": "0.2.0-rc.2",
162
- "@deepseek-ai/dsh-fs-local": "0.2.0-rc.2",
163
- "@deepseek-ai/dsh-fs-sandbox": "0.2.0-rc.2",
164
- "@deepseek-ai/dsh-host-directory-picker": "0.2.0-rc.2",
165
- "@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
166
- "@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
167
- "@deepseek-ai/dsh-subprocess": "0.2.0-rc.2",
168
- "@deepseek-ai/dsh-subprocess-local": "0.2.0-rc.2",
169
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
168
+ "@deepseek-ai/dsh-api-workspace-files": "0.2.1-rc.2",
169
+ "@deepseek-ai/dsh-bash-local": "0.2.1-rc.2",
170
+ "@deepseek-ai/dsh-fs": "0.2.1-rc.2",
171
+ "@deepseek-ai/dsh-fs-local": "0.2.1-rc.2",
172
+ "@deepseek-ai/dsh-fs-sandbox": "0.2.1-rc.2",
173
+ "@deepseek-ai/dsh-host-directory-picker": "0.2.1-rc.2",
174
+ "@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
175
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.2",
176
+ "@deepseek-ai/dsh-subprocess": "0.2.1-rc.2",
177
+ "@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.2",
178
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2",
170
179
  "@types/js-yaml": "^4.0.9",
171
180
  "@types/node": "~22.20.5",
172
181
  "js-yaml": "4.3.2",
173
182
  "typescript": "~6.0.3"
174
183
  },
175
184
  "peerDependencies": {
176
- "@deepseek-ai/cordis": "~4.0.4",
177
- "@deepseek-ai/dsh-bash-local": "0.2.0-rc.2",
178
- "@deepseek-ai/dsh-fs": "0.2.0-rc.2",
179
- "@deepseek-ai/dsh-fs-local": "0.2.0-rc.2",
180
- "@deepseek-ai/dsh-host-directory-picker": "0.2.0-rc.2",
181
- "@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
182
- "@deepseek-ai/dsh-subprocess": "0.2.0-rc.2",
183
- "@deepseek-ai/dsh-subprocess-local": "0.2.0-rc.2",
184
- "@deepseek-ai/schemastery": "~3.18.4"
185
+ "@deepseek-ai/cordis": "~4.0.5",
186
+ "@deepseek-ai/dsh-api-workspace-files": "0.2.1-rc.2",
187
+ "@deepseek-ai/dsh-bash-local": "0.2.1-rc.2",
188
+ "@deepseek-ai/dsh-fs": "0.2.1-rc.2",
189
+ "@deepseek-ai/dsh-fs-local": "0.2.1-rc.2",
190
+ "@deepseek-ai/dsh-host-directory-picker": "0.2.1-rc.2",
191
+ "@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
192
+ "@deepseek-ai/dsh-subprocess": "0.2.1-rc.2",
193
+ "@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.2",
194
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2",
195
+ "@deepseek-ai/schemastery": "~3.18.5"
185
196
  },
186
197
  "peerDependenciesMeta": {
187
198
  "@deepseek-ai/cordis": {
188
199
  "optional": true
189
200
  },
201
+ "@deepseek-ai/dsh-api-workspace-files": {
202
+ "optional": true
203
+ },
190
204
  "@deepseek-ai/dsh-bash-local": {
191
205
  "optional": true
192
206
  },
@@ -208,6 +222,9 @@
208
222
  "@deepseek-ai/dsh-subprocess-local": {
209
223
  "optional": true
210
224
  },
225
+ "@deepseek-ai/dsh-typert-protocol": {
226
+ "optional": true
227
+ },
211
228
  "@deepseek-ai/schemastery": {
212
229
  "optional": true
213
230
  }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bash
2
+ # Collect a read-only diagnostic report for a bug report, in one command:
3
+ #
4
+ # pnpm run diagnose -- [distro] # distro optional; default distro otherwise
5
+ #
6
+ # Prints versions and probe verdicts as plain text that can be pasted into an
7
+ # issue as-is. It runs NO installation, changes nothing in the distro, and reads
8
+ # nothing secret: the report is tool versions, a kernel release and probe
9
+ # verdicts. Exit code is always 0 — the report is informational; the verdict
10
+ # lines carry the failures.
11
+ #
12
+ # Deeper sandbox probing lives in `npm run probe:sandbox` (run inside the
13
+ # distro); this script answers "what is installed and does bwrap run at all".
14
+
15
+ set -u
16
+ export WSL_UTF8=1 # wsl.exe writes UTF-16LE without it; read as UTF-8 that is mojibake
17
+
18
+ DISTRO="${1:-}"
19
+
20
+ ok() { printf 'ok %s\n' "$1"; }
21
+ miss() { printf 'MISSING %s\n' "$1"; }
22
+ info() { printf 'info %s\n' "$1"; }
23
+ header(){ printf '\n== %s ==\n' "$1"; }
24
+
25
+ echo "# dsh-plugin-wsl-env diagnose report"
26
+
27
+ header "host"
28
+ winver="$(cmd.exe /c ver 2>/dev/null | tr -d '\r' | grep -o '[0-9][0-9.]*' | head -1)"
29
+ if [ -n "$winver" ]; then ok "windows build $winver"; else info "windows build unknown (cmd.exe not reachable through interop)"; fi
30
+ wsl_ver="$(wsl.exe --version 2>/dev/null | tr -d '\0\r' | head -2 | tr '\n' ' ')"
31
+ if [ -n "$wsl_ver" ]; then ok "wsl: $wsl_ver"; else info "wsl --version unsupported on this WSL build"; fi
32
+ wsl_status="$(wsl.exe --status 2>/dev/null | tr -d '\0\r' | tr '\n' ' ')"
33
+ [ -n "$wsl_status" ] && info "wsl status: $wsl_status"
34
+ wsl.exe -l -v 2>/dev/null | tr -d '\0\r' | grep -v '^$' | sed 's/^/ /'
35
+
36
+ header "plugin"
37
+ echo " plugin version: $(node -p "require('./package.json').version" 2>/dev/null || echo unknown)"
38
+ echo " dsh version: $(dsh --version 2>/dev/null || echo unknown)"
39
+
40
+ if [ -z "$DISTRO" ]; then
41
+ DISTRO="$(wsl.exe -l -q 2>/dev/null | tr -d '\0\r' | grep -v '^$' | head -1)"
42
+ fi
43
+ if [ -z "$DISTRO" ]; then
44
+ header "distro"
45
+ miss "no distro installed — 'wsl.exe --list --online' lists names, 'wsl --install -d <name>' installs one"
46
+ exit 0
47
+ fi
48
+
49
+ header "distro: $DISTRO"
50
+ in_distro() { wsl.exe -d "$DISTRO" --exec sh -c "$1" 2>/dev/null; }
51
+
52
+ info "kernel: $(in_distro 'uname -r')"
53
+ family="$(in_distro 'for m in apt-get dnf pacman zypper; do command -v $m && break; done' | tr -d '\r')"
54
+ case "$family" in
55
+ *apt-get) info "package family: apt" ;;
56
+ *dnf) info "package family: dnf" ;;
57
+ *pacman) info "package family: pacman" ;;
58
+ *zypper) info "package family: zypper" ;;
59
+ *) info "package family: unknown" ;;
60
+ esac
61
+
62
+ for tool in bwrap rg git inotifywait; do
63
+ if in_distro "command -v $tool" | grep -q .; then
64
+ case "$tool" in
65
+ bwrap) ver="$(in_distro 'bwrap --version' | tr -d '\r' | head -1)" ;;
66
+ rg) ver="$(in_distro 'rg --version' | tr -d '\r' | head -1)" ;;
67
+ git) ver="$(in_distro 'git --version' | tr -d '\r' | head -1)" ;;
68
+ inotifywait) ver="inotifywait (no version flag)" ;;
69
+ esac
70
+ ok "$ver"
71
+ else
72
+ case "$tool" in
73
+ bwrap) miss "bwrap — REQUIRED; every confined command fails closed without it. Install: wsl.exe -d $DISTRO -u root -- apt-get install -y bubblewrap (or: pnpm run bootstrap -- $DISTRO --install)" ;;
74
+ rg) miss "ripgrep — the search tool falls back to slower matching without it" ;;
75
+ git) miss "git — snapshot and diff baselines degrade without it" ;;
76
+ inotifywait) miss "inotify-tools — file watching falls back to polling without it" ;;
77
+ esac
78
+ fi
79
+ done
80
+
81
+ if in_distro 'command -v bwrap' | grep -q .; then
82
+ if in_distro 'bwrap --ro-bind / / -- true'; then
83
+ ok "bwrap runs a read-only profile (the usability probe passes)"
84
+ else
85
+ miss "bwrap is installed but FAILED the read-only profile probe — likely the kernel (unprivileged user namespaces disabled) or an unusable build; reinstalling will not help"
86
+ fi
87
+ fi
88
+
89
+ header "notes"
90
+ info "trace file: set DSH_WSL_TRACE=<path> to record the auto-preset decisions, then re-run the scenario"
91
+ info "paste this report into the issue's Output section; redact anything you would rather not publish"
92
+ exit 0