dsh-wsl-tool 1.10.11 → 1.11.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/PUBLISHING.md CHANGED
@@ -78,7 +78,7 @@ both that the entry stays relative and that it resolves.
78
78
  It must print `function` and the resolved defaults. If it prints `undefined`,
79
79
  the plugin declares no `Config`, the platform has no schema to project, the
80
80
  entry's config status stays `absent` and the sidebar panel renders without a
81
- single switch while all three tools keep working. The fragile step is the
81
+ single switch while every tool keeps working. The fragile step is the
82
82
  interop hop: `@deepseek-ai/schemastery` exports its builder as the **default**
83
83
  export, so `const { Schema } = await import(...)` silently yields `undefined`
84
84
  (see `lib/schema.js`, and the regression test that pins the picking).
@@ -117,6 +117,22 @@ both that the entry stays relative and that it resolves.
117
117
  artifact, not just the metadata. Scripted requests to `npmjs.com`'s HTML hit a
118
118
  Cloudflare challenge; the registry API is the source of truth.
119
119
 
120
+ Two things make a published release look unpublished, so rule them out before
121
+ believing a negative:
122
+
123
+ - **A green run is not proof that npm was published.** The `Publish to npm`
124
+ step exits 0 with a warning when `NPM_TOKEN` is absent, so the run stays
125
+ green while npm is skipped. Ask the jobs API whether the step really ran —
126
+ a real run has `started_at`; a skipped one has `conclusion: skipped` and no
127
+ timestamp — and ask the registry what actually handles the version
128
+ (`time[<version>]` on the packument).
129
+
130
+ - **A failing `npm whoami` says nothing about the release.** A stale token in
131
+ `~/.npmrc` only means that machine cannot publish by hand; CI has its own
132
+ credential. Release state lives in the three artifacts — the tag's commit,
133
+ the release asset, and the registry tarball — so compare their bytes rather
134
+ than your credentials.
135
+
120
136
  Expect the registry's caches to lag a publish by minutes, and to lag
121
137
  *inconsistently*: the full packument, the abbreviated (install) packument,
122
138
  `/-/package/<name>/dist-tags` and the tarball URL each cache separately, so one
@@ -171,24 +187,43 @@ consequences worth remembering:
171
187
  hand-written keys are `url`, `name`, `category`, `tarball` and `description`;
172
188
  everything else (including `screenshots`) is generated and must not be written by
173
189
  hand. Only the description needs a PR against `awesome-dsh-plugin`.
174
- - **Replacing a file in place refreshes the preview at the same URL**, which is why
175
- `assets/screenshot-1.png` keeps its name across redesigns. GitHub's raw endpoint
176
- caches, so a stale image can survive a few minutes to a day.
177
-
178
- The current preview is rendered from `assets/market-preview.html` (the editable
179
- source of truth) with headless Edge:
190
+ - **Replacing a file in place does NOT refresh the preview.** Two caches are keyed on
191
+ the exact URL string and neither revalidates the bytes: dsh-market loads images
192
+ through the `images.weserv.nl` proxy (`?url=<the raw URL>&h=<height>&we=1` — see its
193
+ `client/client.js`), and the DSH desktop webview keeps its own HTTP disk cache under
194
+ `%APPDATA%\@deepseek-ai\dsh-desktop\Cache\Cache_Data`. A redesign committed over the
195
+ same path therefore keeps serving the previous image for as long as those copies
196
+ live. The 2026-10-05 preview did exactly that: the mode-picker screenshot (with
197
+ `WSL Dev` ticked) was replaced in place at `assets/screenshot-1.png`, and the listing
198
+ went on showing it. **Ship a preview under a new filename** — add the file, point
199
+ `screenshots.json` and the README embed at it, and retire the old path.
200
+ Retiring it costs one day of transition: the catalog's `screenshots` array is
201
+ generated from this repository and published about once a day, so a client still
202
+ holding the previous catalog requests the old URL after the file is gone and finds
203
+ nothing. Keep the old file until the next catalog publish when that URL is worth
204
+ keeping alive; delete it in the same change when the point is to retire the URL
205
+ (as here), accepting the empty slot until the rebuild.
206
+
207
+ The preview is rendered from `assets/market-preview.html` (the editable source of
208
+ truth) with headless Edge, under a **new filename each redesign** — the name is what
209
+ defeats the caches, so the render target changes with the design:
180
210
 
181
211
  ```powershell
212
+ $out = Join-Path $PWD 'assets\market-preview-<new>.png'
213
+ $url = 'file:///' + ($PWD.Path -replace '\\','/') + '/assets/market-preview.html'
182
214
  & 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe' --headless=new `
183
215
  --disable-gpu --hide-scrollbars --force-device-scale-factor=2 `
184
- --window-size=1280,800 --virtual-time-budget=3000 `
185
- --user-data-dir="$env:TEMP\edge-shot" `
186
- --screenshot="assets\screenshot-1.png" "file:///$PWD/assets/market-preview.html"
216
+ --window-size=1280,800 --virtual-time-budget=4000 `
217
+ --user-data-dir="$env:TEMP\edge-shot" --screenshot="$out" $url
187
218
  ```
188
219
 
189
- It renders 2560×1600 (1280×800 at 2×) in a few seconds with no browser window. The
190
- `--user-data-dir` keeps it away from the user's real Edge profile. Edit the HTML,
191
- re-run, and commit the PNG: no build step, no design tool.
220
+ Two things the renderer demands: `--screenshot` must be an **absolute** path (Edge
221
+ resolves it against its own working directory, and a relative one fails with
222
+ "Failed to write file … 系统找不到指定的路径"), and `--headless=new` needs a moment
223
+ before the file appears. It renders 2560×1600 (1280×800 at 2×) in a few seconds with
224
+ no browser window; `--user-data-dir` keeps it away from the user's real Edge profile.
225
+ Edit the HTML, re-run, commit the PNG, and point `screenshots.json` plus the README
226
+ embed at the new name: no build step, no design tool.
192
227
 
193
228
  ## Things that bite
194
229
 
package/README.md CHANGED
@@ -15,17 +15,20 @@ project kept on a Windows drive keeps **Windows** filesystem semantics — no PO
15
15
  permissions, case-insensitive names, no file-change notifications — at a fraction
16
16
  of the speed (see [Notes](#notes)).
17
17
 
18
- ![The three tools, the left-sidebar switch panel, and the two limits](assets/market-preview.png)
18
+ ![The feature list and the left-sidebar switch panel](assets/market-preview-features-3.png)
19
19
 
20
20
  ## Tools
21
21
 
22
- The plugin registers three tools:
22
+ The plugin registers five tools (the last two can be switched off independently,
23
+ and `wsl-bootstrap` is OFF by default):
23
24
 
24
25
  | Tool | Purpose |
25
26
  |---|---|
26
27
  | `wsl` | Run a Linux command and return `stdout`/`stderr` with exit-code, timeout and truncation markers. |
27
28
  | `wsl-path` | Convert between Windows and WSL paths via `wslpath`. |
28
29
  | `wsl-env` | Summarize the WSL environment (distros, kernel, cpu, mem, disk). |
30
+ | `wsl-doctor` | Compare what a project needs with what the distribution has — including commands that are really Windows binaries. Read-only. |
31
+ | `wsl-bootstrap` | Install the missing toolchain inside the distribution, from a fixed recipe list, plan first. |
29
32
 
30
33
  ### `wsl`
31
34
 
@@ -90,6 +93,96 @@ unknown distro is an error rather than a partial answer. Note that `wsl --versio
90
93
  is **localized**, so its labels are passed through as the launcher printed them
91
94
  rather than parsed by name; the Direct3D/MSRDC/DXCore versions are omitted.
92
95
 
96
+ ### `wsl-doctor`
97
+
98
+ Answers the question a project's own README cannot: **will the tooling this
99
+ checkout expects actually work inside this distribution?** It reads the manifests
100
+ in the directory (`package.json` and lockfiles, `.nvmrc`, `tsconfig.json`,
101
+ `Cargo.toml`, `go.mod`, `pyproject.toml`, `requirements.txt`, `Dockerfile`,
102
+ `docker-compose.yml`, `Makefile`, `CMakeLists.txt`) and reports, per need, whether
103
+ it resolves on the Linux side:
104
+
105
+ ```
106
+ distro: Ubuntu-22.04 (system default)
107
+ workspace: /mnt/d/proj (Windows drive mount /mnt/d — builds, installs and git are much slower here; …)
108
+ project: package.json, pnpm-lock.yaml
109
+
110
+ needs (from the project): node, npm, pnpm
111
+ MISSING node — not on PATH in this distribution (from package.json)
112
+ WINDOWS npm — resolves to /mnt/c/Program Files/nodejs/npm, a Windows binary reached through interop; …
113
+ MISSING pnpm — not on PATH in this distribution (from pnpm-lock.yaml)
114
+
115
+ usable on the Linux side: python3 3.10.12, gcc 11.4.0, make 4.3, git 2.34.1, curl, rsync, tar, sudo
116
+ Windows binaries on the Linux PATH (interop): npm -> /mnt/c/Program Files/nodejs/npm, npx -> …
117
+ privileges: uid=1000 (xiny) · sudo: requires a password (unusable from a tool call) · root: switched off
118
+
119
+ next: node, npm are not usable — wsl-bootstrap({ recipes: ["node", "pnpm"] }) installs them …
120
+ ```
121
+
122
+ That `WINDOWS` row is the reason the tool exists. WSL appends the Windows `PATH`
123
+ to the Linux one, so `npm` resolves on a machine whose distribution has no `node`
124
+ at all — measured here as `/mnt/c/Program Files/nodejs/npm` — and the errors a
125
+ model gets from running it against a Linux directory name neither cause. Overall
126
+ it is read-only, cheap, and worth running before blaming a failed command on the
127
+ command. Takes an optional `workspace` (a Linux directory; default: the configured
128
+ directory, else the session workspace) and `distro`.
129
+
130
+ ### `wsl-bootstrap`
131
+
132
+ Installs the missing toolchain **inside the distribution**, from a fixed recipe
133
+ list and nothing else: `node` (Node.js LTS into `/usr/local`, checksum-verified,
134
+ with corepack), `pnpm`, `python` (python3 + pip + venv), `build`
135
+ (build-essential), `tools` (jq, rsync, curl, git, ca-certificates). Dependencies
136
+ are added when they are needed: `pnpm` brings `node`, and `node` brings `tools`
137
+ when `curl` is missing.
138
+
139
+ **It is off by default**, and its `dryRun` defaults to `true`, so the first call
140
+ prints the plan and changes nothing:
141
+
142
+ ```
143
+ dry run — nothing has been installed (distro Ubuntu-22.04)
144
+
145
+ already present:
146
+ python — python3, pip and venv (apt)
147
+
148
+ to install (python, tools):
149
+ [tools] apt packages: ca-certificates curl git jq rsync — runs as root inside the distribution
150
+ export DEBIAN_FRONTEND=noninteractive; apt-get update && apt-get install -y ca-certificates curl git jq rsync
151
+
152
+ apt would report:
153
+ Inst libjq1 (1.6-2.1ubuntu3.2 Ubuntu:22.04/jammy-updates [amd64])
154
+ …
155
+
156
+ run it for real with `dryRun: false`.
157
+ ```
158
+
159
+ The apt recipes collapse into **one** step (the `apt-get update` is the slow half,
160
+ and running it once per recipe would look like a hang), and an `apt-get -s`
161
+ simulation is included so the plan shows what apt itself would do. Recipes already
162
+ satisfied are skipped, so a second run is cheap. The steps run through
163
+ `wsl -u root` — see the next section for why — and a failing step stops the run
164
+ with the tail of its output.
165
+
166
+ ## Running privileged commands (`asRoot`)
167
+
168
+ `sudo` cannot work from a tool call: it needs a password, and stdin is `/dev/null`
169
+ unless you send one. WSL has its own answer — `wsl.exe -u root` needs no password
170
+ (measured: it answers `uid=0` on a machine where `sudo -n` fails) — so the `wsl`
171
+ tool exposes it as `asRoot: true`, gated by a switch in the panel
172
+ (「管理员模式(root)」, off by default):
173
+
174
+ ```
175
+ wsl({ command: 'apt-get install -y jq', description: 'install jq', asRoot: true })
176
+ ```
177
+
178
+ The authorisation is the switch, not the parameter: with it off, `asRoot: true` is
179
+ **refused with that instruction** rather than quietly downgraded to your user
180
+ account — a caller that asked for root and got a permission error would look for
181
+ the cause in the wrong place. Every privileged result is marked
182
+ (`[ran as root: wsl -u root]`) so a transcript says what happened, and the
183
+ destructive-command guard applies independently: `rm -rf` still needs
184
+ `allowDangerous: true`, as root or not.
185
+
93
186
  ## Install
94
187
 
95
188
  The published npm package is **`dsh-wsl-tool`**, not `dsh-wsl`: the registry
@@ -110,8 +203,10 @@ specifier you install under.
110
203
  checkout (which names the dependency after this package).
111
204
 
112
205
  2. Nothing else. The package's `cordis.patch.yml` inserts the `tool-wsl` row
113
- itself when the bundle is loaded, process-wide, so the three tools are
114
- available to every agent preset without further wiring.
206
+ itself when the bundle is loaded, process-wide, so the tools are
207
+ available to every agent preset without further wiring. **There is no mode to
208
+ pick and nothing to add to a preset** — the left-sidebar WSL panel arrives the
209
+ same way.
115
210
 
116
211
  Do **not** also list `tool-wsl` in a preset: DSH registers tools by name, and
117
212
  the second registration fails with
@@ -122,13 +217,23 @@ specifier you install under.
122
217
  The settings panel is the one optional piece: its form needs
123
218
  `@deepseek-ai/schemastery`, which most profiles already have (any plugin that
124
219
  depends on it brings it in — add it to the profile's dependencies if yours does
125
- not). Without it, the three tools and the panel's 「WSL 终端启动路径」 field work
220
+ not). Without it, the tools and the panel's 「WSL 终端启动路径」 field work
126
221
  as usual and only the switches are absent — the panel says so instead of waiting
127
222
  forever.
128
223
 
129
224
  ## Compatibility
130
225
 
131
- Verified against DSH **0.1.7-rc.2** (and 0.1.5-rc.2 before it): the tool schemas
226
+ The manifest declares the DSH it needs — `"engines": { "dsh": ">=0.1.7-rc.2" }` —
227
+ which is the floor this section documents. The plugin market reads that
228
+ declaration from the published manifest and shows it as a requirement on the
229
+ entry (`DSH >=0.1.7-rc.2`), warning before an install or update onto an older
230
+ host; DSH itself ignores `engines`, so the declaration is advice, not a gate.
231
+ The `-rc.2` is load-bearing: `>=0.1.7` alone excludes the `0.1.7-rc.2` release
232
+ this line was verified on, and the market would then report the plugin as
233
+ incompatible with its own verified host.
234
+
235
+ Verified against DSH **0.1.7-rc.2** (earlier releases of this plugin were verified
236
+ on 0.1.5-rc.2): the tool schemas
132
237
  pass DSH's own `assertSupportedJsonSchema`, the subprocess seam is exercised
133
238
  against the real provider rather than a shim, and the background-job path is
134
239
  checked against the real job registry, including the session-id ownership fence
@@ -160,6 +265,9 @@ explanation.
160
265
  | Linux 默认工作目录 | a fixed Linux directory (for example `/mnt/d/project`) used when `workdir` is omitted. Filling it in wins over the switch above; clearing it goes back to following the session |
161
266
  | WSL 终端启动路径 | the optional sidebar terminal's startup directory — `--cd <dir>` on the `terminal-controller` row of your **profile patch** (see [Optional: a WSL terminal in the sidebar](#optional-a-wsl-terminal-in-the-sidebar)). Empty clears it, and the panel reads the current value from the same file |
162
267
  | 危险命令守卫 | whether a destructive command needs an explicit `allowDangerous` |
268
+ | 管理员模式(root) | whether `wsl` accepts `asRoot: true` (`wsl -u root`). WSL's root needs no password, so this switch is the authorisation itself; **off by default** |
269
+ | wsl-doctor 项目体检 | registers `wsl-doctor` |
270
+ | wsl-bootstrap 安装工具链 | registers `wsl-bootstrap`, which may install into the distribution. **Off by default**, and its `dryRun` still has to be turned off per call |
163
271
 
164
272
  The panel edits the plugin's own configuration, so the same values can be written
165
273
  by hand (`- id: tool-wsl` with `config:` in a profile patch) or by environment
@@ -179,7 +287,7 @@ features — set them in the patch or with `DSH_WSL_DISTRO` / `DSH_WSL_TIMEOUT_M
179
287
  and the panel shows what is currently in effect.
180
288
 
181
289
  The settings surface needs `@deepseek-ai/schemastery`, which the plugin declares as
182
- an optional peer dependency: without it the three tools still run on their defaults
290
+ an optional peer dependency: without it the tools still run on their defaults
183
291
  and only the panel is missing.
184
292
 
185
293
  At the bottom of the panel there is one feedback entry, next to it a short
@@ -246,10 +354,18 @@ start too.
246
354
  | `stdin` | no | string | text written to the command stdin (UTF-8) before it runs |
247
355
  | `runInBackground` | no | boolean | run as a job and return its id immediately; read with `job_output`, stop with `job_kill` |
248
356
  | `allowDangerous` | no | boolean | set `true` to run destructive commands |
357
+ | `asRoot` | no | boolean | run this call as root (`wsl -u root`); refused unless the panel's 管理员模式 switch is on |
249
358
  | `translatePaths` | no | boolean | default `true`; set `false` to pass `command` through verbatim |
250
359
 
251
360
  ## Configuration
252
361
 
362
+ The panel's switches and value fields (`tools.wsl`, `tools.path`, `tools.env`,
363
+ `tools.doctor`, `tools.bootstrap`, `backgroundJobs`, `translatePaths`,
364
+ `startInSessionWorkspace`, `workdir`, `dangerGuard`, `allowRoot`, `distro`,
365
+ `timeoutMs`) live in the profile patch and take effect as described on each row.
366
+ Two of them have no environment variable on purpose: `dangerGuard` (its OFF
367
+ position is the footgun) and `allowRoot` (WSL's root needs no password, so the
368
+ switch IS the authorisation). The environment layer:
253
369
  | Environment variable | Default | Effect |
254
370
  |---|---|---|
255
371
  | `DSH_WSL_DISTRO` | (system default) | Pin the distribution for every call. |
@@ -259,7 +375,7 @@ start too.
259
375
  | `DSH_WSL_MAX_OUTPUT_BYTES` | `65536` | Per-stream in-memory window (1 KiB – 8 MiB). Also raises the spill ceiling when set above 64 MiB. |
260
376
 
261
377
  An unparsable or out-of-range value falls back to the default: one bad variable
262
- must not take all three tools down. Values are read once per mount, so a change
378
+ must not take every tool down. Values are read once per mount, so a change
263
379
  takes effect on restart.
264
380
 
265
381
  ## Notes
@@ -362,9 +478,11 @@ is split by concern:
362
478
  | `lib/result.js` | Launcher-noise filters, truncation facts, marker rendering. |
363
479
  | `lib/diagnostics.js` | The `wsl-env` capability probe, its parser and its lines. |
364
480
  | `lib/runner.js` | The single spawn path plus launcher-error classification. |
365
- | `lib/tools/*.js` | The three tool definitions (schema, execute, presentCall). |
481
+ | `lib/tools/*.js` | The tool definitions (schema, execute, presentCall). |
482
+ | `lib/doctor.js` | The project probe for `wsl-doctor`: one shell script, its parser, the report composer. |
483
+ | `lib/bootstrap.js` | The recipes, plan builder and outcome rendering for `wsl-bootstrap`. |
366
484
 
367
- - `apply()` resolves the configuration and registers three tools, each with a
485
+ - `apply()` resolves the configuration and registers the enabled tools, each with a
368
486
  JSON-schema parameter definition, an output schema, a `render` hook and an
369
487
  async `execute`.
370
488
  - Every call goes through one spawn path (`runner.runWsl`): `wsl.exe -d <distro>
@@ -394,8 +512,8 @@ is split by concern:
394
512
  segments, judges each `rm` invocation on its own flags, and matches the
395
513
  device/power tools at command position before dispatch.
396
514
 
397
- The plugin publishes no services of its own, so it sits loose in an agent
398
- preset without a realm.
515
+ The plugin publishes no services of its own and is inserted process-wide by its own
516
+ bundle patch, so it needs no realm — and no preset entry either.
399
517
 
400
518
  ## Development
401
519
 
@@ -406,19 +524,19 @@ DSH host plane. Iterate by pointing a profile dependency at the checkout:
406
524
  { "dependencies": { "dsh-wsl-tool": "file:/path/to/dsh-wsl" } }
407
525
  ```
408
526
 
409
- then restart DSH and exercise the tools from a session that uses a preset
410
- containing the `tool-wsl` row.
527
+ then restart DSH and call the tools from **any** session: the bundle patch registers
528
+ them process-wide, so no preset declares the row.
411
529
 
412
530
  ### Tests
413
531
 
414
532
  ```sh
415
- npm test # 260+ checks against real WSL, with a shim standing in for ctx.subprocess
533
+ npm test # 591 host checks against real WSL (shim for ctx.subprocess) + 133 client checks
416
534
  npm run test:real # the same checks against the REAL provider, plus the seam-fact suite
417
535
  ```
418
536
 
419
537
  `npm test` substitutes only `ctx.subprocess`, with a shim that reproduces the
420
538
  seam's bounded tail windows, spill files and termination ladder, and drives the
421
- three tools against the real WSL installation. It covers path translation,
539
+ the tools against the real WSL installation. It covers path translation,
422
540
  workdir quoting, the destructive guard, distro selection, exit-code/timeout/
423
541
  truncation markers, `wsl-path`, `wsl-env`, argument validation, configuration
424
542
  parsing, launcher-error classification, the returned shape against each declared
package/README.zh-CN.md CHANGED
@@ -11,17 +11,19 @@ Linux 原生 DSH:真实的 Linux 内核与 bash、退出码与信号、超时
11
11
  (见[沙箱边界](#沙箱边界));项目放在 Windows 盘上时仍是 **Windows** 的文件系统语义——没有 POSIX
12
12
  权限位、文件名大小写不敏感、收不到文件变更通知——速度也低一个数量级(见[注意事项](#注意事项))。
13
13
 
14
- ![三个工具、左侧栏开关面板与两条限制](assets/market-preview.png)
14
+ ![功能清单与左侧栏开关面板](assets/market-preview-features-3.png)
15
15
 
16
16
  ## 工具
17
17
 
18
- 插件注册三个工具:
18
+ 插件注册五个工具(后两个可单独关掉,其中 `wsl-bootstrap` **默认关**):
19
19
 
20
20
  | 工具 | 用途 |
21
21
  |---|---|
22
22
  | `wsl` | 执行 Linux 命令,返回带退出码、超时与截断标记的 `stdout`/`stderr` |
23
23
  | `wsl-path` | 通过 `wslpath` 在 Windows 与 WSL 路径间互转 |
24
24
  | `wsl-env` | 汇总 WSL 环境(发行版、内核、CPU、内存、磁盘) |
25
+ | `wsl-doctor` | 对照项目清单与发行版实际能力,指出缺什么——包括那些其实是 Windows 二进制的命令。只读 |
26
+ | `wsl-bootstrap` | 按固定配方在发行版里补装缺失的工具链,先出计划再动手 |
25
27
 
26
28
  ### `wsl`
27
29
 
@@ -78,6 +80,82 @@ launcher: WSL 版本: 2.6.3.0 · 内核版本: 6.6.87.2-1 · WSLg 版本: 1.0.71
78
80
  发行版不存在则直接报错,而不是给出一份残缺答案。注意 `wsl --version` 输出是**本地化**的,
79
81
  因此其标签按启动器原样透传、不按名称解析;Direct3D/MSRDC/DXCore 版本作为噪声被省略。
80
82
 
83
+ ### `wsl-doctor`
84
+
85
+ 回答一个项目自己的 README 答不了的问题:**这份代码指望的工具链,在这套发行版里真的能用吗?**
86
+ 它读取目录里的清单(`package.json` 与各种 lockfile、`.nvmrc`、`tsconfig.json`、`Cargo.toml`、
87
+ `go.mod`、`pyproject.toml`、`requirements.txt`、`Dockerfile`、`docker-compose.yml`、`Makefile`、
88
+ `CMakeLists.txt`),然后逐项给出它在 Linux 侧的位置:
89
+
90
+ ```
91
+ distro: Ubuntu-22.04 (system default)
92
+ workspace: /mnt/d/proj (Windows drive mount /mnt/d — builds, installs and git are much slower here; …)
93
+ project: package.json, pnpm-lock.yaml
94
+
95
+ needs (from the project): node, npm, pnpm
96
+ MISSING node — not on PATH in this distribution (from package.json)
97
+ WINDOWS npm — resolves to /mnt/c/Program Files/nodejs/npm, a Windows binary reached through interop; …
98
+ MISSING pnpm — not on PATH in this distribution (from pnpm-lock.yaml)
99
+
100
+ usable on the Linux side: python3 3.10.12, gcc 11.4.0, make 4.3, git 2.34.1, curl, rsync, tar, sudo
101
+ Windows binaries on the Linux PATH (interop): npm -> /mnt/c/Program Files/nodejs/npm, npx -> …
102
+ privileges: uid=1000 (xiny) · sudo: requires a password (unusable from a tool call) · root: switched off
103
+
104
+ next: node, npm are not usable — wsl-bootstrap({ recipes: ["node", "pnpm"] }) installs them …
105
+ ```
106
+
107
+ 那行 `WINDOWS` 就是它存在的理由。WSL 会把 Windows 的 `PATH` 接到 Linux 的后面,于是
108
+ `npm` 在一套**根本没有 `node`** 的发行版里"找得到"(本机实测就是
109
+ `/mnt/c/Program Files/nodejs/npm`),模型拿它去跑 Linux 目录时报的错两头都不沾。整体只读、
110
+ 开销很小,值得在把失败归咎于命令本身之前先跑一次。可选参数 `workspace`(Linux 目录;默认取
111
+ 配置的目录,否则取会话工作区)与 `distro`。
112
+
113
+ ### `wsl-bootstrap`
114
+
115
+ **在发行版内部**补装缺失的工具链,只认固定配方:`node`(Node.js LTS 装进 `/usr/local`,
116
+ 校验 sha256,并启用 corepack)、`pnpm`、`python`(python3 + pip + venv)、`build`
117
+ (build-essential)、`tools`(jq、rsync、curl、git、ca-certificates)。依赖按需带上:
118
+ `pnpm` 会带上 `node`,`curl` 缺失时 `node` 会带上 `tools`。
119
+
120
+ **它默认关**,且 `dryRun` 默认为 `true`,所以第一次调用只打印计划、什么都不改:
121
+
122
+ ```
123
+ dry run — nothing has been installed (distro Ubuntu-22.04)
124
+
125
+ already present:
126
+ python — python3, pip and venv (apt)
127
+
128
+ to install (python, tools):
129
+ [tools] apt packages: ca-certificates curl git jq rsync — runs as root inside the distribution
130
+ export DEBIAN_FRONTEND=noninteractive; apt-get update && apt-get install -y ca-certificates curl git jq rsync
131
+
132
+ apt would report:
133
+ Inst libjq1 (1.6-2.1ubuntu3.2 Ubuntu:22.04/jammy-updates [amd64])
134
+ …
135
+
136
+ run it for real with `dryRun: false`.
137
+ ```
138
+
139
+ 所有 apt 配方会**合并成一步**(`apt-get update` 是慢的那一半,每个配方各跑一次看起来就像卡死),
140
+ 并附带一条 `apt-get -s` 只读模拟,让计划直接显示 apt 自己会做什么。已经满足的配方会被跳过,
141
+ 所以第二次跑很便宜。步骤通过 `wsl -u root` 执行——为什么见下一节——某一步失败即停下并附上
142
+ 它的输出末尾。
143
+
144
+ ## 以 root 执行(`asRoot`)
145
+
146
+ `sudo` 在工具调用里根本用不了:它要密码,而除非你显式喂 stdin,stdin 就是 `/dev/null`。
147
+ WSL 自带答案——`wsl.exe -u root` **免密**(本机实测:`sudo -n` 失败时它照样给出 `uid=0`)——
148
+ 于是 `wsl` 工具把它暴露成 `asRoot: true`,由面板里一项开关把关(「管理员模式(root)」,默认关):
149
+
150
+ ```
151
+ wsl({ command: 'apt-get install -y jq', description: 'install jq', asRoot: true })
152
+ ```
153
+
154
+ 授权来自开关而不是参数:开关关着时,`asRoot: true` 会被**明确拒绝并给出开关位置**,而不是
155
+ 悄悄降级成你的普通用户——一个要了 root 却拿到权限错误的人,会去错误的地方找原因。每次以 root
156
+ 执行的结果都会标注(`[ran as root: wsl -u root]`),转录里说得清发生过什么;危险命令守卫则
157
+ 独立生效:`rm -rf` 无论是不是 root 都仍需 `allowDangerous: true`。
158
+
81
159
  ## 安装
82
160
 
83
161
  发布到 npm 的包名是 **`dsh-wsl-tool`**,不是 `dsh-wsl`:registry 判定 `dsh-wsl` 与既有包
@@ -96,7 +174,8 @@ launcher: WSL 版本: 2.6.3.0 · 内核版本: 6.6.87.2-1 · WSLg 版本: 1.0.71
96
174
  (依赖名会取本包自身的名字)。
97
175
 
98
176
  2. 不必再做别的。本包的 `cordis.patch.yml` 会在组合包加载时**自己插入** `tool-wsl` 行
99
- (进程级),因此三个工具对所有 agent preset 都可用,无需额外接线。
177
+ (进程级),因此这些工具对所有 agent preset 都可用,无需额外接线。**不需要选择任何模式,
178
+ 也不要在 preset 里加任何东西**——左侧栏的 WSL 面板同样是这样出现的。
100
179
 
101
180
  **不要**再在 preset 里列一遍 `tool-wsl`:DSH 按名字注册工具,第二次注册会直接失败
102
181
  —— `tool "wsl" is already registered in this scope`。这一行只由组合包提供。
@@ -104,12 +183,18 @@ launcher: WSL 版本: 2.6.3.0 · 内核版本: 6.6.87.2-1 · WSLg 版本: 1.0.71
104
183
  3. 重启 DSH。
105
184
 
106
185
  设置面板是唯一可选的一块:它的表单需要 `@deepseek-ai/schemastery`,多数 profile 已经有了
107
- (只要装过任何依赖它的插件;没有的话把它加进 profile 的依赖即可)。没有它时,三个工具与
186
+ (只要装过任何依赖它的插件;没有的话把它加进 profile 的依赖即可)。没有它时,工具与
108
187
  面板里的「WSL 终端启动路径」照常可用,只是开关不显示 —— 面板会直接说明这一点,不会一直转圈等待。
109
188
 
110
189
  ## 兼容性
111
190
 
112
- 已在 DSH **0.1.7-rc.2** 上验证(此前为 0.1.5-rc.2):工具 schema 通过 DSH 自己的
191
+ 清单里声明了它需要的 DSH —— `"engines": { "dsh": ">=0.1.7-rc.2" }`,也就是本节记录的
192
+ 下限。插件市场会从已发布的清单读取这条声明,在条目上标成要求(`DSH >=0.1.7-rc.2`),并在
193
+ 装到更老的宿主上之前给出提醒;DSH 自身不读 `engines`,所以它是提示而不是门禁。那个
194
+ `-rc.2` 不能省:只写 `>=0.1.7` 会把本条验证过的 `0.1.7-rc.2` 排除掉,市场于是会把插件
195
+ 判成与它自己验证过的宿主不兼容。
196
+
197
+ 已在 DSH **0.1.7-rc.2** 上验证(本插件的更早版本曾在 0.1.5-rc.2 上验证):工具 schema 通过 DSH 自己的
113
198
  `assertSupportedJsonSchema`;subprocess 接缝是跑在**真实 provider** 上而非替身;后台任务
114
199
  路径跑在**真实 job 注册表**上,包含 0.1.7 收紧的「会话 id 属主围栏」。这套检查就是
115
200
  `test/real-seam.mjs`,约一分钟即可重验一个新宿主——升级后把它指向新的 DSH 安装即可:
@@ -138,6 +223,9 @@ DSH_SUBPROCESS_LOCAL=/path/to/dsh/node_modules npm run test:real
138
223
  | Linux 默认工作目录 | 未传 `workdir` 时使用的固定 Linux 目录(如 `/mnt/d/project`)。填了就优先于上面的开关;清空则回到跟随会话 |
139
224
  | WSL 终端启动路径 | 可选的侧边栏终端从哪个目录启动 —— 写进你 **profile patch** 里 `terminal-controller` 那一行的 `--cd <目录>`(见[可选:在侧边栏开一个 WSL 终端](#可选在侧边栏开一个-wsl-终端))。留空则删掉该参数;当前值也是从这个文件读的 |
140
225
  | 危险命令守卫 | 危险命令是否必须显式 `allowDangerous` |
226
+ | 管理员模式(root) | `wsl` 是否接受 `asRoot: true`(`wsl -u root`)。WSL 的 root 免密,所以这项开关就是授权本身;**默认关** |
227
+ | wsl-doctor 项目体检 | 注册 `wsl-doctor` |
228
+ | wsl-bootstrap 安装工具链 | 注册 `wsl-bootstrap`,它可能往发行版里装东西。**默认关**,且每次调用还得显式关掉 `dryRun` |
141
229
 
142
230
  面板改的是插件自己的配置,所以同样的值也可以手写进 profile patch(`- id: tool-wsl` 加 `config:`)
143
231
  或用环境变量设。优先级由 `lib/config.js` 定:**插件配置 > 环境变量 > 内置默认值**;开关停在默认值时
@@ -150,7 +238,7 @@ patch 层 —— 每次写入前先备份该文件,只重写终端那一行的
150
238
  **改动在下次启动 DSH 后生效**:宿主每次挂载只读一次该配置,面板里也写着这句。发行版与超时属于"值"
151
239
  而不是"功能":在 patch 里或用 `DSH_WSL_DISTRO` / `DSH_WSL_TIMEOUT_MS` 设置,面板只显示当前生效值。
152
240
 
153
- 这个设置界面需要 `@deepseek-ai/schemastery`(插件把它声明为可选 peer 依赖):没有它三个工具照常按
241
+ 这个设置界面需要 `@deepseek-ai/schemastery`(插件把它声明为可选 peer 依赖):没有它工具照常按
154
242
  默认值工作,只是没有面板。
155
243
 
156
244
  面板**最底部**是一个反馈入口,旁边跟着一段简短的提交指南,以及一个「复制插件信息」按钮(详见
@@ -205,10 +293,17 @@ patch 层 —— 每次写入前先备份该文件,只重写终端那一行的
205
293
  | `stdin` | 否 | string | 在命令运行前写入其 stdin 的文本(UTF-8) |
206
294
  | `runInBackground` | 否 | boolean | 作为后台任务运行并立即返回任务 id;用 `job_output` 读、`job_kill` 停 |
207
295
  | `allowDangerous` | 否 | boolean | 置 `true` 才允许执行危险命令 |
296
+ | `asRoot` | 否 | boolean | 本次调用以 root 执行(`wsl -u root`);面板「管理员模式」关着时会被拒绝 |
208
297
  | `translatePaths` | 否 | boolean | 默认 `true`;置 `false` 时 `command` 原样传入,不做路径改写 |
209
298
 
210
299
  ## 配置
211
300
 
301
+ 面板里的开关与取值字段(`tools.wsl`、`tools.path`、`tools.env`、`tools.doctor`、
302
+ `tools.bootstrap`、`backgroundJobs`、`translatePaths`、`startInSessionWorkspace`、`workdir`、
303
+ `dangerGuard`、`allowRoot`、`distro`、`timeoutMs`)都写在 profile patch 里,各自行上有说明。
304
+ 其中两项**故意没有环境变量**:`dangerGuard`(它的关闭位置本身就是风险)与 `allowRoot`
305
+ (WSL 的 root 免密,所以那项开关就是授权本身)。环境变量这一层:
306
+
212
307
  | 环境变量 | 默认值 | 作用 |
213
308
  |---|---|---|
214
309
  | `DSH_WSL_DISTRO` | (系统默认) | 为所有调用固定发行版 |
@@ -217,7 +312,7 @@ patch 层 —— 每次写入前先备份该文件,只重写终端那一行的
217
312
  | `DSH_WSL_MAX_OUTPUT_BYTES` | `65536` | 每条流的内存窗口(1 KiB – 8 MiB);设到 64 MiB 以上时也会抬高落盘上限 |
218
313
  | `DSH_WSL_WORKDIR` | `home` | 不传 `workdir` 时的起点:`home`(Linux 的 `~`)、`session`(会话工作目录,Windows 检出对应 `/mnt/<盘>/...`),或任意显式路径 |
219
314
 
220
- 无法解析或越界的值会回退到默认值——一个写错的环境变量不该让三个工具一起挂掉。
315
+ 无法解析或越界的值会回退到默认值——一个写错的环境变量不该让所有工具一起挂掉。
221
316
  配置在挂载时读取一次,改动需重启 DSH 生效。
222
317
 
223
318
  ## 注意事项
@@ -331,9 +426,11 @@ argv 包一层过 `ctx.sandbox`)和文件系统服务(`@deepseek-ai/dsh-fs-s
331
426
  | `lib/result.js` | 启动器噪声过滤、截断事实、标记渲染 |
332
427
  | `lib/diagnostics.js` | `wsl-env` 的能力探针、解析器与输出行 |
333
428
  | `lib/runner.js` | 唯一的 spawn 路径与启动器错误分类 |
334
- | `lib/tools/*.js` | 三个工具定义(schema / execute / presentCall) |
429
+ | `lib/tools/*.js` | 工具定义(schema / execute / presentCall) |
430
+ | `lib/doctor.js` | `wsl-doctor` 的项目探针:一段 shell、它的解析器与报告组装 |
431
+ | `lib/bootstrap.js` | `wsl-bootstrap` 的配方、计划生成与结果渲染 |
335
432
 
336
- - `apply()` 解析配置并注册三个工具,每个都有 JSON-schema 参数定义、输出 schema、
433
+ - `apply()` 解析配置并注册被启用的工具,每个都有 JSON-schema 参数定义、输出 schema、
337
434
  `render` 钩子与异步 `execute`。
338
435
  - 所有调用都走同一条 spawn 路径(`runner.runWsl`):通过宿主 `subprocess` 执行
339
436
  `wsl.exe -d <distro> -e bash -lc "<exports; cd workdir && command>"`,stdout/stderr
@@ -353,7 +450,8 @@ argv 包一层过 `ctx.sandbox`)和文件系统服务(`@deepseek-ai/dsh-fs-s
353
450
  - 危险命令防护把命令按 `;`/`&`/`|`/换行切成段,每次 `rm` 调用按自身标志单独判定,
354
451
  设备/电源类工具在命令位置匹配后才拦截。
355
452
 
356
- 插件不发布任何自身服务,因此它可以无 realm 地挂在 agent preset 中。
453
+ 插件不发布任何自身服务,且由它自己的组合包 patch **进程级**插入,因此既不需要 realm,
454
+ 也不需要任何 preset 条目。
357
455
 
358
456
  ## 开发
359
457
 
@@ -364,17 +462,18 @@ argv 包一层过 `ctx.sandbox`)和文件系统服务(`@deepseek-ai/dsh-fs-s
364
462
  { "dependencies": { "dsh-wsl-tool": "file:/path/to/dsh-wsl" } }
365
463
  ```
366
464
 
367
- 然后重启 DSH,在包含 `tool-wsl` 行的 preset 会话中调用工具验证。
465
+ 然后重启 DSH,在**任意**会话里调用工具即可:组合包 patch 是进程级注册的,没有哪个 preset
466
+ 需要声明这一行。
368
467
 
369
468
  ### 测试
370
469
 
371
470
  ```sh
372
- npm test # 260+ 项检查,跑在真实 WSL 上,仅用 shim 顶替 ctx.subprocess
471
+ npm test # 591 项宿主检查(跑在真实 WSL 上,仅用 shim 顶替 ctx.subprocess)+ 133 项客户端检查
373
472
  npm run test:real # 同一套检查改跑真实 provider,外加 seam 事实套件
374
473
  ```
375
474
 
376
475
  `npm test` 只替换 `ctx.subprocess`,用一个复刻了 seam 行为(有界尾窗、落盘文件、
377
- 终止阶梯)的 shim 驱动三个工具跑在真实 WSL 上,覆盖路径改写、workdir 引号处理、
476
+ 终止阶梯)的 shim 驱动这些工具跑在真实 WSL 上,覆盖路径改写、workdir 引号处理、
378
477
  危险命令防护、发行版选择、退出码/超时/截断标记、`wsl-path`、`wsl-env`、参数校验、
379
478
  配置解析、启动器错误分类、返回结构与各自 `output.schema` 的一致性,以及模型可见
380
479
  目录的 token 预算。
package/SUPPORT.md CHANGED
@@ -32,5 +32,5 @@ The panel ends with **one** feedback entry: a submission guide beside it (how to
32
32
 
33
33
  ## 修复的节奏 / How fixes happen
34
34
 
35
- 报告 → 复现(必要时我会请你补 `wsl-env` 输出)→ 改代码 → 测试(这个插件有 347 项宿主检查 + 76 项客户端检查)→ 发版(npm + 市场资产同一次发布,逐字节一致)→ 你升级后回帖确认 → 关闭。
36
- Report → reproduce (I may ask for a full `wsl-env`) → fix → tests (the plugin ships with 347 host checks and 76 client checks) → release (npm and the market asset go out byte-identical in one run) → you confirm after upgrading → close.
35
+ 报告 → 复现(必要时我会请你补 `wsl-env` 输出)→ 改代码 → 测试(这个插件有 591 项宿主检查 + 133 项客户端检查)→ 发版(npm + 市场资产同一次发布,逐字节一致)→ 你升级后回帖确认 → 关闭。
36
+ Report → reproduce (I may ask for a full `wsl-env`) → fix → tests (the plugin ships with 591 host checks and 133 client checks) → release (npm and the market asset go out byte-identical in one run) → you confirm after upgrading → close.
package/index.js CHANGED
@@ -10,7 +10,9 @@
10
10
  //
11
11
  // Each call runs in a fresh shell, so no state persists between calls. This
12
12
  // plugin publishes nothing and only consumes the host-plane `subprocess` and
13
- // `tools` registries, so it sits loose in an agent preset without a realm.
13
+ // `tools` registries, so it needs no realm. Its own `cordis.patch.yml` inserts
14
+ // the `tool-wsl` row process-wide, which is why the tools reach every agent
15
+ // preset with no preset entry of its own (and why adding one there would fail).
14
16
  //
15
17
  // The implementation lives in `lib/`: `config` (defaults, environment overrides
16
18
  // and the plugin's own switches), `paths` (path translation and shell quoting),
@@ -33,6 +35,8 @@ import { readTerminalCwd, reviewTerminalWrite, writeTerminalCwd } from './lib/te
33
35
  import { createWslTool } from './lib/tools/wsl.js'
34
36
  import { createWslPathTool } from './lib/tools/wsl-path.js'
35
37
  import { createWslEnvTool } from './lib/tools/wsl-env.js'
38
+ import { createWslDoctorTool } from './lib/tools/wsl-doctor.js'
39
+ import { createWslBootstrapTool } from './lib/tools/wsl-bootstrap.js'
36
40
 
37
41
  export const name = 'tool-wsl'
38
42
  export const inject = ['tools', 'subprocess']
@@ -89,12 +93,15 @@ export const Config = Schema?.object({
89
93
  wsl: Schema.boolean().default(true).description('注册 `wsl` 工具:在 WSL 发行版中执行 Linux 命令。').volatile(),
90
94
  path: Schema.boolean().default(true).description('注册 `wsl-path` 工具:在 Windows 路径与 `/mnt/...` 之间互转。').volatile(),
91
95
  env: Schema.boolean().default(true).description('注册 `wsl-env` 工具:汇总发行版、内核、systemd、cgroup、GPU 直通、docker 与挂载盘。').volatile(),
92
- }).description('要注册哪些工具;这三项在重启 DSH 后生效。'),
96
+ doctor: Schema.boolean().default(true).description('注册 `wsl-doctor` 工具:对照项目清单与发行版实际能力,指出缺什么,以及哪些命令其实是 Windows 的。').volatile(),
97
+ bootstrap: Schema.boolean().default(false).description('注册 `wsl-bootstrap` 工具:在发行版里补装缺失的工具链。默认关;打开后它以 root 执行固定配方,且调用方仍需显式 `dryRun: false`。').volatile(),
98
+ }).description('要注册哪些工具;这几项在重启 DSH 后生效。'),
93
99
  backgroundJobs: Schema.boolean().default(true).description('允许长任务以 `runInBackground` 后台执行,结果由内置 job 工具读取。').volatile(),
94
100
  translatePaths: Schema.boolean().default(true).description('命令中的 Windows 路径默认转为 `/mnt/...`。').volatile(),
95
101
  startInSessionWorkspace: Schema.boolean().default(true).description('未传 `workdir` 时从会话目录启动;默认开。关闭后使用下方固定目录,该目录留空时为家目录 `~`。').volatile(),
96
102
  workdir: Schema.string().default('').description('未传 `workdir` 时使用的固定 Linux 目录(如 `/mnt/d/project`),优先于「跟随会话工作区」;留空表示不指定。').volatile(),
97
103
  dangerGuard: Schema.boolean().default(true).description('危险命令(删除、分区、关机等)需显式 `allowDangerous` 才放行;关闭后模型可直接执行。').volatile(),
104
+ allowRoot: Schema.boolean().default(false).description('允许 `wsl` 以 root 执行(`asRoot: true`)。WSL 的 root 免密,所以这项开关就是授权本身;默认关。').volatile(),
98
105
  distro: Schema.string().default('').description('固定使用的发行版;留空表示使用系统默认(也可用 `DSH_WSL_DISTRO`)。').volatile(),
99
106
  timeoutMs: Schema.number().default(0).description('默认命令超时(毫秒);0 表示使用内置默认(也可用 `DSH_WSL_TIMEOUT_MS`)。').volatile(),
100
107
  }).description('dsh-wsl 的功能开关与默认值。')
@@ -583,6 +590,7 @@ async function selfInfo(config, runner) {
583
590
  translatePaths: config.translatePaths,
584
591
  startInSessionWorkspace: config.startInSessionWorkspace,
585
592
  dangerGuard: config.dangerGuard,
593
+ allowRoot: config.allowRoot,
586
594
  distro: config.distro,
587
595
  commandTimeoutMs: config.commandTimeoutMs,
588
596
  maxOutputBytes: config.maxOutputBytes,
@@ -601,6 +609,11 @@ export function apply(ctx, settings = {}) {
601
609
  if (config.tools.wsl) ctx.tools.register(createWslTool({ ctx, config, runner }))
602
610
  if (config.tools.path) ctx.tools.register(createWslPathTool({ config, runner }))
603
611
  if (config.tools.env) ctx.tools.register(createWslEnvTool({ config, runner }))
612
+ if (config.tools.doctor) ctx.tools.register(createWslDoctorTool({ config, runner }))
613
+ // The installer is the one tool that is NOT registered by default: turning on
614
+ // its switch is the user's decision to let this plugin add software to their
615
+ // distribution.
616
+ if (config.tools.bootstrap) ctx.tools.register(createWslBootstrapTool({ config, runner }))
604
617
 
605
618
  // ---------------------------------------------------------------- self info
606
619
  //