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 +43 -1
- package/LICENSE +0 -6
- package/README.md +38 -2
- package/README.zh.md +37 -2
- package/lib/client.js +135 -11
- package/lib/dependency.js +84 -18
- package/lib/index.js +154 -27
- package/lib/installs.js +196 -0
- package/lib/mirrors.js +236 -0
- package/lib/pypi.js +171 -0
- package/lib/updater.js +496 -0
- package/package.json +1 -1
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.
|
|
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
|
+
[](https://www.npmjs.com/package/dsh-windows-mcp)
|
|
6
|
+
[](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
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/dsh-windows-mcp)
|
|
6
|
+
[](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
|
|
247
|
-
|
|
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.
|
|
456
|
-
[
|
|
457
|
-
|
|
458
|
-
|
|
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(
|
|
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 () {
|
|
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(
|
|
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
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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(
|
|
102
|
+
collect(join(toolDirectory, "Lib", "site-packages"));
|
|
98
103
|
try {
|
|
99
|
-
for (const entry of readdirSync(join(
|
|
100
|
-
if (entry.isDirectory() && /^python\d/u.test(entry.name)) collect(join(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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;
|