dsh-app-updater 1.3.5 → 1.3.7

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
@@ -2,6 +2,32 @@
2
2
 
3
3
  本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [1.3.7] — 2026-11-02
6
+
7
+ **改名回退**:GitHub 仓库 `19952730389/dsh-appupdater` → **改回 `19952730389/dsh-app-updater`**。
8
+ 1.3.6 的仓库改名(为绕开 deepseek.club Plugin Hub 的撞名)经你确认不再需要,整体撤销:
9
+ 仓库名、package.json 的 repository/homepage/bugs、README 链接全部回到原名。
10
+ deepseek.club 那家不上架了(RELEASING.md 保留提交入口与表单字段资料,供将来参考)。
11
+
12
+ - npm 包名/插件 id/路由/数据目录自 1.3.5 起就未变过,本次回退**对用户零影响**。
13
+ - v1.3.6 的 tag 与 Release 留在仓库里作为过程记录(资产文件名一直是 `dsh-app-updater.tgz`,未受影响)。
14
+ - 代码与 1.3.5/1.3.6 一致(仅文档与 URL 差异);市场条目继续指向 `releases/latest/download/dsh-app-updater.tgz`。
15
+
16
+ ## [1.3.6] — 2026-11-02
17
+
18
+ **GitHub 仓库改名**:`19952730389/dsh-app-updater` → **`19952730389/dsh-appupdater`**
19
+ (旧 URL 由 GitHub 自动重定向;README / package.json 里的仓库链接与源码安装命令已指向新名,
20
+ tarball 资产名 `dsh-app-updater.tgz` 不变,各市场 `releases/latest` 链接不受影响)。
21
+
22
+ 改名原因:deepseek.club Plugin Hub 的插件 slug `dsh-app-updater` 已被另一个同名插件占用
23
+ (`m1452700576/dsh-app-updater`,用途是检测并更新 DSH 自身),在那家上架必须避开撞名。
24
+
25
+ - **npm 包名、插件 id、HTTP 路由前缀(`/dsh-app-updater/*`)、数据目录维持不变**:
26
+ npm 的防混淆规则不允许同一作者发布过于相似的名字(`dsh-appupdater` 被 registry 拒绝),
27
+ 与其把生态拆成两套名字(用户迁移成本 + 市场条目重写),不如维持现状:
28
+ `dsh plugin add dsh-app-updater` 照旧。已配置的环境变量与插件配置也全部沿用。
29
+ - 代码与 1.3.5 逐字节一致,仅仓库 URL 与文档差异。
30
+
5
31
  ## [1.3.5] — 2026-11-02
6
32
 
7
33
  两轮对抗式复查的合并收口(1.3.4 之后),修掉 9 个真实缺陷、补上 1 处文档与实现不一致的承诺。
package/README.en.md CHANGED
@@ -1,256 +1,256 @@
1
- # dsh-app-updater — App Updater for DeepSeek Harness
2
-
3
- **English** | [简体中文](README.md)
4
- [![npm](https://img.shields.io/npm/v/dsh-app-updater?style=flat-square&label=npm&color=0891b2&logo=npm)](https://www.npmjs.com/package/dsh-app-updater)
5
- ![license](https://img.shields.io/badge/license-MIT-2f6f4f?style=flat-square)
6
- ![platform](https://img.shields.io/badge/platform-Windows-7da1de?style=flat-square)
7
- ![dsh-plugin](https://img.shields.io/badge/DSH-plugin-263146?style=flat-square)
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-app-updater
30
-
31
- # prebuilt tgz (see Releases)
32
- dsh plugin --profile web add https://github.com/19952730389/dsh-app-updater/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
-
1
+ # dsh-app-updater — App Updater for DeepSeek Harness
2
+
3
+ **English** | [简体中文](README.md)
4
+ [![npm](https://img.shields.io/npm/v/dsh-app-updater?style=flat-square&label=npm&color=0891b2&logo=npm)](https://www.npmjs.com/package/dsh-app-updater)
5
+ ![license](https://img.shields.io/badge/license-MIT-2f6f4f?style=flat-square)
6
+ ![platform](https://img.shields.io/badge/platform-Windows-7da1de?style=flat-square)
7
+ ![dsh-plugin](https://img.shields.io/badge/DSH-plugin-263146?style=flat-square)
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-app-updater
30
+
31
+ # prebuilt tgz (see Releases)
32
+ dsh plugin --profile web add https://github.com/19952730389/dsh-app-updater/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
+
256
256
  MIT