@twinsearth/w2m-dsh-plugin 1.1.17 → 1.1.18

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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
8
- "version": "1.1.17"
8
+ "version": "1.1.18"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "w2m-fleet",
13
13
  "source": "./",
14
14
  "description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
15
- "version": "1.1.17",
15
+ "version": "1.1.18",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "w2m-fleet",
3
3
  "description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
4
- "version": "1.1.17",
4
+ "version": "1.1.18",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## [1.1.18] — 2026-10-18
6
+
7
+ **The developer declares this a STABLE release.** It ships the three-channel updater and the release plumbing that makes
8
+ the beta channel real.
9
+
10
+ ### Three channels, resolved from the two releases that exist
11
+
12
+ `w2m_update action:"status" channels:true` reports:
13
+
14
+ | Channel | Source | Rule |
15
+ |---|---|---|
16
+ | `stable` | GitHub's `/releases/latest` | never a prerelease |
17
+ | `beta` | the newest **prerelease**, fetched from `/releases` and filtered | exists only when one is published |
18
+ | `latest` | whichever of the two is newer by SemVer | equal versions resolve to stable |
19
+
20
+ Every decision comes from the existing `decideUpdate`, so the channel logic cannot drift from the install rule; this
21
+ module only chooses which two releases to ask about. A missing beta is reported as `available: null` rather than
22
+ substituted with stable — "there is no beta right now" is a fact the UI has to be able to show as a disabled button.
23
+
24
+ **The channel report is opt-in.** A plain `status` remains local and offline; making every status call reach GitHub would
25
+ slow the tool and break it behind a proxy. Asserted by a test that fails if any fetch happens.
26
+
27
+ ### Installing a channel, with the checksum enforced twice
28
+
29
+ `w2m_update action:"apply" channel:"stable"|"beta"|"latest"`: the release's `SHA256SUMS` is downloaded, the hash for the
30
+ tarball is required (no hash, no install), `downloadVerified` removes its temp file on any mismatch, and `installUpdate`
31
+ receives `expectedSha256` as well and keeps its rollback path. A mismatch is not retried: it means the asset and its
32
+ checksums disagree, which is never a transient condition.
33
+
34
+ ### The beta channel is a GitHub prerelease, and never npm
35
+
36
+ `.github/workflows/beta.yml`: a bump is a beta by default; a beta becomes a **prerelease** (so neither GitHub nor npm
37
+ resolves it as latest) gated by the `beta-access` environment, which requires a reviewer. There is **no `npm publish`
38
+ line in that file** — a channel cannot leak into the default install if the publication cannot happen at all. npm
39
+ receives a version only when the developer declares it stable, as this release does.
40
+
41
+ `.github/workflows/packages.yml` mirrors a released stable version to GitHub Packages with the registry overridden **on
42
+ the command line**, because `publishConfig.registry` would silently redirect the operator-channel publish too.
43
+
44
+ ### Verified
45
+
46
+ Gate GREEN twice, 15/15. 2504 tests / 2490 pass / 0 fail / 14 skip. tools 122, update-channels 4, update-prerelease 2.
47
+ The first GitHub Packages publication succeeded; the package page itself is confirmed by the operator, since the PAT has
48
+ no `read:packages` scope to read it back.
49
+
5
50
  ## [1.1.17] — 2026-10-18
6
51
 
7
52
  **Fleet chat: messages live in a session, are refused by a byte budget rather than silently dropped, are stored
package/README.md CHANGED
@@ -66,18 +66,18 @@ The manifests live in [`.claude-plugin/`](.claude-plugin/) and are version-locke
66
66
  # copy this block into your own codex/mcp-servers.toml
67
67
  [mcp_servers.w2m]
68
68
  command = "npx"
69
- args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.17", "w2m-mcp"]
69
+ args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.18", "w2m-mcp"]
70
70
  ```
71
71
 
72
72
  ### 5. Verified release tarball (offline)
73
73
 
74
- From the `v1.1.17` release page download **both** files — `twinsearth-w2m-dsh-plugin-1.1.17.tgz` and `SHA256SUMS` —
74
+ From the `v1.1.18` release page download **both** files — `twinsearth-w2m-dsh-plugin-1.1.18.tgz` and `SHA256SUMS` —
75
75
  then:
76
76
 
77
77
  ```bash
78
- curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.17/twinsearth-w2m-dsh-plugin-1.1.17.tgz
78
+ curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.18/twinsearth-w2m-dsh-plugin-1.1.18.tgz
79
79
  sha256sum -c SHA256SUMS # verify before installing, always
80
- npm install -g ./twinsearth-w2m-dsh-plugin-1.1.17.tgz
80
+ npm install -g ./twinsearth-w2m-dsh-plugin-1.1.18.tgz
81
81
  ```
82
82
 
83
83
  Every release carries `SHA256SUMS` for exactly this reason. If a release is missing it, do not install that release.
@@ -198,6 +198,27 @@ Linux, npm + GitHub release + checksums consistent, end-to-end fleet runs on a r
198
198
 
199
199
  ---
200
200
 
201
+ ## Release channels
202
+
203
+ Three channels, and only one of them is what \
204
+ pm install\ gives you.
205
+
206
+ | Channel | Where it comes from | Who should use it |
207
+ |---|---|---|
208
+ | **stable** | npm \latest\ — a version the developer declared stable | everyone; this is the default install |
209
+ | **beta** | a GitHub **prerelease** (tarball + \SHA256SUMS\), **never on npm** | developers granted \eta-access\ who accept that it can fail |
210
+ | **latest build** | the newest of the two, whatever its channel | a developer who wants the newest code and knows the above |
211
+
212
+ A bump is a **beta by default**. npm receives a version only when the developer says, in so many words, that this one is a
213
+ stable release: a beta is published as a prerelease (so neither GitHub nor npm ever resolves it as \latest\), it is
214
+ downloadable only after the \eta-access\ environment approves the run, and installing it requires reading
215
+ [SECURITY.md](SECURITY.md) and accepting the risk.
216
+
217
+ There is no \
218
+ pm publish\ line in the beta workflow. That is deliberate — a channel cannot leak to the default install if
219
+ the publication cannot happen at all.
220
+ ---
221
+
201
222
  ## Links
202
223
 
203
224
  - [CHANGELOG](CHANGELOG.md) — every version, including what it deliberately did **not** ship
@@ -267,17 +288,17 @@ w2m-mcp --help
267
288
  # 把这段复制进你自己的 codex/mcp-servers.toml
268
289
  [mcp_servers.w2m]
269
290
  command = "npx"
270
- args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.17", "w2m-mcp"]
291
+ args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.18", "w2m-mcp"]
271
292
  ```
272
293
 
273
294
  #### 5. 校验过的发布包(离线)
274
295
 
275
- 从 `v1.1.17` 的 release 页面下载**两个**文件 —— `twinsearth-w2m-dsh-plugin-1.1.17.tgz` 与 `SHA256SUMS` —— 然后:
296
+ 从 `v1.1.18` 的 release 页面下载**两个**文件 —— `twinsearth-w2m-dsh-plugin-1.1.18.tgz` 与 `SHA256SUMS` —— 然后:
276
297
 
277
298
  ```bash
278
- curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.17/twinsearth-w2m-dsh-plugin-1.1.17.tgz
299
+ curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.18/twinsearth-w2m-dsh-plugin-1.1.18.tgz
279
300
  sha256sum -c SHA256SUMS # 安装前务必校验
280
- npm install -g ./twinsearth-w2m-dsh-plugin-1.1.17.tgz
301
+ npm install -g ./twinsearth-w2m-dsh-plugin-1.1.18.tgz
281
302
  ```
282
303
 
283
304
  每个 release 都带 `SHA256SUMS` 正是为了这一步。若某个 release 缺少它,**不要安装该版本**。
@@ -372,6 +393,23 @@ CI 全绿、npm 与 GitHub 发布及校验和一致、在真实的 Linux/Mac/Win
372
393
  | 裁决总是 `partial` | 来自已下线机器的陈旧租约 | 1.1.2 已修:派发只对在线且未沉默的机器建租约 |
373
394
  | `P2P_PUNCH_FAILED` | NAT,或宿主密码学能力缺口 | 会自动降级到 relay 路径,结果不受影响 |
374
395
 
396
+ ### 发布通道(中文)
397
+
398
+ 三个通道,其中只有一个是你 \
399
+ pm install\ 默认拿到的。
400
+
401
+ | 通道 | 来源 | 适合谁 |
402
+ |---|---|---|
403
+ | **stable** | npm \latest\,开发者明确声明为稳定版 | 所有人;默认安装就是它 |
404
+ | **beta** | GitHub **prerelease**(tarball + \SHA256SUMS\),**绝不进 npm** | 被授予 \eta-access\、且接受它可能失败的开发者 |
405
+ | **最新构建** | 两者中版本较新的那个,不论通道 | 想要最新代码、并理解上述含义的开发者 |
406
+
407
+ **默认的 bump 是 beta**。只有开发者明确说明"这次是稳定版",npm 才会收到该版本:beta 以 prerelease 形式发布(因此
408
+ GitHub 与 npm 都不会把它解析成 \latest\),只有在 \eta-access\ 环境审批通过后才能下载,安装前需阅读
409
+ [SECURITY.md](SECURITY.md) 并自担风险。
410
+
411
+ beta 工作流里**没有 \
412
+ pm publish\ 这一行**,这是刻意的 —— 只要发布动作不可能发生,这个通道就不可能泄漏到默认安装。
375
413
  ### 链接
376
414
 
377
415
  - [CHANGELOG](CHANGELOG.md) —— 每个版本,包括它**故意没有**交付的内容
@@ -12,6 +12,6 @@
12
12
 
13
13
  [mcp_servers.w2m-fleet]
14
14
  command = "npx"
15
- args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.1.17", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.1.18", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.1.17",
3
+ "version": "1.1.18",
4
4
  "description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -129,13 +129,13 @@
129
129
  "pack": "node scripts/pack.mjs",
130
130
  "reachability": "node scripts/reachability-check.mjs",
131
131
  "reachability:classify": "node scripts/reachability-classify.mjs",
132
- "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs",
132
+ "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs",
133
133
  "test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
134
134
  "test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
135
135
  "test:admission": "node --test --test-force-exit test/admission.test.mjs",
136
136
  "test:admission-routes": "node --test --test-force-exit test/admission-routes.test.mjs",
137
137
  "test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
138
- "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs",
138
+ "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs",
139
139
  "test:approval-gate": "node --test --test-force-exit test/approval-gate.test.mjs",
140
140
  "test:approval-index": "node --test --test-force-exit test/approval-index.test.mjs",
141
141
  "test:authority": "node --test --test-force-exit test/authority.test.mjs",
@@ -179,7 +179,9 @@
179
179
  "test:fleet-messages": "node --test --test-force-exit test/fleet-messages.test.mjs",
180
180
  "test:message-shards": "node --test --test-force-exit test/message-shards.test.mjs",
181
181
  "test:fleet-messages-http": "node --test --test-force-exit test/fleet-messages-http.test.mjs",
182
- "test:message-notify": "node --test --test-force-exit test/message-notify.test.mjs"
182
+ "test:message-notify": "node --test --test-force-exit test/message-notify.test.mjs",
183
+ "test:update-channels": "node --test --test-force-exit test/update-channels.test.mjs",
184
+ "test:update-prerelease": "node --test --test-force-exit test/update-prerelease.test.mjs"
183
185
  },
184
186
  "keywords": [
185
187
  "dsh-bundle",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "twinsearth-w2m-dsh",
4
- "version": "1.1.17",
4
+ "version": "1.1.18",
5
5
  "description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -89,7 +89,10 @@
89
89
  */
90
90
 
91
91
  import { execFile } from 'node:child_process';
92
- import { createHash, randomUUID } from 'node:crypto';
92
+
93
+ import { resolveChannels } from './update-channels.mjs';
94
+ import { fetchLatestRelease, fetchPrereleaseRelease, requireChecksum, downloadVerified, DEFAULT_REPO } from './update-source.mjs';
95
+ import { installUpdate } from './update-install.mjs';import { createHash, randomUUID } from 'node:crypto';
93
96
  import { promises as fs } from 'node:fs';
94
97
  import os from 'node:os';
95
98
  import path from 'node:path';
@@ -214,7 +217,7 @@ const MAX_STAGES = 16;
214
217
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
215
218
  * overrides it at runtime for tests and packagers.
216
219
  */
217
- const PLUGIN_VERSION = '1.1.17';
220
+ const PLUGIN_VERSION = '1.1.18';
218
221
 
219
222
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
220
223
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -4858,7 +4861,50 @@ export async function apply(ctx, config = {}) {
4858
4861
  // `jsonOutput` is a renderer, not a serialiser; the tool still owns turning its value into
4859
4862
  // text.
4860
4863
  if (action === 'status') {
4861
- return JSON.stringify({ ok: true, action, update: updater.describe() }, null, 2);
4864
+ const described = { ok: true, action, update: updater.describe() };
4865
+ // Opt-in on purpose: the plain status call is local and offline, and making it reach GitHub would slow every
4866
+ // w2m_status and fail behind a proxy. The three channels need two API calls, so they are asked for explicitly.
4867
+ if (args?.channels !== true) return JSON.stringify(described, null, 2);
4868
+ const repo = (typeof process.env.W2M_UPDATE_REPO === 'string' && process.env.W2M_UPDATE_REPO.trim() !== '')
4869
+ ? process.env.W2M_UPDATE_REPO.trim() : DEFAULT_REPO;
4870
+ const [stableRelease, betaRelease] = await Promise.all([fetchLatestRelease({ repo }), fetchPrereleaseRelease({ repo })]);
4871
+ const currentVersion = resolveCurrentVersion(null, process.env);
4872
+ return JSON.stringify({ ...described, ...resolveChannels({ stable: stableRelease?.ok === true ? stableRelease : null, beta: betaRelease?.ok === true ? betaRelease : null, currentVersion }), sources: { stable: stableRelease, beta: betaRelease } }, null, 2);
4873
+ }
4874
+
4875
+ /**
4876
+ * v1.1.18: install a specific channel. The beta channel exists only as a GitHub prerelease, so it is verified here
4877
+ * rather than trusted: the tarball sha256 must match the SHA256SUMS shipped with the same release, and
4878
+ * downloadVerified removes its temp file on a mismatch, leaving the installed profile untouched.
4879
+ */
4880
+ if (action === 'apply') {
4881
+ const channel = typeof args?.channel === 'string' ? args.channel : 'stable';
4882
+ if (channel !== 'stable' && channel !== 'beta' && channel !== 'latest') {
4883
+ throw new W2MError('W2M_CHANNEL', 'W2M_CHANNEL: channel must be stable, beta or latest', { hint: 'call status with channels=true to see what each channel offers' });
4884
+ }
4885
+ const repo = (typeof process.env.W2M_UPDATE_REPO === 'string' && process.env.W2M_UPDATE_REPO.trim() !== '')
4886
+ ? process.env.W2M_UPDATE_REPO.trim() : DEFAULT_REPO;
4887
+ const [stableRelease, betaRelease] = await Promise.all([fetchLatestRelease({ repo }), fetchPrereleaseRelease({ repo })]);
4888
+ const currentVersion = resolveCurrentVersion(null, process.env);
4889
+ const resolved = resolveChannels({ stable: stableRelease?.ok === true ? stableRelease : null, beta: betaRelease?.ok === true ? betaRelease : null, currentVersion });
4890
+ const wanted = channel === 'latest' ? resolved.latest.channel : channel;
4891
+ const release = wanted === 'beta' ? betaRelease : stableRelease;
4892
+ if (release?.ok !== true) {
4893
+ throw new W2MError('W2M_NO_CHANNEL', 'W2M_NO_CHANNEL: nothing is available on the ' + channel + ' channel', { hint: release?.code ? release.code + ': ' + release.error : undefined });
4894
+ }
4895
+ const sumsAsset = release.assets?.sums;
4896
+ const tarballAsset = release.assets?.tarball;
4897
+ if (!sumsAsset || !tarballAsset) throw new W2MError('W2M_NO_ASSETS', 'W2M_NO_ASSETS: the release carries no tarball and SHA256SUMS');
4898
+ const sumsResponse = await fetch(sumsAsset.url, { headers: sumsAsset.headers ?? {} });
4899
+ if (sumsResponse?.ok !== true) throw new W2MError('W2M_SUMS', 'W2M_SUMS: could not download SHA256SUMS (HTTP ' + sumsResponse?.status + ')');
4900
+ const sha256 = requireChecksum(await sumsResponse.text(), tarballAsset.name);
4901
+ const profileDir = args?.profile_dir ?? findProfileDir({});
4902
+ const staged = await downloadVerified({ url: tarballAsset.url, sha256, destPath: (args?.staging_dir ?? profileDir) + '/.w2m-update/' + tarballAsset.name });
4903
+ if (staged?.ok !== true) {
4904
+ throw new W2MError('W2M_CHECKSUM', 'W2M_CHECKSUM: refusing to install; ' + (staged?.code ?? 'download-failed') + ' ' + (staged?.error ?? ''), { hint: 'a checksum mismatch is not retried: the asset and its SHA256SUMS disagree' });
4905
+ }
4906
+ const installed = await installUpdate({ tarballPath: staged.path, profileDir, expectedSha256: sha256, dryRun: args?.dry_run === true });
4907
+ return JSON.stringify({ ok: installed?.ok === true, action, channel, version: resolved.channels[wanted]?.available ?? null, sha256, install: installed }, null, 2);
4862
4908
  }
4863
4909
  const result = await updater.check('manual');
4864
4910
  // `ok:false` inside `result` means "the check could not complete", never "the tool failed".
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The three channels an operator chooses between, resolved from the two releases that exist.
3
+ *
4
+ * ## Why this file is thin
5
+ *
6
+ * The hard rule -- never install downwards, never install a prerelease unless asked -- is already implemented, and
7
+ * re-implementing it here would be the second copy that eventually disagrees. So every decision in this file comes from
8
+ * \`decideUpdate\`, and this module's only job is to pick WHICH TWO RELEASES the rule is asked about.
9
+ *
10
+ * ## The channels, as the operator defined them
11
+ *
12
+ * stable npm \`latest\`; a version the developer declared stable. Prereleases are refused.
13
+ * beta a GitHub **prerelease**. Reaching it requires \`beta-access\` approval and a signed risk acknowledgment;
14
+ * npm never carries it.
15
+ * latest whichever of the two is newer by SemVer. Equal versions mean the stable one, because the answer should not
16
+ * depend on which list was read first.
17
+ *
18
+ * A missing channel is reported as \`available: null\` rather than substituted: "there is no beta right now" is a fact a
19
+ * UI must be able to show as a disabled button, and inventing a fallback would hide it.
20
+ */
21
+ import { decideUpdate } from './auto-update.mjs';
22
+
23
+ /** The version a release names, as a comparable string. */
24
+ function versionOf(release) {
25
+ if (release === null || typeof release !== 'object') return null;
26
+ const raw = release.version;
27
+ if (typeof raw === 'string') return raw;
28
+ if (raw !== null && typeof raw === 'object' && Number.isInteger(raw.major)) {
29
+ return [raw.major, raw.minor ?? 0, raw.patch ?? 0].join('.');
30
+ }
31
+ const tag = typeof release.tag === 'string' ? release.tag : null;
32
+ return tag !== null && tag.startsWith('v') ? tag.slice(1) : null;
33
+ }
34
+
35
+ /**
36
+ * @param {{stable?: object|null, beta?: object|null, currentVersion: string|null}} input
37
+ * @returns {{current_version: string|null, channels: object, latest: object}}
38
+ */
39
+ export function resolveChannels({ stable = null, beta = null, currentVersion = null } = {}) {
40
+ const stableDecision = stable === null ? null : decideUpdate({ release: stable, currentVersion, allowPrerelease: false });
41
+ const betaDecision = beta === null ? null : decideUpdate({ release: beta, currentVersion, allowPrerelease: true });
42
+
43
+ const stableVersion = versionOf(stable);
44
+ const betaVersion = versionOf(beta);
45
+
46
+ let latest = { version: null, channel: null };
47
+ if (stableVersion !== null && betaVersion !== null) {
48
+ // Ask the same rule whether beta outranks stable. "skip" means it does not, which also covers equal versions.
49
+ const betaOutranksStable = decideUpdate({
50
+ release: beta,
51
+ currentVersion: stableVersion,
52
+ allowPrerelease: true,
53
+ }).action === 'install';
54
+ latest = betaOutranksStable
55
+ ? { version: betaVersion, channel: 'beta' }
56
+ : { version: stableVersion, channel: 'stable' };
57
+ } else if (betaVersion !== null) {
58
+ latest = { version: betaVersion, channel: 'beta' };
59
+ } else if (stableVersion !== null) {
60
+ latest = { version: stableVersion, channel: 'stable' };
61
+ }
62
+
63
+ return {
64
+ current_version: currentVersion,
65
+ channels: {
66
+ stable: { available: stableVersion, decision: stableDecision },
67
+ beta: { available: betaVersion, decision: betaDecision },
68
+ },
69
+ latest,
70
+ };
71
+ }
@@ -684,3 +684,74 @@ export async function downloadVerified({
684
684
  }
685
685
 
686
686
  export default { fetchLatestRelease, downloadVerified, parseVersion, compareVersions, isNewer, parseSha256Sums, findChecksum };
687
+
688
+ /**
689
+ * The beta channel's source: the newest PRERELEASE.
690
+ *
691
+ * ## Why this cannot reuse `fetchLatestRelease`
692
+ *
693
+ * That function asks GitHub for `/releases/latest`, and GitHub's own definition of "latest" **excludes prereleases**
694
+ * -- which is exactly right for the stable channel and exactly wrong for the beta one. The beta channel needs the
695
+ * opposite query: list releases, keep the prereleases, take the highest by SemVer.
696
+ *
697
+ * ## What it deliberately does not do
698
+ *
699
+ * It does not fall back to a stable release when no prerelease exists. "There is no beta right now" is a fact the update
700
+ * UI has to be able to show (a disabled button); substituting the stable version would make the beta button a stable
701
+ * button with a misleading label.
702
+ *
703
+ * @param {{repo?: string, fetchImpl?: Function, timeoutMs?: number, token?: string|null,
704
+ * apiBase?: string, userAgent?: string, perPage?: number, signal?: AbortSignal|null}} [options]
705
+ * @returns {Promise<{ok:true, tag:string, version:object|null, prerelease:true, htmlUrl:string|null}
706
+ * | {ok:false, code:string, error:string, status?:number, rateLimited?:boolean}>}
707
+ */
708
+ export async function fetchPrereleaseRelease({
709
+ repo = DEFAULT_REPO,
710
+ fetchImpl = globalThis.fetch,
711
+ timeoutMs = DEFAULT_TIMEOUT_MS,
712
+ token = null,
713
+ apiBase = DEFAULT_API_BASE,
714
+ userAgent = DEFAULT_USER_AGENT,
715
+ perPage = 30,
716
+ signal = null,
717
+ } = {}) {
718
+ try {
719
+ if (typeof fetchImpl !== 'function') {
720
+ throw new UpdateSourceError('NO_FETCH', 'no fetch implementation available (Node.js 18+ required)');
721
+ }
722
+ if (typeof repo !== 'string' || !/^[\w.-]+\/[\w.-]+$/.test(repo)) {
723
+ throw new UpdateSourceError('BAD_REPO', 'repo must look like "owner/name"');
724
+ }
725
+ const base = String(apiBase).replace(/\/+$/, '');
726
+ const url = base + '/repos/' + repo + '/releases?per_page=' + perPage;
727
+ const headers = { accept: 'application/vnd.github+json', 'user-agent': userAgent };
728
+ if (typeof token === 'string' && token !== '') headers.authorization = 'Bearer ' + token;
729
+ const response = await fetchImpl(url, { headers, signal: signal ?? AbortSignal.timeout(timeoutMs) });
730
+ const status = response?.status ?? 0;
731
+ if (response?.ok !== true) {
732
+ const rateLimited = status === 403 || status === 429;
733
+ return { ok: false, status, rateLimited, code: rateLimited ? 'RATE_LIMITED' : 'HTTP_' + status, error: 'GitHub answered HTTP ' + status };
734
+ }
735
+ const list = await response.json();
736
+ if (!Array.isArray(list)) return { ok: false, code: 'BAD_SHAPE', error: 'the releases response was not an array' };
737
+ const candidates = list.filter((r) => r && r.draft !== true && r.prerelease === true && typeof r.tag_name === 'string');
738
+ if (candidates.length === 0) {
739
+ // Not an error: the beta channel is empty, and the caller must be able to say so.
740
+ return { ok: false, code: 'NO_PRERELEASE', error: 'there is no prerelease at the moment' };
741
+ }
742
+ let best = candidates[0];
743
+ for (const candidate of candidates.slice(1)) {
744
+ // Highest SemVer wins; `compareVersions` is the same comparison the install decision uses.
745
+ if (compareVersions(candidate.tag_name, best.tag_name) > 0) best = candidate;
746
+ }
747
+ return {
748
+ ok: true,
749
+ tag: best.tag_name,
750
+ version: parseVersion(best.tag_name),
751
+ prerelease: true,
752
+ htmlUrl: typeof best.html_url === 'string' ? best.html_url : null,
753
+ };
754
+ } catch (error) {
755
+ return { ok: false, code: 'FETCH_FAILED', error: String(error && error.message ? error.message : error) };
756
+ }
757
+ }