dsh-windows-mcp 0.1.0 → 0.2.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/CHANGELOG.md CHANGED
@@ -4,7 +4,49 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [0.1.0] - 2026-10-10
7
+ ## [0.2.0] - 2026-10-24
8
+
9
+ ### Added
10
+
11
+ - **Windows-MCP version management** (`lib/updater.js`, `lib/installs.js`): the
12
+ plugin checks the mirrors for the newest stable `windows-mcp`, installs a
13
+ version **side by side** under `$DSH_HOME/dsh-windows-mcp/toolchain/<version>`
14
+ (each version its own `UV_TOOL_DIR`, so nothing is overwritten), rewrites the
15
+ bridge row to the freshly installed executable, and keeps the replaced version
16
+ so **Roll back** switches back to it without downloading anything. The check
17
+ result, active/previous version and the install history are persisted in
18
+ `$DSH_HOME/dsh-windows-mcp.versions.json`. Checks run on boot and are cached
19
+ for six hours.
20
+ - **Fast-mirror selection for the Python dependency** (`lib/mirrors.js`,
21
+ `lib/pypi.js`): PyPI plus the USTC, Aliyun, Tencent Cloud, Huawei Cloud,
22
+ Nanjing University and TUNA mirrors are probed with a 96 KiB ranged read and
23
+ ranked by measured throughput; the install uses the fastest working mirror and
24
+ falls back to the next candidate on failure.
25
+ - On the plugin page: running version, newest version, kept versions, the
26
+ measured mirror speed, and **Check for updates** / **Update to vX** / **Roll
27
+ back to vPrev** actions, with the streamed `uv` log.
28
+ - `tools/update-e2e.mjs`, a maintainer harness that runs the real check, install,
29
+ activation and rollback against this machine's profile (never published).
30
+
31
+ ### Changed
32
+
33
+ - `createPrepare` / the prepare route now install into the managed tool root
34
+ through `UV_TOOL_DIR`, support a chosen `--default-index` mirror, and resolve
35
+ the executable from the version that was installed.
36
+ - `uv` installs set `UV_CONCURRENT_DOWNLOADS=4`, `UV_HTTP_RETRIES=3` and
37
+ `UV_HTTP_TIMEOUT=600`: at uv's default of eight parallel downloads the mirrors
38
+ answer HTTP 429 and the install fails.
39
+ - The bridge-row writer, the doctor report and the `/state` payload all report
40
+ which side is live (`managed` vs the global `uv-tool` install).
41
+
42
+ ### Fixed
43
+
44
+ - The PyPI index parser dropped files whose link carried a `#sha256=…` fragment
45
+ (every wheel on a real mirror), so the newest version looked missing.
46
+ - The version pin regex stopped at YAML punctuation but not at the backtick used
47
+ in the written block's prose comment.
48
+
49
+ ## [0.1.0] - 2026-10-11
8
50
 
9
51
  ### Added
10
52
 
package/LICENSE CHANGED
@@ -19,9 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
23
- ---
24
-
25
- This plugin does not bundle Windows-MCP: at runtime it launches the upstream
26
- package `windows-mcp` (CursorTouch/Windows-MCP, MIT) and mounts the MCP client
27
- bridge that ships with DeepSeek Harness. Both remain under their own licenses.
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # dsh-windows-mcp
2
2
 
3
+ **English** · [中文](https://github.com/helloHub1212/dsh-windows-mcp/blob/main/README.zh.md)
4
+
5
+ [![npm version](https://img.shields.io/npm/v/dsh-windows-mcp.svg)](https://www.npmjs.com/package/dsh-windows-mcp)
6
+ [![CI](https://github.com/helloHub1212/dsh-windows-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/helloHub1212/dsh-windows-mcp/actions/workflows/ci.yml)
7
+
3
8
  **Windows desktop automation for [DeepSeek Harness](https://github.com/sandbaseai/deepseek-harness-handbook)** — lets the DSH agent see your screen and drive your PC.
4
9
 
5
10
  This plugin connects [CursorTouch/Windows-MCP](https://github.com/CursorTouch/Windows-MCP) to DSH by mounting one loader row against the **MCP client bridge that already ships inside DeepSeek Harness**. There is no reimplemented protocol client and no second copy of the MCP SDK: the plugin is a *carrier bundle*, and everything you can do comes from the official bridge.
@@ -98,6 +103,21 @@ The block it writes is exactly the row documented under [Pin a local interpreter
98
103
 
99
104
  The page also shows the **Patch target**: the `cordis.patch.yml` a manual write would touch, which is the profile this host booted — including a `dsh --profile web` host. If this package sits in more than one profile and the host does not report which one it started, the plugin writes nothing and says so instead of guessing: set `DSH_WINDOWS_MCP_PROFILE=<profile name>` (or an absolute profile directory) for the process that runs DSH and retry.
100
105
 
106
+ ### Update Windows-MCP, and roll back
107
+
108
+ The same page manages the upstream version, so an upgrade never means reinstalling the dependency by hand:
109
+
110
+ - **It checks by itself.** On boot, and whenever the page asks, the plugin probes the PyPI mirrors, takes the fastest one that answers and reads its `windows-mcp` index. The result is cached for six hours in `$DSH_HOME/dsh-windows-mcp.versions.json`, so restarts do not hammer the network.
111
+ - **Versions live side by side.** Every version is installed into its own tool root, `$DSH_HOME/dsh-windows-mcp/toolchain/<version>/windows-mcp`, through `UV_TOOL_DIR`. Activating a version only rewrites the bridge row (backed up, as above): no reinstall, and the previous version stays on disk.
112
+ - **Update in one click.** **Update to vX** downloads that version from the fastest mirror that works, verifies the executable and its `dist-info`, then activates it — the panel names the running version and the mirror it came from.
113
+ - **Roll back the same way.** **Roll back to vPrev** points the bridge back at the version the active one replaced; nothing is downloaded.
114
+
115
+ Only the managed installs are touched: until you install one, the bridge keeps using the global `uv tool` installation, and the panel says which side is live (`uv-tool` vs `managed`).
116
+
117
+ ### Fast mirrors for the Python dependency
118
+
119
+ The check measures all seven candidates — PyPI plus the USTC, Aliyun, Tencent Cloud, Huawei Cloud, Nanjing University and TUNA mirrors — with a 96 KiB ranged read, and ranks them by throughput. An install uses the fastest mirror that works and falls back to the next one when a mirror fails, with `UV_CONCURRENT_DOWNLOADS=4`, `UV_HTTP_RETRIES=3` and `UV_HTTP_TIMEOUT=600`, because mirrors rate-limit uv's default of eight parallel downloads (HTTP 429). A mirror that has blocked this network is ranked out instead of failing the install; the measured speed is shown per mirror in the panel.
120
+
101
121
  ## Verify
102
122
 
103
123
  ```text
@@ -293,12 +313,28 @@ Layout:
293
313
 
294
314
  ```text
295
315
  cordis.patch.yml the integration: two insert rows
296
- lib/index.js host half: windows_mcp_doctor + startup audit (no dependencies)
297
- test/plugin.test.mjs patch composition + hostile-context tests
316
+ lib/index.js host half: routes + windows_mcp_doctor + startup audit (no dependencies)
317
+ lib/dependency.js dependency state, uv invocation, profile bridge-row writer
318
+ lib/pypi.js PyPI index parsing and version maths (pure functions)
319
+ lib/mirrors.js mirror candidates, ranged speed probe, ranking
320
+ lib/installs.js per-version tool roots and the versions state file
321
+ lib/updater.js version manager: check / install / activate / roll back
322
+ lib/client.js web half: composer switch + plugin panel
323
+ test/plugin.test.mjs patch composition + hostile-context + panel tests
324
+ test/update.test.mjs version maths, mirror ranking, isolation and rollback state machine
298
325
  scripts/dump-config.mjs dev helper for composing against a scratch profile
299
326
  tools/ zero-dependency debug probes (not published to npm)
300
327
  ```
301
328
 
329
+ To exercise the update path for real — real mirrors, real `uv`, real profile
330
+ write — use the e2e harness:
331
+
332
+ ```powershell
333
+ node tools/update-e2e.mjs --check # probe the mirrors, read the newest stable version
334
+ node tools/update-e2e.mjs --install 0.8.8 # download, activate, rewrite the bridge row
335
+ node tools/update-e2e.mjs --rollback
336
+ ```
337
+
302
338
  Contributions are welcome; keep `lib/index.js` free of runtime dependencies and
303
339
  keep `apply()` throw-proof.
304
340
 
package/README.zh.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # dsh-windows-mcp
2
2
 
3
+ [English](https://github.com/helloHub1212/dsh-windows-mcp/blob/main/README.md) · **中文**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/dsh-windows-mcp.svg)](https://www.npmjs.com/package/dsh-windows-mcp)
6
+ [![CI](https://github.com/helloHub1212/dsh-windows-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/helloHub1212/dsh-windows-mcp/actions/workflows/ci.yml)
7
+
3
8
  **给 [DeepSeek Harness](https://github.com/sandbaseai/deepseek-harness-handbook) 接上 Windows 桌面自动化** —— 让 DSH 的 agent 能"看见"你的屏幕并操作你的电脑。
4
9
 
5
10
  本插件把 [CursorTouch/Windows-MCP](https://github.com/CursorTouch/Windows-MCP) 接到 DSH 上,做法是**挂载 DSH 本体自带的那座 MCP 客户端桥**:不重新实现协议客户端,也不重复打包 MCP SDK。插件本身是一个 *carrier bundle*(只有补丁、没有自己的服务),agent 能用的一切能力都来自官方那座桥。
@@ -94,6 +99,21 @@ Windows-MCP 本身是个 Python 程序,所以 **设置 → 插件 → dsh-wind
94
99
 
95
100
  面板上还会显示 **写入目标**:手动写入会落到哪个 `cordis.patch.yml`,也就是这个宿主启动的那个 profile(`dsh --profile web` 启动的 web 宿主同样适用)。如果本插件装在多个 profile 里、而宿主没有报告自己启动的是哪一个,插件**不会猜**:它什么都不写并直接说明——给运行 DSH 的进程设 `DSH_WINDOWS_MCP_PROFILE=<profile 名>`(或 profile 的绝对路径)再重试。
96
101
 
102
+ ### 更新 Windows-MCP,以及回退
103
+
104
+ 同一个面板也管上游版本,所以升级不用再手动重装依赖:
105
+
106
+ - **自己检查。** 宿主启动时(以及页面要求时)插件会探测各 PyPI 镜像,取响应最快的那个读它的 `windows-mcp` 索引;结果在 `$DSH_HOME/dsh-windows-mcp.versions.json` 里缓存 6 小时,重启不会反复打网络。
107
+ - **版本各装各的。** 每个版本通过 `UV_TOOL_DIR` 装进自己的目录:`$DSH_HOME/dsh-windows-mcp/toolchain/<版本>/windows-mcp`。激活一个版本只是重写桥那一行(同样先备份),**不需要重装**,上一版原样留在磁盘上。
108
+ - **一键更新。** **更新到 vX** 会从能用的最快镜像下载该版本(失败自动换下一个镜像),校验可执行文件和 `dist-info` 之后再激活;面板会显示当前运行版本和它来自哪个镜像。
109
+ - **回退同样一键。** **回退到 vPrev** 把桥指回“当前版本替换掉的那个版本”,不下载任何东西。
110
+
111
+ 只动插件自己管理的安装:在你安装第一个托管版本之前,桥仍然用全局 `uv tool` 装的那份;面板会标明当前生效的是 `uv-tool` 还是 `managed`。
112
+
113
+ ### Python 依赖的镜像加速
114
+
115
+ 检查阶段会用 96 KiB 的 range 读取量一遍七个候选——PyPI 官方,加上中科大、阿里云、腾讯云、华为云、南京大学、清华 TUNA——按实测吞吐量排序。安装时用最快且能用的镜像,失败就回退到下一个;同时固定 `UV_CONCURRENT_DOWNLOADS=4`、`UV_HTTP_RETRIES=3`、`UV_HTTP_TIMEOUT=600`,因为镜像会限流 uv 默认的 8 并发(HTTP 429)。对当前网络直接封锁的镜像(本机 TUNA 现在返回 403)会被排到后面而不是让安装失败;每个镜像的实测速度都显示在面板上。
116
+
97
117
  ## 验证
98
118
 
99
119
  ```text
@@ -243,12 +263,27 @@ npm run dump
243
263
 
244
264
  ```text
245
265
  cordis.patch.yml 集成本体:两行 insert
246
- lib/index.js 宿主半边:windows_mcp_doctor + 启动审计(零依赖)
247
- test/plugin.test.mjs 补丁合成测试 + 恶意 ctx 测试
266
+ lib/index.js 宿主半边:路由 + windows_mcp_doctor + 启动审计(零依赖)
267
+ lib/dependency.js 依赖状态、uv 调用封装、profile 桥行写入
268
+ lib/pypi.js PyPI 索引解析与版本比较(纯函数)
269
+ lib/mirrors.js 镜像候选、限速探测与排序
270
+ lib/installs.js 按版本隔离的工具目录 + 版本状态文件
271
+ lib/updater.js 版本管理:检查 / 安装 / 激活 / 回退
272
+ lib/client.js 网页半边:输入框开关 + 插件面板
273
+ test/plugin.test.mjs 补丁合成测试 + 恶意 ctx 测试 + 面板测试
274
+ test/update.test.mjs 版本解析、镜像排序、版本隔离与回退状态机测试
248
275
  scripts/dump-config.mjs 面向影子 profile 的合成调试脚本
249
276
  tools/ 零依赖调试探针(不发布到 npm)
250
277
  ```
251
278
 
279
+ 要对着真实网络和本机 profile 验证升级链路,用 `tools/update-e2e.mjs`:
280
+
281
+ ```powershell
282
+ node tools/update-e2e.mjs --check # 探测镜像并读出最新稳定版
283
+ node tools/update-e2e.mjs --install 0.8.8 # 真下载、真激活、真改桥行
284
+ node tools/update-e2e.mjs --rollback
285
+ ```
286
+
252
287
  欢迎贡献;请保持 `lib/index.js` 无运行时依赖,并保持 `apply()` 永不抛错。
253
288
 
254
289
  ## 致谢与许可
package/lib/client.js CHANGED
@@ -31,6 +31,7 @@ window.__ModuleLoader__.load({
31
31
  var PREPARE_PATH = "/dsh-windows-mcp/prepare";
32
32
  var PREPARE_CANCEL_PATH = "/dsh-windows-mcp/prepare/cancel";
33
33
  var BRIDGE_PATH = "/dsh-windows-mcp/bridge";
34
+ var UPDATE_PATH = "/dsh-windows-mcp/update";
34
35
  var STYLE_ID = PACKAGE_ID + "-computer-use-styles";
35
36
  var LABEL = "Computer Use";
36
37
  /** Only `slots` is injected: it is certainly provided, and a declaration for
@@ -300,6 +301,22 @@ window.__ModuleLoader__.load({
300
301
  ready: "依赖已就绪,桌面工具可用(记得把 Computer Use 开关打开)。",
301
302
  restart: "已把桥指向新安装的版本,重启 DSH 后生效。",
302
303
  hostDown: "连不上宿主",
304
+ version: "运行版本",
305
+ latest: "最新版本",
306
+ previous: "可回退",
307
+ kept: "已保留版本",
308
+ mirror: "镜像",
309
+ updateAvailable: "有新版本",
310
+ upToDate: "已是最新",
311
+ notChecked: "还没检查",
312
+ check: "检查更新",
313
+ checking: "正在测速…",
314
+ updateTo: "更新到",
315
+ rollback: "回退到",
316
+ installVersion: "安装版本",
317
+ versionInstalled: "版本已切换,重启 DSH 生效。",
318
+ noPrevious: "没有可回退的版本",
319
+ updateHint: "更新只下载新版本、旧版本原样保留;出问题一键回退,不需要重装依赖。",
303
320
  },
304
321
  en: {
305
322
  title: "Windows desktop automation (Windows-MCP)",
@@ -327,6 +344,22 @@ window.__ModuleLoader__.load({
327
344
  ready: "Dependency ready — the desktop tools are available (turn the Computer Use switch on).",
328
345
  restart: "The bridge now points at the freshly installed build; restart DSH to apply it.",
329
346
  hostDown: "Host unreachable",
347
+ version: "Running version",
348
+ latest: "Latest version",
349
+ previous: "Rollback to",
350
+ kept: "Kept versions",
351
+ mirror: "Mirror",
352
+ updateAvailable: "update available",
353
+ upToDate: "up to date",
354
+ notChecked: "not checked yet",
355
+ check: "Check for updates",
356
+ checking: "Probing mirrors…",
357
+ updateTo: "Update to",
358
+ rollback: "Roll back to",
359
+ installVersion: "Install version",
360
+ versionInstalled: "Version switched — restart DSH to apply it.",
361
+ noPrevious: "no previous version to roll back to",
362
+ updateHint: "An update only downloads the new build; the old one is kept as-is. Roll back with one click — no reinstall.",
330
363
  },
331
364
  };
332
365
 
@@ -381,6 +414,19 @@ window.__ModuleLoader__.load({
381
414
  });
382
415
  }
383
416
 
417
+ /** POST JSON — used by the version manager (check / install / activate / rollback). */
418
+ function postJson(path, payload) {
419
+ return fetch(apiUrl(path), {
420
+ method: "POST",
421
+ headers: { accept: "application/json", "content-type": "application/json" },
422
+ body: JSON.stringify(payload || {}),
423
+ }).then(function (response) {
424
+ return response.json().then(function (body) {
425
+ return { ok: response.ok, status: response.status, body: body };
426
+ });
427
+ });
428
+ }
429
+
384
430
  function statusDot(tone) {
385
431
  return React.createElement("span", { className: "dwmc-dot", "data-tone": tone });
386
432
  }
@@ -419,6 +465,19 @@ window.__ModuleLoader__.load({
419
465
  var installed = dependency.installed === true;
420
466
  var bridged = typeof state.bridged === "number" ? state.bridged : null;
421
467
  var log = Array.isArray(prepare.log) ? prepare.log : [];
468
+ var update = state.update || {};
469
+ var versions = update.versions || {};
470
+ var check = update.check || {};
471
+ var install = update.install || {};
472
+ var updateAvailable = versions.updateAvailable === true && typeof check.latest === "string";
473
+ var mirrors = Array.isArray(check.mirrors) ? check.mirrors : [];
474
+ var bestMirror = null;
475
+ for (var mirrorIndex = 0; mirrorIndex < mirrors.length; mirrorIndex += 1) {
476
+ if (mirrors[mirrorIndex] && mirrors[mirrorIndex].id === check.best) {
477
+ bestMirror = mirrors[mirrorIndex];
478
+ break;
479
+ }
480
+ }
422
481
  var profile = dependency.profile && typeof dependency.profile === "object" ? dependency.profile : null;
423
482
  var profileTarget =
424
483
  profile === null
@@ -441,10 +500,28 @@ window.__ModuleLoader__.load({
441
500
  });
442
501
  };
443
502
 
503
+ var actUpdate = function (payload) {
504
+ setBusy(true);
505
+ postJson(UPDATE_PATH, payload)
506
+ .catch(function () {
507
+ /* the next poll reports the truth */
508
+ })
509
+ .then(function () {
510
+ setBusy(false);
511
+ reload();
512
+ });
513
+ };
514
+
515
+ var speedText = function (bytesPerSecond) {
516
+ var value = Number(bytesPerSecond);
517
+ if (!isFinite(value) || value <= 0) return "—";
518
+ return value >= 1048576 ? (value / 1048576).toFixed(1) + " MB/s" : Math.max(1, Math.round(value / 1024)) + " KB/s";
519
+ };
520
+
444
521
  var head = React.createElement(
445
522
  "div",
446
523
  { className: "dwmc-panel__head", key: "head" },
447
- statusDot(running ? "warn" : installed ? "on" : "warn"),
524
+ statusDot(running ? "warn" : updateAvailable ? "warn" : installed ? "on" : "warn"),
448
525
  React.createElement("span", null, t.title),
449
526
  );
450
527
 
@@ -452,11 +529,23 @@ window.__ModuleLoader__.load({
452
529
  if (!compact) {
453
530
  var rows = [
454
531
  [t.dependency, installed ? t.installed + (dependency.version ? " " + dependency.version : "") : dependency.uv === null ? t.uvMissing : t.missing],
455
- [t.tool, dependency.executable || "—"],
456
- [t.bridge, bridged === null ? "—" : bridged + " " + t.bridgedTools],
457
- [t.profile, profileTarget],
458
- [t.toggle, state.enabled === true ? t.on : t.off],
532
+ [t.version, versions.active ? "v" + versions.active : dependency.version ? "v" + dependency.version : "—"],
533
+ [
534
+ t.latest,
535
+ typeof check.latest === "string"
536
+ ? "v" + check.latest + " · " + (updateAvailable ? t.updateAvailable : t.upToDate)
537
+ : t.notChecked,
538
+ ],
459
539
  ];
540
+ if (typeof versions.previous === "string") rows.push([t.previous, "v" + versions.previous]);
541
+ if (Array.isArray(versions.available) && versions.available.length > 0) {
542
+ rows.push([t.kept, versions.available.map(function (version) { return "v" + version; }).join(", ")]);
543
+ }
544
+ if (bestMirror !== null) rows.push([t.mirror, bestMirror.label + " · " + speedText(bestMirror.speedBps)]);
545
+ rows.push([t.tool, dependency.executable || "—"]);
546
+ rows.push([t.bridge, bridged === null ? "—" : bridged + " " + t.bridgedTools]);
547
+ rows.push([t.profile, profileTarget]);
548
+ rows.push([t.toggle, state.enabled === true ? t.on : t.off]);
460
549
  children.push(
461
550
  React.createElement(
462
551
  "dl",
@@ -473,7 +562,13 @@ window.__ModuleLoader__.load({
473
562
 
474
563
  if (running) {
475
564
  children.push(React.createElement("div", { className: "dwmc-progress", key: "progress" }, React.createElement("i")));
476
- children.push(React.createElement("p", { className: "dwmc-note", key: "running" }, t.running));
565
+ children.push(
566
+ React.createElement(
567
+ "p",
568
+ { className: "dwmc-note", key: "running" },
569
+ (install.version ? t.installVersion + " v" + install.version + "… " : "") + t.running,
570
+ ),
571
+ );
477
572
  var tail = log.slice(-3).join("\n");
478
573
  if (tail) children.push(React.createElement("pre", { className: "dwmc-log", key: "log" }, tail));
479
574
  } else if (prepare.phase === "failed") {
@@ -492,17 +587,46 @@ window.__ModuleLoader__.load({
492
587
  } else if (!installed && !running) {
493
588
  children.push(React.createElement("p", { className: "dwmc-note", key: "estimate" }, t.estimate));
494
589
  }
590
+ if (!compact && (running || updateAvailable)) {
591
+ children.push(React.createElement("p", { className: "dwmc-note", key: "updateHint" }, t.updateHint));
592
+ }
495
593
  if (view.phase === "error") {
496
594
  children.push(React.createElement("p", { className: "dwmc-error", key: "offline" }, t.hostDown + ": " + view.error));
497
595
  }
498
596
 
499
597
  var actions = [];
500
598
  if (running) {
501
- actions.push(actionButton(t.cancel, function () { act(PREPARE_CANCEL_PATH); }, busy, true));
502
- } else if (!installed) {
503
- actions.push(actionButton(t.install, function () { act(PREPARE_PATH); }, busy));
599
+ actions.push(actionButton(t.cancel, function () { actUpdate({ action: "cancel" }); }, busy, true));
504
600
  } else {
505
- actions.push(actionButton(t.reinstall, function () { act(PREPARE_PATH); }, busy, true));
601
+ actions.push(
602
+ actionButton(
603
+ check.phase === "checking" ? t.checking : t.check,
604
+ function () { actUpdate({ action: "check" }); },
605
+ busy,
606
+ true,
607
+ ),
608
+ );
609
+ if (updateAvailable) {
610
+ actions.push(
611
+ actionButton(
612
+ t.updateTo + " v" + check.latest,
613
+ function () { actUpdate({ action: "install", version: check.latest }); },
614
+ busy,
615
+ ),
616
+ );
617
+ }
618
+ if (typeof versions.previous === "string") {
619
+ actions.push(
620
+ actionButton(
621
+ t.rollback + " v" + versions.previous,
622
+ function () { actUpdate({ action: "rollback" }); },
623
+ busy,
624
+ true,
625
+ ),
626
+ );
627
+ }
628
+ if (!installed) actions.push(actionButton(t.install, function () { act(PREPARE_PATH); }, busy));
629
+ else actions.push(actionButton(t.reinstall, function () { act(PREPARE_PATH); }, busy, true));
506
630
  if (prepare.bridge && prepare.bridge.ok !== true) {
507
631
  actions.push(actionButton(t.pointBridge, function () { act(BRIDGE_PATH); }, busy, true));
508
632
  }
@@ -601,7 +725,7 @@ window.__ModuleLoader__.load({
601
725
 
602
726
  exports.apply = apply;
603
727
  exports.inject = inject;
604
- exports.__internals = { CSS: CSS, apiUrl: apiUrl, tooltipFor: tooltipFor, LABEL: LABEL, STATE_PATH: STATE_PATH };
728
+ exports.__internals = { CSS: CSS, apiUrl: apiUrl, tooltipFor: tooltipFor, LABEL: LABEL, STATE_PATH: STATE_PATH, UPDATE_PATH: UPDATE_PATH };
605
729
  return module.exports;
606
730
  },
607
731
  });
package/lib/dependency.js CHANGED
@@ -80,10 +80,15 @@ export function windowsMcpExecutable() {
80
80
  return null;
81
81
  }
82
82
 
83
- /** Version of the installed tool, read from its dist-info directory. */
84
- export function windowsMcpVersion(executable = windowsMcpExecutable()) {
85
- if (executable === null) return null;
86
- const directory = windowsMcpToolDirectory();
83
+ /**
84
+ * Version of a `uv tool` installation, read from the dist-info directory inside
85
+ * the tool's environment. Works for the global tool root and for the managed
86
+ * per-version directories this plugin keeps (see `installs.js`).
87
+ * @param toolDirectory - the directory that contains `Scripts/` and `Lib/`,
88
+ * i.e. `…/tools/windows-mcp` or `…/tools/<version>/windows-mcp`.
89
+ */
90
+ export function toolVersionIn(toolDirectory) {
91
+ if (typeof toolDirectory !== "string" || toolDirectory === "") return null;
87
92
  const packages = [];
88
93
  const collect = (sitePackages) => {
89
94
  try {
@@ -94,10 +99,10 @@ export function windowsMcpVersion(executable = windowsMcpExecutable()) {
94
99
  /* layout absent */
95
100
  }
96
101
  };
97
- collect(join(directory, "Lib", "site-packages"));
102
+ collect(join(toolDirectory, "Lib", "site-packages"));
98
103
  try {
99
- for (const entry of readdirSync(join(directory, "lib"), { withFileTypes: true })) {
100
- if (entry.isDirectory() && /^python\d/u.test(entry.name)) collect(join(directory, "lib", entry.name, "site-packages"));
104
+ for (const entry of readdirSync(join(toolDirectory, "lib"), { withFileTypes: true })) {
105
+ if (entry.isDirectory() && /^python\d/u.test(entry.name)) collect(join(toolDirectory, "lib", entry.name, "site-packages"));
101
106
  }
102
107
  } catch {
103
108
  /* POSIX layout absent */
@@ -109,9 +114,17 @@ export function windowsMcpVersion(executable = windowsMcpExecutable()) {
109
114
  return null;
110
115
  }
111
116
 
117
+ /** Version of the globally installed tool, read from its dist-info directory. */
118
+ export function windowsMcpVersion(executable = windowsMcpExecutable()) {
119
+ if (executable === null) return null;
120
+ return toolVersionIn(windowsMcpToolDirectory());
121
+ }
122
+
112
123
  /** The pinned upstream version our patch mounts, e.g. `0.8.8`, or null. */
113
124
  export function windowsMcpVersionPin(patchText) {
114
- const match = /windows-mcp@([0-9][^\s,'"\]]*)/u.exec(String(patchText ?? ""));
125
+ // Stop at anything that ends a version in YAML or prose: whitespace, quotes,
126
+ // commas, closing brackets, and the backtick/paren used in comments.
127
+ const match = /windows-mcp@([0-9][^\s,'"\]`);]*)/u.exec(String(patchText ?? ""));
115
128
  return match === null ? null : match[1];
116
129
  }
117
130
 
@@ -143,11 +156,67 @@ export function dependencySnapshot(resolver, pinned, env = process.env) {
143
156
  * install
144
157
  * ------------------------------------------------------------------ */
145
158
 
159
+ /**
160
+ * Environment for `uv tool install`.
161
+ *
162
+ * `UV_CONCURRENT_DOWNLOADS` is deliberately below uv's default of 8: the
163
+ * mainland mirrors rate-limit aggressive parallelism (measured: USTC answered
164
+ * HTTP 429 while unzipping a dependency when uv fetched 8 files at once), and a
165
+ * slow-but-successful install beats a fast-but-failing one.
166
+ *
167
+ * @param toolDirectory - when set, the install is redirected into that tool
168
+ * root (`UV_TOOL_DIR`) instead of the global one, which is how this plugin
169
+ * keeps several upstream versions side by side.
170
+ */
171
+ export function uvInstallEnvironment(toolDirectory = null) {
172
+ const environment = {
173
+ ...process.env,
174
+ UV_HTTP_TIMEOUT: "600",
175
+ UV_CONCURRENT_DOWNLOADS: "4",
176
+ UV_HTTP_RETRIES: "3",
177
+ };
178
+ if (typeof toolDirectory === "string" && toolDirectory !== "") environment.UV_TOOL_DIR = toolDirectory;
179
+ return environment;
180
+ }
181
+
182
+ /**
183
+ * Start one `uv tool install` and stream its output to `onLine`.
184
+ * @returns `{ child, args }` — the caller owns the child's lifecycle.
185
+ */
186
+ export function spawnUvInstall(options = {}) {
187
+ const uv = options.uv;
188
+ const spec = options.spec;
189
+ const indexUrl = typeof options.indexUrl === "string" && options.indexUrl !== "" ? options.indexUrl : null;
190
+ const toolDirectory = typeof options.toolDirectory === "string" && options.toolDirectory !== "" ? options.toolDirectory : null;
191
+ const onLine = typeof options.onLine === "function" ? options.onLine : () => {};
192
+ const args = ["tool", "install", "--force"];
193
+ if (indexUrl !== null) args.push("--default-index", indexUrl);
194
+ args.push(spec);
195
+ const child = spawn(uv, args, {
196
+ cwd: typeof options.cwd === "string" ? options.cwd : homedir(),
197
+ env: { ...uvInstallEnvironment(toolDirectory), ...(options.env ?? {}) },
198
+ windowsHide: true,
199
+ stdio: ["ignore", "pipe", "pipe"],
200
+ });
201
+ try {
202
+ child.stdout?.on("data", onLine);
203
+ child.stderr?.on("data", onLine);
204
+ } catch {
205
+ /* a child without pipes still closes */
206
+ }
207
+ return { child, args };
208
+ }
209
+
146
210
  /**
147
211
  * One `uv tool install` at a time, with a bounded log the page can follow.
148
212
  * @param options.warn - warning sink.
149
213
  * @param options.resolver - PATH lookup used to find `uv`.
150
214
  * @param options.pinned - upstream version pin.
215
+ * @param options.indexUrl - PyPI index to install from (a mirror), or null.
216
+ * @param options.toolDirectory - `UV_TOOL_DIR` for an isolated install, or null
217
+ * for the global tool root.
218
+ * @param options.resolveExecutable - where to look for the installed executable
219
+ * afterwards; defaults to the global `uv tool` installation.
151
220
  * @param options.afterInstall - called with the installed executable, returns
152
221
  * the bridge-write outcome that `snapshot()` then reports.
153
222
  */
@@ -202,22 +271,19 @@ export function createPrepare(options = {}) {
202
271
  return { started: false, reason: state.error, state: this.snapshot() };
203
272
  }
204
273
  const spec = options.pinned === null || options.pinned === undefined ? WINDOWS_MCP_TOOL : `${WINDOWS_MCP_TOOL}@${options.pinned}`;
274
+ const indexUrl = typeof options.indexUrl === "string" && options.indexUrl !== "" ? options.indexUrl : null;
275
+ const toolDirectory = typeof options.toolDirectory === "string" && options.toolDirectory !== "" ? options.toolDirectory : null;
276
+ const resolveExecutable = typeof options.resolveExecutable === "function" ? options.resolveExecutable : windowsMcpExecutable;
205
277
  state.phase = "running";
206
278
  state.startedAt = new Date().toISOString();
207
279
  state.finishedAt = null;
208
280
  state.error = null;
209
281
  state.bridge = null;
210
- state.log = [`$ ${uv} tool install --force ${spec}`];
282
+ const command = [uv, "tool", "install", "--force", ...(indexUrl === null ? [] : ["--default-index", indexUrl]), spec].join(" ");
283
+ state.log = [`$ ${command}${toolDirectory === null ? "" : ` (UV_TOOL_DIR=${toolDirectory})`}`];
211
284
  try {
212
- const running = spawn(uv, ["tool", "install", "--force", spec], {
213
- cwd: homedir(),
214
- env: { ...process.env, UV_HTTP_TIMEOUT: "600", UV_CONCURRENT_DOWNLOADS: "8" },
215
- windowsHide: true,
216
- stdio: ["ignore", "pipe", "pipe"],
217
- });
285
+ const { child: running } = spawnUvInstall({ uv, spec, indexUrl, toolDirectory, onLine: append });
218
286
  child = running;
219
- running.stdout?.on("data", append);
220
- running.stderr?.on("data", append);
221
287
  running.on("error", (error) => settle("failed", String(error)));
222
288
  running.on("close", (code) => {
223
289
  if (state.phase === "cancelled") return;
@@ -225,7 +291,7 @@ export function createPrepare(options = {}) {
225
291
  settle("failed", `uv exited with code ${code}`);
226
292
  return;
227
293
  }
228
- const executable = windowsMcpExecutable();
294
+ const executable = resolveExecutable();
229
295
  if (executable === null) {
230
296
  settle("failed", "uv reported success, but no windows-mcp executable appeared in the tool directory.");
231
297
  return;