dsh-app-updater 1.3.4 → 1.3.6
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 +87 -0
- package/README.en.md +255 -243
- package/README.md +284 -275
- package/RELEASING.md +14 -1
- package/lib/elevate.js +56 -6
- package/lib/engine.js +1423 -1315
- package/lib/index.js +60 -8
- package/lib/macos.js +494 -494
- package/lib/platform.js +199 -199
- package/lib/proc.js +91 -91
- package/lib/registry.js +131 -120
- package/lib/version.js +120 -81
- package/lib/winget.js +340 -337
- package/package.json +68 -68
- package/test/unit.mjs +95 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,93 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [1.3.6] — 2026-11-02
|
|
6
|
+
|
|
7
|
+
**GitHub 仓库改名**:`19952730389/dsh-app-updater` → **`19952730389/dsh-appupdater`**
|
|
8
|
+
(旧 URL 由 GitHub 自动重定向;README / package.json 里的仓库链接与源码安装命令已指向新名,
|
|
9
|
+
tarball 资产名 `dsh-app-updater.tgz` 不变,各市场 `releases/latest` 链接不受影响)。
|
|
10
|
+
|
|
11
|
+
改名原因:deepseek.club Plugin Hub 的插件 slug `dsh-app-updater` 已被另一个同名插件占用
|
|
12
|
+
(`m1452700576/dsh-app-updater`,用途是检测并更新 DSH 自身),在那家上架必须避开撞名。
|
|
13
|
+
|
|
14
|
+
- **npm 包名、插件 id、HTTP 路由前缀(`/dsh-app-updater/*`)、数据目录维持不变**:
|
|
15
|
+
npm 的防混淆规则不允许同一作者发布过于相似的名字(`dsh-appupdater` 被 registry 拒绝),
|
|
16
|
+
与其把生态拆成两套名字(用户迁移成本 + 市场条目重写),不如维持现状:
|
|
17
|
+
`dsh plugin add dsh-app-updater` 照旧。已配置的环境变量与插件配置也全部沿用。
|
|
18
|
+
- 代码与 1.3.5 逐字节一致,仅仓库 URL 与文档差异。
|
|
19
|
+
|
|
20
|
+
## [1.3.5] — 2026-11-02
|
|
21
|
+
|
|
22
|
+
两轮对抗式复查的合并收口(1.3.4 之后),修掉 9 个真实缺陷、补上 1 处文档与实现不一致的承诺。
|
|
23
|
+
这一版没有新功能 —— 全是「判断错误 / 静默失败 / 条件触发的安全面」类问题。
|
|
24
|
+
|
|
25
|
+
### 修复
|
|
26
|
+
|
|
27
|
+
- **尾缀字典序把 build 号判反**(`lib/version.js`)。数字段一致后,尾缀(`-build10`、
|
|
28
|
+
`-r100`、`-beta2`、Java 的 `8u202` 这类)原来是**整串字典序**比较:`-build9` > `-build10`、
|
|
29
|
+
`8u99` > `8u202` —— 位数一变顺序就翻转。对插件来说这是静默漏报:已装 `1.2.3-build9`、
|
|
30
|
+
有 `1.2.3-build10`,`isNewer()` 返回 false → 用户看到「已是最新」;复核路径还会把成功
|
|
31
|
+
安装误判「没有变化」。现在尾缀按**自然序**比较(数字块按数值、字母块按字典序,
|
|
32
|
+
`-beta2 < -beta10`),保留原有语义:正式版 > 任何预发布尾缀。
|
|
33
|
+
- **Windows 扫描不检查取消/超时,半截输出伪装成「0 个可更新」**(`lib/engine.js scanApps`)。
|
|
34
|
+
取消或 240s 超时杀掉 `winget list` 后,半截 stdout 被解析成残缺清单;
|
|
35
|
+
`if (upRes.ok || upRes.stdout)` 又把超时残留字符串真值当成功 → `hasUpdate` 全 false →
|
|
36
|
+
整表覆盖上一次完整结果。macOS 路径 1.3.4 修过同款,这次补上 Windows 侧:
|
|
37
|
+
取消一律向上抛(不算故障、不写红横幅),非零退出/超时不再解析,降级注册表源并如实
|
|
38
|
+
警告「本次清单可能不完整」。
|
|
39
|
+
- **注册表读取会被一条无关的 PowerShell 警告整体误伤**(`lib/registry.js`)。stderr 原来与
|
|
40
|
+
stdout 共用同一缓冲数组:任何一条执行策略提示或 .NET 横幅插进 JSON 中部 → 整体解析失败
|
|
41
|
+
→ 注册表源全量丢弃,用户以为这台机器读不到已装应用。winget 侧同病(`lib/winget.js`):
|
|
42
|
+
stderr 混进定宽表还可能伪造数据行。现在两个进程都分桶收集,`stderr` 字段如实传给错误分类。
|
|
43
|
+
- **哈希校验:文档承诺 ≠ 实际行为 → 现在真的做了**(`lib/engine.js`)。README 写「下载时按清单
|
|
44
|
+
核对 SHA-256」——winget 自己确实会核对(我们从不传 `--skip-hash-check`),但**执行前的那个文件**
|
|
45
|
+
此前没有任何本地校验。而这个文件紧接着会以**管理员**身份静默执行。1.3.5 起:
|
|
46
|
+
- `parseInstallerManifest` 按 Installer 块配对收集 `Architecture → InstallerSha256`
|
|
47
|
+
(兼容 `- Architecture:` 列表首项写法);
|
|
48
|
+
- `verifyInstallerHash` 流式(`createReadStream`,不整读几百 MB 的包)复核实际哈希;
|
|
49
|
+
- 配对策略:先按本机架构键,再用文件名别名(`64-bit`/`x64`/`arm64`/`aarch64`…),
|
|
50
|
+
单哈希清单直接用;**多架构配对不出时宁可跳过并记 info**,绝不拿别架构哈希把正确文件判成篡改;
|
|
51
|
+
- 不一致 → **拒绝执行**,报「期望/实际」两个哈希,照旧走 `winget upgrade` 兜底;
|
|
52
|
+
- 更新结果新增 `hash: verified|skipped|mismatch`。
|
|
53
|
+
- **兼容模式(legacy loopback)可被任意网页 CSRF 驱动提权安装**(`lib/index.js`)。开启
|
|
54
|
+
`allowLegacyLoopback` 后,第三方网页的 `fetch("http://127.0.0.1:…/update",{mode:"no-cors"})`
|
|
55
|
+
天然不带 Cookie → 过「无凭据才豁免」那条规则 → 无认证触发「下载任意包 + UAC 提权静默安装」。
|
|
56
|
+
现在豁免路径加来源围栏:`Origin`/`Referer` 存在且与 `Host` 不同源、或
|
|
57
|
+
`Sec-Fetch-Site: cross-site`,一律 403(浏览器禁止脚本伪造这些头,判定可靠;本机脚本与
|
|
58
|
+
IPC 桥不带这些头,不受影响)。
|
|
59
|
+
- **取消/超时杀不掉提权安装树**(`lib/elevate.js`)。旧实现 Node 侧只能杀**启动器**;
|
|
60
|
+
提权副本与安装器在独立进程树里继续跑,而队列已排到下一个应用(并发双装 + 幽灵安装)。
|
|
61
|
+
现在助手脚本(本身就是提权进程)自己盯 `cancel.flag`(500ms 轮询),看到就
|
|
62
|
+
`taskkill /T /F` 杀整棵安装器树再报告退出码;读不到退出码时按失败处理,绝不伪造 0。
|
|
63
|
+
- **「取消后立刻重试」的竞态**(`lib/index.js`)。旧任务 finally 无条件清 `control` 槽位,
|
|
64
|
+
会把新任务句柄清成 null → `/cancel` 声称成功但新任务不可取消,旧半截结果还会覆盖新结果。
|
|
65
|
+
现在 finally 做所有权检查(`control.scanAbort === abort` 才清),被接管的任务结果作废。
|
|
66
|
+
- **macOS 更新复核失败仍报「更新完成」**(`lib/engine.js runMacosUpdates`)。
|
|
67
|
+
`brew list` 读空时此前直接 `log success` 且 `entry.ok=true` 无标注;现在与 Windows 准则
|
|
68
|
+
一致:读不到版本 → note 标「未复核:brew list 读不到更新后的版本」+ warn 日志;
|
|
69
|
+
取消/超时优先判错,不给误导性原因。
|
|
70
|
+
- **注册表 PowerShell 枚举没有取消通道**(`lib/registry.js`)。扫描取消后仍跑满 60s;
|
|
71
|
+
现在 `spawn({ signal })` 直通,取消立刻杀。
|
|
72
|
+
- **`normalizeName` 的 `\b版本\b` 是死分支**(`lib/registry.js`)。JS `\b` 只认 ASCII 边界,
|
|
73
|
+
「版」「本」都是非词字符 → 永不命中 → 「XXX 版本 1.2.3」与 winget 的「XXX」认不成同一条,
|
|
74
|
+
同一软件重复列出。改为字面匹配 `版本`。
|
|
75
|
+
- **中文 winget 的落盘行匹配不到**(`lib/engine.js downloadInstaller`)。只认英文
|
|
76
|
+
`Installer downloaded:`,中文系统的「安装程序已下载:」落到不可靠的目录 diff,失手时报
|
|
77
|
+
「没找到安装包」→ 触发兜底**把同一个包再下载一遍**。正则改为中英双语(与
|
|
78
|
+
`classifyWingetError` 一致)。
|
|
79
|
+
|
|
80
|
+
### 文档
|
|
81
|
+
|
|
82
|
+
- README / README.en 的执行流程表补充「执行前本地复核哈希(1.3.5 起)」,使承诺与实现一致。
|
|
83
|
+
|
|
84
|
+
### 测试
|
|
85
|
+
|
|
86
|
+
- `node test/unit.mjs` **184/184**(+31:自然序 8、清单哈希配对 4、哈希端到端 3、
|
|
87
|
+
来源围栏 9、归一化 2、双语匹配 3、取消检查 2)、`node test/render.mjs` **51/51**、
|
|
88
|
+
`node test/deny.mjs` **12/12**。
|
|
89
|
+
- 哈希端到端用 `mkdtemp` 临时目录真算真比:一致 → 通过;不一致 → 拒绝且错误里带两个哈希;
|
|
90
|
+
清单缺哈希 → `skipped` 而不是失败。
|
|
91
|
+
|
|
5
92
|
## [1.3.4] — 2026-11-01
|
|
6
93
|
|
|
7
94
|
1.3.3 发布后对着同样几台真实机器做了一轮对抗式复查,修掉 5 个「1.3.3 自己引入或没修干净」的问题。
|
package/README.en.md
CHANGED
|
@@ -1,244 +1,256 @@
|
|
|
1
|
-
# dsh-app-updater — App Updater for DeepSeek Harness
|
|
2
|
-
|
|
3
|
-
**English** | [简体中文](README.md)
|
|
4
|
-
[](https://www.npmjs.com/package/dsh-app-updater)
|
|
5
|
-

|
|
6
|
-

|
|
7
|
-

|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Detect every installed application that has a newer version, then update them
|
|
11
|
-
with one click — downloading the **full installer from each vendor's own website** and
|
|
12
|
-
installing it silently. **The detected applications are never launched.**
|
|
13
|
-
Windows (winget) and macOS (Homebrew), architecture-aware: ARM64 and x64/Intel installers are
|
|
14
|
-
chosen to match the machine.
|
|
15
|
-
|
|
16
|
-
Where to find it: DeepSeek Harness → Settings → **应用更新 (App Updater)**
|
|
17
|
-
|
|
18
|
-
## Install
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
# npm package (recommended; also one-click installable from the plugin center)
|
|
22
|
-
dsh plugin --profile web add dsh-app-updater
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Without npm, install from source or from the prebuilt tarball:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
# source repository
|
|
29
|
-
dsh plugin --profile web add github:19952730389/dsh-
|
|
30
|
-
|
|
31
|
-
# prebuilt tgz (see Releases)
|
|
32
|
-
dsh plugin --profile web add https://github.com/19952730389/dsh-
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Restart DSH afterwards; the plugin lives at **Settings → 应用更新**. The Electron-managed
|
|
36
|
-
`desktop` profile does not accept `dsh plugin` writes, so link the package into that
|
|
37
|
-
profile's `node_modules` and add it to `dsh.profile.bundles` manually.
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## What it actually does
|
|
42
|
-
|
|
43
|
-
| Step | How |
|
|
44
|
-
|---|---|
|
|
45
|
-
| 1. List installed apps | `winget list` (apps known to the winget catalog plus Store/MSIX packages) **and** the three Windows registry `Uninstall` keys (apps the catalog does not know). Read-only: no application is ever started. |
|
|
46
|
-
| 2. Decide whether an update exists | `winget upgrade --include-unknown` gives "installed version → available version" from the official winget catalog, merged into the inventory by package id. |
|
|
47
|
-
| 3. One-click update | `winget download` fetches the **vendor's full installer** from the URL recorded in the winget manifest (SHA-256 verified), the downloaded manifest supplies the publisher's own `InstallerSwitches.Silent`, the installer runs silently, and the resulting version is re-read with `winget list --id`. |
|
|
48
|
-
| 4. Fallback | If the vendor re-signed the installer (hash no longer matches the catalog) or the manifest forbids direct download, it falls back to `winget upgrade --id … --exact --silent` — still the full installer from the vendor, with winget doing the fetching and installing. |
|
|
49
|
-
|
|
50
|
-
Those download URLs really are the vendors' own: `github.com/git-for-windows/…`,
|
|
51
|
-
`dl.google.com/dl/chrome/install/…`, `download.cpuid.com/cpu-z/…`, `dl.hdslb.com/…`.
|
|
52
|
-
Both the installer and its `.yaml` manifest are kept in the download folder, so the
|
|
53
|
-
`InstallerUrl`, `InstallerSha256` and silent switches can be audited afterwards.
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## Usage
|
|
58
|
-
|
|
59
|
-
1. Open **Settings → 应用更新**.
|
|
60
|
-
2. Click **刷新检测** (nothing runs in the background; scanning only happens when you ask).
|
|
61
|
-
3. The table sorts updateable apps first. Use the search box and the
|
|
62
|
-
**全部 / 仅可更新 / 未收录** segments to narrow it down.
|
|
63
|
-
4. Tick the apps to update (or press **全部更新**), or update a single app from its row.
|
|
64
|
-
5. While updating you get a progress bar, the current app name and a per-item log;
|
|
65
|
-
**取消** stops the items that have not started yet.
|
|
66
|
-
6. **打开下载目录** reveals the downloaded vendor installers in Explorer.
|
|
67
|
-
|
|
68
|
-
### Status values
|
|
69
|
-
|
|
70
|
-
| Status | Meaning |
|
|
71
|
-
|---|---|
|
|
72
|
-
| **有新版本** | The catalog reports a higher version; one-click update is available. |
|
|
73
|
-
| **已是最新** | No newer version in the catalog. |
|
|
74
|
-
| **未收录** | An update exists in the catalog but this plugin cannot drive it (see reasons below). |
|
|
75
|
-
|
|
76
|
-
| Reason | Meaning |
|
|
77
|
-
|---|---|
|
|
78
|
-
| 软件目录未收录 | The app is not in the winget catalog (registry-only software), so its official update cannot be discovered or downloaded. |
|
|
79
|
-
| 由应用商店管理 | MSIX / Store app — update it through Microsoft Store. |
|
|
80
|
-
| 版本未知 | The installed version cannot be read (winget reports `Unknown`); updating is still allowed. |
|
|
81
|
-
| 系统组件 | System component / runtime; not driven by this plugin by default. |
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## Settings
|
|
86
|
-
|
|
87
|
-
State file: `%USERPROFILE%\.dsh\dsh-app-updater\state.json`
|
|
88
|
-
|
|
89
|
-
| Key | Default | Meaning |
|
|
90
|
-
|---|---|---|
|
|
91
|
-
| `includeStoreApps` | `true` | List store apps too (MSIX on Windows, App Store on macOS; display only). |
|
|
92
|
-
| `includeRegistryOnly` | `true` | Windows: fill in apps missing from the winget catalog using the registry (display only). |
|
|
93
|
-
| `includeUnmanagedApps` | `true` | macOS: also list apps that Homebrew does not manage (drag-to-Applications installs; display only). |
|
|
94
|
-
| `downloadDir` | empty | Custom download folder; empty = `%USERPROFILE%\Downloads\DSH App Updater`. Must be absolute. |
|
|
95
|
-
| `keepInstallers` | `true` | Keep the installer after a successful update. |
|
|
96
|
-
| `hashFallbackUpgrade` | `true` | Fall back to `winget upgrade` when the download fails. |
|
|
97
|
-
| `dryRun` | `false` | **Rehearsal mode**: download the vendor's full installer but never install it. |
|
|
98
|
-
| `matchSystemArch` | `true` | Match the installer to the detected system and CPU architecture (Windows passes `--architecture`; macOS is resolved by Homebrew). |
|
|
99
|
-
| `autoElevate` | `true` | **Automatic elevation**: install commands that need administrator rights run in an elevated copy (UAC prompts once per install, when UAC prompts at all). |
|
|
100
|
-
|
|
101
|
-
Everything above is editable from the plugin's own settings panel inside the settings page.
|
|
102
|
-
The panel also accepts API calls:
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
curl -X POST -H "Content-Type: application/json" \
|
|
106
|
-
-d '{"confirm":true,"settings":{"dryRun":true}}' \
|
|
107
|
-
http://127.0.0.1:<port>/dsh-app-updater/settings
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
---
|
|
111
|
-
|
|
112
|
-
## Safety boundaries
|
|
113
|
-
|
|
114
|
-
- **Every route is deny-by-default**: each request first goes through `ctx.connection.admit(request)`
|
|
115
|
-
— DSH's Host/Origin fence plus browser-session / API authentication, the same gate the host uses for
|
|
116
|
-
its own `/api` routes. Rejected requests get `401` / `403` / `409` with `X-DSH-Admission: denied`.
|
|
117
|
-
- **Legacy loopback mode is off by default** (since 1.3.1): when the host has no `connection`
|
|
118
|
-
service, requests are no longer trusted just because they come from loopback — they get `403`.
|
|
119
|
-
If you really need it on an old host, opt in explicitly, either via plugin config:
|
|
120
|
-
|
|
121
|
-
```yaml
|
|
122
|
-
- id: dsh-app-updater
|
|
123
|
-
config:
|
|
124
|
-
allowLegacyLoopback: true
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
or by exporting `DSH_APP_UPDATER_ALLOW_LEGACY_LOOPBACK=1` before starting DSH. Once enabled, allowed
|
|
128
|
-
requests carry `X-DSH-Admission: loopback-legacy-optin` and `X-DSH-Admission-Warning`, the
|
|
129
|
-
`admission` field in `state.json` / `health.json` reports `mode: "loopback-legacy-optin"`, the
|
|
130
|
-
settings page shows a red banner, and a `warn` line is written to the log.
|
|
131
|
-
- **
|
|
132
|
-
`
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
-
|
|
145
|
-
- **
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
The
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
1
|
+
# dsh-app-updater — App Updater for DeepSeek Harness
|
|
2
|
+
|
|
3
|
+
**English** | [简体中文](README.md)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-app-updater)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Detect every installed application that has a newer version, then update them
|
|
11
|
+
with one click — downloading the **full installer from each vendor's own website** and
|
|
12
|
+
installing it silently. **The detected applications are never launched.**
|
|
13
|
+
Windows (winget) and macOS (Homebrew), architecture-aware: ARM64 and x64/Intel installers are
|
|
14
|
+
chosen to match the machine.
|
|
15
|
+
|
|
16
|
+
Where to find it: DeepSeek Harness → Settings → **应用更新 (App Updater)**
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# npm package (recommended; also one-click installable from the plugin center)
|
|
22
|
+
dsh plugin --profile web add dsh-app-updater
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Without npm, install from source or from the prebuilt tarball:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# source repository
|
|
29
|
+
dsh plugin --profile web add github:19952730389/dsh-appupdater
|
|
30
|
+
|
|
31
|
+
# prebuilt tgz (see Releases)
|
|
32
|
+
dsh plugin --profile web add https://github.com/19952730389/dsh-appupdater/releases/latest/download/dsh-app-updater.tgz
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Restart DSH afterwards; the plugin lives at **Settings → 应用更新**. The Electron-managed
|
|
36
|
+
`desktop` profile does not accept `dsh plugin` writes, so link the package into that
|
|
37
|
+
profile's `node_modules` and add it to `dsh.profile.bundles` manually.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## What it actually does
|
|
42
|
+
|
|
43
|
+
| Step | How |
|
|
44
|
+
|---|---|
|
|
45
|
+
| 1. List installed apps | `winget list` (apps known to the winget catalog plus Store/MSIX packages) **and** the three Windows registry `Uninstall` keys (apps the catalog does not know). Read-only: no application is ever started. |
|
|
46
|
+
| 2. Decide whether an update exists | `winget upgrade --include-unknown` gives "installed version → available version" from the official winget catalog, merged into the inventory by package id. |
|
|
47
|
+
| 3. One-click update | `winget download` fetches the **vendor's full installer** from the URL recorded in the winget manifest (SHA-256 verified by winget), the plugin **re-checks the hash locally before executing the file** (since 1.3.5 — the installer is about to run, possibly elevated, so a swapped or corrupted download is refused and the fallback path is taken), the downloaded manifest supplies the publisher's own `InstallerSwitches.Silent`, the installer runs silently, and the resulting version is re-read with `winget list --id`. |
|
|
48
|
+
| 4. Fallback | If the vendor re-signed the installer (hash no longer matches the catalog) or the manifest forbids direct download, it falls back to `winget upgrade --id … --exact --silent` — still the full installer from the vendor, with winget doing the fetching and installing. |
|
|
49
|
+
|
|
50
|
+
Those download URLs really are the vendors' own: `github.com/git-for-windows/…`,
|
|
51
|
+
`dl.google.com/dl/chrome/install/…`, `download.cpuid.com/cpu-z/…`, `dl.hdslb.com/…`.
|
|
52
|
+
Both the installer and its `.yaml` manifest are kept in the download folder, so the
|
|
53
|
+
`InstallerUrl`, `InstallerSha256` and silent switches can be audited afterwards.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
1. Open **Settings → 应用更新**.
|
|
60
|
+
2. Click **刷新检测** (nothing runs in the background; scanning only happens when you ask).
|
|
61
|
+
3. The table sorts updateable apps first. Use the search box and the
|
|
62
|
+
**全部 / 仅可更新 / 未收录** segments to narrow it down.
|
|
63
|
+
4. Tick the apps to update (or press **全部更新**), or update a single app from its row.
|
|
64
|
+
5. While updating you get a progress bar, the current app name and a per-item log;
|
|
65
|
+
**取消** stops the items that have not started yet.
|
|
66
|
+
6. **打开下载目录** reveals the downloaded vendor installers in Explorer.
|
|
67
|
+
|
|
68
|
+
### Status values
|
|
69
|
+
|
|
70
|
+
| Status | Meaning |
|
|
71
|
+
|---|---|
|
|
72
|
+
| **有新版本** | The catalog reports a higher version; one-click update is available. |
|
|
73
|
+
| **已是最新** | No newer version in the catalog. |
|
|
74
|
+
| **未收录** | An update exists in the catalog but this plugin cannot drive it (see reasons below). |
|
|
75
|
+
|
|
76
|
+
| Reason | Meaning |
|
|
77
|
+
|---|---|
|
|
78
|
+
| 软件目录未收录 | The app is not in the winget catalog (registry-only software), so its official update cannot be discovered or downloaded. |
|
|
79
|
+
| 由应用商店管理 | MSIX / Store app — update it through Microsoft Store. |
|
|
80
|
+
| 版本未知 | The installed version cannot be read (winget reports `Unknown`); updating is still allowed. |
|
|
81
|
+
| 系统组件 | System component / runtime; not driven by this plugin by default. |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Settings
|
|
86
|
+
|
|
87
|
+
State file: `%USERPROFILE%\.dsh\dsh-app-updater\state.json`
|
|
88
|
+
|
|
89
|
+
| Key | Default | Meaning |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `includeStoreApps` | `true` | List store apps too (MSIX on Windows, App Store on macOS; display only). |
|
|
92
|
+
| `includeRegistryOnly` | `true` | Windows: fill in apps missing from the winget catalog using the registry (display only). |
|
|
93
|
+
| `includeUnmanagedApps` | `true` | macOS: also list apps that Homebrew does not manage (drag-to-Applications installs; display only). |
|
|
94
|
+
| `downloadDir` | empty | Custom download folder; empty = `%USERPROFILE%\Downloads\DSH App Updater`. Must be absolute. |
|
|
95
|
+
| `keepInstallers` | `true` | Keep the installer after a successful update. |
|
|
96
|
+
| `hashFallbackUpgrade` | `true` | Fall back to `winget upgrade` when the download fails. |
|
|
97
|
+
| `dryRun` | `false` | **Rehearsal mode**: download the vendor's full installer but never install it. |
|
|
98
|
+
| `matchSystemArch` | `true` | Match the installer to the detected system and CPU architecture (Windows passes `--architecture`; macOS is resolved by Homebrew). |
|
|
99
|
+
| `autoElevate` | `true` | **Automatic elevation**: install commands that need administrator rights run in an elevated copy (UAC prompts once per install, when UAC prompts at all). |
|
|
100
|
+
|
|
101
|
+
Everything above is editable from the plugin's own settings panel inside the settings page.
|
|
102
|
+
The panel also accepts API calls:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
curl -X POST -H "Content-Type: application/json" \
|
|
106
|
+
-d '{"confirm":true,"settings":{"dryRun":true}}' \
|
|
107
|
+
http://127.0.0.1:<port>/dsh-app-updater/settings
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Safety boundaries
|
|
113
|
+
|
|
114
|
+
- **Every route is deny-by-default**: each request first goes through `ctx.connection.admit(request)`
|
|
115
|
+
— DSH's Host/Origin fence plus browser-session / API authentication, the same gate the host uses for
|
|
116
|
+
its own `/api` routes. Rejected requests get `401` / `403` / `409` with `X-DSH-Admission: denied`.
|
|
117
|
+
- **Legacy loopback mode is off by default** (since 1.3.1): when the host has no `connection`
|
|
118
|
+
service, requests are no longer trusted just because they come from loopback — they get `403`.
|
|
119
|
+
If you really need it on an old host, opt in explicitly, either via plugin config:
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
- id: dsh-app-updater
|
|
123
|
+
config:
|
|
124
|
+
allowLegacyLoopback: true
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
or by exporting `DSH_APP_UPDATER_ALLOW_LEGACY_LOOPBACK=1` before starting DSH. Once enabled, allowed
|
|
128
|
+
requests carry `X-DSH-Admission: loopback-legacy-optin` and `X-DSH-Admission-Warning`, the
|
|
129
|
+
`admission` field in `state.json` / `health.json` reports `mode: "loopback-legacy-optin"`, the
|
|
130
|
+
settings page shows a red banner, and a `warn` line is written to the log.
|
|
131
|
+
- **Cross-site fence on the legacy exemption (since 1.3.5)**: even when the opt-in is enabled, a
|
|
132
|
+
loopback request is rejected (`403`) when it looks browser-originated and cross-site — an
|
|
133
|
+
`Origin` / `Referer` whose host differs from the request `Host`, or `Sec-Fetch-Site: cross-site`.
|
|
134
|
+
Browsers forbid scripts from forging these headers, so the check is reliable: an arbitrary web
|
|
135
|
+
page can no longer drive "download + elevated silent install" with a cookie-less `fetch(…/update)`.
|
|
136
|
+
Local scripts and the host's IPC bridge (which send none of these headers) are unaffected.
|
|
137
|
+
- **The installer hash is re-checked locally before running (since 1.3.5)**: after `winget download`,
|
|
138
|
+
the plugin recomputes SHA-256 over the on-disk file against the manifest's `InstallerSha256`
|
|
139
|
+
before executing it (streaming, so a 500 MB package is not read into memory). That file is about
|
|
140
|
+
to run, possibly elevated, so a swapped or corrupted download is refused and the fallback taken.
|
|
141
|
+
When the manifest has no hash pairable to this machine's architecture, the check is skipped and
|
|
142
|
+
logged — never judged against another architecture's hash.
|
|
143
|
+
- **Every write still requires explicit confirmation**: `scan` / `update` / `cancel` /
|
|
144
|
+
`settings` / `open-downloads` require `confirm:true`.
|
|
145
|
+
- **No guessed silent switches**: the publisher's declared `Silent` switches are preferred;
|
|
146
|
+
generic defaults are used only when a manifest is unavailable.
|
|
147
|
+
- **Elevation only for installs, and on demand**: install commands that need administrator rights
|
|
148
|
+
(MSI packages, installers writing into `Program Files`, the `winget upgrade` fallback) run in an
|
|
149
|
+
elevated copy through `Start-Process -Verb RunAs`. With UAC set to "always notify" you get one
|
|
150
|
+
prompt per install; with `ConsentPromptBehaviorAdmin = 0` it is completely silent. Reads
|
|
151
|
+
(`winget list` / `upgrade --include-unknown` / `--version`) never elevate.
|
|
152
|
+
- **A zero exit code is not success**: after installing, the plugin compares the installed version
|
|
153
|
+
against the target version and reports a failure when nothing actually changed — machine-scope
|
|
154
|
+
installers silently exit 0 without installing anything when they lack administrator rights
|
|
155
|
+
(Logitech G HUB used to be reported as "updated" that way).
|
|
156
|
+
- **System components** (`.NET`, VC++ runtimes, …) are listed but not updated by default.
|
|
157
|
+
- **Only its own directory is written**: state and logs live in `~/.dsh/dsh-app-updater/`.
|
|
158
|
+
|
|
159
|
+
## Requirements
|
|
160
|
+
|
|
161
|
+
- **Windows**: `winget.exe` (Microsoft App Installer). Without winget the plugin can still
|
|
162
|
+
list registry apps, but cannot tell whether they are up to date.
|
|
163
|
+
- **macOS**: no extra dependency is needed to **list** apps (the plugin reads `.app` bundles from
|
|
164
|
+
`/Applications` and friends); judging and applying updates needs `brew` ([Homebrew](https://brew.sh)).
|
|
165
|
+
Without Homebrew the apps are still listed, with a note that installing Homebrew enables one-click
|
|
166
|
+
cask upgrades.
|
|
167
|
+
- No third-party npm dependencies: the host half uses Node built-ins only, the browser half
|
|
168
|
+
only the host-provided `react`.
|
|
169
|
+
|
|
170
|
+
## Supported systems and architectures
|
|
171
|
+
|
|
172
|
+
The plugin detects its own environment at startup and picks the installer that matches it:
|
|
173
|
+
|
|
174
|
+
| System | Architecture | Engine | Notes |
|
|
175
|
+
|---|---|---|---|
|
|
176
|
+
| Windows 10 / 11 | x64 / ARM64 / x86 | **winget** | the detected architecture is passed as `winget --architecture`; if the manifest has no installer for it, the plugin falls back to automatic matching and says so in the log |
|
|
177
|
+
| macOS 11+ | Apple Silicon / Intel | **Homebrew casks** | apps are enumerated from `/Applications` and friends, then matched against the cask catalog; Homebrew resolves the right cask for the machine. Without Homebrew the apps are still listed, they just cannot be upgraded in one click |
|
|
178
|
+
| anything else | — | none | the scan/update routes answer 400 with the reason |
|
|
179
|
+
|
|
180
|
+
The result is shown in the status line and in the settings panel's “运行环境 / Environment” row, e.g.
|
|
181
|
+
`Windows 11(build 26300) · x64(AMD64) · winget 1.29.380` or
|
|
182
|
+
`macOS 15(Sequoia) · Apple Silicon(ARM64) · Homebrew 4.5.0`.
|
|
183
|
+
|
|
184
|
+
The “按本机架构安装包 / match system architecture” switch (on by default) can be turned off to hand
|
|
185
|
+
the choice back to winget; Homebrew always resolves for the local architecture.
|
|
186
|
+
|
|
187
|
+
> The macOS app enumeration (`/Applications` + `Info.plist`) and cask matching are covered by unit
|
|
188
|
+
> tests and were validated against the real `formulae.brew.sh` catalog (7,800+ casks), but the
|
|
189
|
+
> macOS **install/update execution** has not been verified on a real Mac — this plugin is developed
|
|
190
|
+
> on Windows. Please open an issue if something misbehaves there.
|
|
191
|
+
>
|
|
192
|
+
> Only the standard application directories are enumerated. Apps installed elsewhere (a custom
|
|
193
|
+
> folder, the Desktop, an external volume) are outside the scan scope by design.
|
|
194
|
+
|
|
195
|
+
## HTTP API (host half)
|
|
196
|
+
|
|
197
|
+
| Method | Path | Purpose |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| GET | `/dsh-app-updater/state.json` | Status, inventory, log, settings |
|
|
200
|
+
| GET | `/dsh-app-updater/apps.json` | Inventory and statistics only |
|
|
201
|
+
| GET | `/dsh-app-updater/health.json` | Self-check: version, environment, engine, capabilities, route count, data directory |
|
|
202
|
+
| POST | `/dsh-app-updater/scan` | `{confirm:true, force:true}` start scanning |
|
|
203
|
+
| POST | `/dsh-app-updater/update` | `{confirm:true, ids:[...]}` start the update queue |
|
|
204
|
+
| POST | `/dsh-app-updater/cancel` | `{confirm:true}` cancel items not yet started |
|
|
205
|
+
| POST | `/dsh-app-updater/settings` | `{confirm:true, settings:{...}}` |
|
|
206
|
+
| POST | `/dsh-app-updater/open-downloads` | `{confirm:true}` open the download folder |
|
|
207
|
+
|
|
208
|
+
## Install (from a checkout)
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# from a local checkout
|
|
212
|
+
dsh plugin --profile <profile> add file:/path/to/dsh-app-updater
|
|
213
|
+
|
|
214
|
+
# or as a local link
|
|
215
|
+
dsh plugin --profile <profile> add link:/path/to/dsh-app-updater
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Then add `dsh-app-updater` to `dsh.profile.bundles` in the profile's `package.json`
|
|
219
|
+
(the bundled `cordis.patch.yml` mounts the host half) and reload DSH.
|
|
220
|
+
|
|
221
|
+
## Uninstall
|
|
222
|
+
|
|
223
|
+
1. Remove `dsh-app-updater` from `dsh.profile.bundles` and from `dependencies` in the profile's `package.json`.
|
|
224
|
+
2. Delete `<profile>/node_modules/dsh-app-updater`.
|
|
225
|
+
3. Restart DSH; the data directory `%USERPROFILE%\.dsh\dsh-app-updater` can be removed as well.
|
|
226
|
+
|
|
227
|
+
## Tests
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
node test/unit.mjs # 153 checks: table parsing, version compare, manifest & switches, OS/arch detection, Homebrew parsing, macOS app enumeration & cask matching
|
|
231
|
+
node test/render.mjs # 51 checks: settings panel (per platform), rehearsal banner, scan-warning banner, column drag, request bodies
|
|
232
|
+
node test/deny.mjs # 12 checks: deny-by-default per route when the host has no connection service
|
|
233
|
+
node test/harness.mjs --scan # boots the host half against a fake cordis ctx + real HTTP
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
> The tests do not depend on whether Homebrew is installed locally: the `findBrew` assertion checks
|
|
237
|
+
> existence across the three default prefixes (Apple Silicon / Intel / Linuxbrew), so it stays green
|
|
238
|
+
> on a real Mac (1.3.2 and earlier hard-coded `=== null` and was always red there; fixed in 1.3.3).
|
|
239
|
+
|
|
240
|
+
## Layout
|
|
241
|
+
|
|
242
|
+
| File | Purpose |
|
|
243
|
+
|---|---|
|
|
244
|
+
| `lib/index.js` | Host half: HTTP routes, write gate, scan/update scheduling |
|
|
245
|
+
| `lib/engine.js` | Inventory merge, vendor installer download, silent install, winget fallback, version re-check |
|
|
246
|
+
| `lib/winget.js` | winget discovery/invocation, fixed-width table parsing (CJK-aware) |
|
|
247
|
+
| `lib/registry.js` | Registry `Uninstall` enumeration (PowerShell, UTF-8) |
|
|
248
|
+
| `lib/version.js` | Windows-style version comparison |
|
|
249
|
+
| `lib/state.js` | Settings and state persistence (atomic writes) |
|
|
250
|
+
| `lib/proc.js` | Child process runner (timeouts, cancellation, UTF-8) |
|
|
251
|
+
| `lib/client.js` | Browser half: the settings page UI |
|
|
252
|
+
| `CONTRACT.md` | Host ↔ client interface contract |
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
244
256
|
MIT
|