@twinsearth/w2m-dsh-plugin 1.6.6 → 1.6.8
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +29 -0
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/1.6.7/FAILOVER-CLEANUP-FIX.json +22 -0
- package/docs/acceptance/1.6.8/PLUGIN-UPDATE-BUTTON.json +22 -0
- package/lib/client.js +57 -1
- package/package.json +3 -3
- package/plugin.json +1 -1
- package/src/plugin/npm-update.mjs +79 -0
- package/src/plugin/tools.mjs +27 -2
- package/src/plugin/update-route.mjs +118 -0
|
@@ -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.6.
|
|
8
|
+
"version": "1.6.8"
|
|
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.6.
|
|
15
|
+
"version": "1.6.8",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
|
-
"version": "1.6.
|
|
29
|
+
"version": "1.6.8"
|
|
30
30
|
}
|
|
@@ -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.6.
|
|
4
|
+
"version": "1.6.8",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [1.6.8] — 2026-10-24
|
|
11
|
+
|
|
12
|
+
**插件面板新增「检查插件更新」按钮:npm 对比 → 自动更新 → 提示重启 DSH。**
|
|
13
|
+
|
|
14
|
+
- 客户端:目标机器菜单 relay 区新增按钮。点击后:① GET `/api/w2m/plugin-update` 联网查 npm latest
|
|
15
|
+
并与已装版本比较;② 有新版时 POST `{action:"apply"}` 执行安装;③ 安装完成显示"请重启 DSH 以载入最新插件"。
|
|
16
|
+
- 宿主:新增路由 `/api/w2m/plugin-update`。检查逻辑在 `src/plugin/npm-update.mjs`(纯函数,可单测:只认
|
|
17
|
+
`latest > installed`,网络失败是错误值而不是"已最新")。
|
|
18
|
+
- 安装复用**既有已验证更新器**(`auto-update.mjs` → `update-install.mjs`):下载 tarball、用 release
|
|
19
|
+
SHA256SUMS 校验 SHA-256、备份旧包、替换新版——按钮点击不会引入第二套弱校验安装路径。
|
|
20
|
+
- 测试:npm-update 4/4、client-bundle 26/26;证据 `docs/acceptance/1.6.8/PLUGIN-UPDATE-BUTTON.json`
|
|
21
|
+
(fleet task `01M5ANYMVMI9EHA`)。
|
|
22
|
+
- 边界:DSH 插件管理页的「卸载」按钮属于宿主应用(打包在 app.asar 内),插件包无法直接注入其旁边;
|
|
23
|
+
本按钮位于 w2m 插件自己的面板中。若要放进宿主插件页,需要 DSH 侧提供插件动作扩展点(已列为待评估)。
|
|
24
|
+
## [1.6.7] — 2026-10-24
|
|
25
|
+
|
|
26
|
+
**修复 v1.6.6 failover 的阻塞 bug:远端 relay 模式下清理函数抛错,切换后无法重启 stack。**
|
|
27
|
+
|
|
28
|
+
现场演练(停主 relay 180s)暴露:机器掉线后没有切到备用,恢复主 relay 后才重新上线。根因:
|
|
29
|
+
|
|
30
|
+
- `startLocalStack` 的清理函数无条件执行 `await relay.close()`;而 `relay` 只在"本进程自建本地 relay"
|
|
31
|
+
时才非空,远端 relay 模式下是 `null` → 抛 TypeError;
|
|
32
|
+
- failover 的切换流程 `await stop()` 抛错后被 catch 吞掉,**后续的 restart 从未执行**(stack 停了没起来)。
|
|
33
|
+
|
|
34
|
+
修复:
|
|
35
|
+
- 清理函数 null-safe/idempotent:仅当 `relay` 是带 `close()` 的对象才调用;`agent.stop()` 失败也继续清理。
|
|
36
|
+
- 切换流程:`stop()` 失败不再跳过 restart;并写诊断日志 `xclient/relay-failover.log`(每轮探测/切换/重启错误)。
|
|
37
|
+
- 测试:relay-failover 5/5、relay 200/200;证据 `docs/acceptance/1.6.7/FAILOVER-CLEANUP-FIX.json`
|
|
38
|
+
(fleet task `01M5ANYMVMFSSP3`)。
|
|
10
39
|
## [1.6.6] — 2026-10-24
|
|
11
40
|
|
|
12
41
|
**relay 自动 failover:主 relay 故障时自动切到备用,恢复后自动切回。**
|
package/codex/mcp-servers.toml
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.6.7",
|
|
3
|
+
"case": "FAILOVER-CLEANUP-FIX",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-24T13:31:02.803Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"requirement": "a remote-relay in-process stack can be stopped and restarted for failover: the cleanup must not assume a local relay object, a failed stop must not skip the restart, and the switch must leave a diagnostics log",
|
|
11
|
+
"task_id": "01M5ANYMVMFSSP3",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"failover_suite": "5/5",
|
|
16
|
+
"relay_suite": "200/200",
|
|
17
|
+
"cleanup": "null-safe and idempotent",
|
|
18
|
+
"diagnostics": "xclient/relay-failover.log"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"note": "v1.6.6 field drill stopped the stack and never restarted it: relay.close() threw on remote-relay stacks"
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.6.8",
|
|
3
|
+
"case": "PLUGIN-UPDATE-BUTTON",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-24T14:39:54.191Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"requirement": "the plugin panel offers one update button: it checks npm for a newer version, installs it through the verified updater (SHA-256 checked, old package backed up), and tells the user to restart DSH",
|
|
11
|
+
"task_id": "01M5ANYMVMI9EHA",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"npm_update_suite": "4/4 pass",
|
|
16
|
+
"check": "only latest > installed",
|
|
17
|
+
"apply": "delegated to auto-update.mjs verified installer",
|
|
18
|
+
"restart_required": "true after install"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"note": "button lives in the w2m plugin panel; the DSH plugin-manager page is host UI packaged in app.asar and not editable by a plugin package"
|
|
22
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -78,6 +78,8 @@ window.__ModuleLoader__.load({
|
|
|
78
78
|
const ROUTE = '/api/w2m/target';
|
|
79
79
|
// v1.4.2: the UI-activated Node-code-execution switch (host: src/plugin/exec-capability-route.mjs).
|
|
80
80
|
const CAPABILITY_ROUTE = '/api/w2m/capability';
|
|
81
|
+
// v1.6.8: npm update check + verified install (host: src/plugin/update-route.mjs).
|
|
82
|
+
const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
81
83
|
|
|
82
84
|
/**
|
|
83
85
|
* The locale namespace and its two dictionaries.
|
|
@@ -113,6 +115,12 @@ window.__ModuleLoader__.load({
|
|
|
113
115
|
relayUnfold: '收起折叠的 relay',
|
|
114
116
|
relayTemporary: '临时 Relay 节点(回退配置)',
|
|
115
117
|
globalSummary: '全服:{fleetsOnline}/{fleets} 个编队 · {online}/{total} 台在线',
|
|
118
|
+
updateButton: '检查插件更新',
|
|
119
|
+
updateChecking: '正在检查 npm…',
|
|
120
|
+
updating: '正在更新…',
|
|
121
|
+
updateCurrent: '已是最新({version})',
|
|
122
|
+
updateInstalled: '已更新到 {version}',
|
|
123
|
+
restartHint: '请重启 DSH 以载入最新插件',
|
|
116
124
|
capLabel: 'Node 执行:{state}',
|
|
117
125
|
capDenied: '拒绝',
|
|
118
126
|
capTemporary: '临时 24 小时',
|
|
@@ -145,6 +153,12 @@ window.__ModuleLoader__.load({
|
|
|
145
153
|
relayUnfold: 'collapse the folded relays',
|
|
146
154
|
relayTemporary: 'temporary relay nodes (fallback config)',
|
|
147
155
|
globalSummary: 'fleets {fleetsOnline}/{fleets} · machines {online}/{total} online',
|
|
156
|
+
updateButton: 'check for plugin update',
|
|
157
|
+
updateChecking: 'checking npm…',
|
|
158
|
+
updating: 'updating…',
|
|
159
|
+
updateCurrent: 'up to date ({version})',
|
|
160
|
+
updateInstalled: 'updated to {version}',
|
|
161
|
+
restartHint: 'restart DSH to load the new plugin',
|
|
148
162
|
capLabel: 'Node exec: {state}',
|
|
149
163
|
capDenied: 'denied',
|
|
150
164
|
capTemporary: 'temporary 24 h',
|
|
@@ -439,7 +453,48 @@ window.__ModuleLoader__.load({
|
|
|
439
453
|
* target is a choice about the *next* dispatch, which is legitimate to change while a turn runs.
|
|
440
454
|
* `t` arrives through the entry's inject face (see {@link apply}).
|
|
441
455
|
*/
|
|
442
|
-
|
|
456
|
+
/** v1.6.8: one button beside the relay rows: check npm for a newer plugin, install it, ask for a restart. */
|
|
457
|
+
function PluginUpdate({ t }) {
|
|
458
|
+
const [state, setState] = React.useState({ status: 'idle', info: null, error: null });
|
|
459
|
+
const call = async (method, body) => {
|
|
460
|
+
const response = await fetch(PLUGIN_UPDATE_ROUTE, {
|
|
461
|
+
method, credentials: 'same-origin',
|
|
462
|
+
headers: { accept: 'application/json', ...(body === undefined ? {} : { 'content-type': 'application/json' }) },
|
|
463
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
464
|
+
});
|
|
465
|
+
let json = null;
|
|
466
|
+
try { json = await response.json(); } catch { /* keep null */ }
|
|
467
|
+
return { status: response.status, json };
|
|
468
|
+
};
|
|
469
|
+
const onClick = async () => {
|
|
470
|
+
setState({ status: 'checking', info: null, error: null });
|
|
471
|
+
try {
|
|
472
|
+
const checked = await call('GET');
|
|
473
|
+
const info = checked.json ?? {};
|
|
474
|
+
if (info.ok !== true) { setState({ status: 'error', info, error: info.error ?? 'update check failed' }); return; }
|
|
475
|
+
if (info.update_available !== true) { setState({ status: 'current', info, error: null }); return; }
|
|
476
|
+
setState({ status: 'applying', info, error: null });
|
|
477
|
+
const applied = await call('POST', { action: 'apply' });
|
|
478
|
+
const result = applied.json ?? {};
|
|
479
|
+
if (result.ok !== true) { setState({ status: 'error', info: result, error: result.error ?? result.reason ?? 'update failed' }); return; }
|
|
480
|
+
setState({ status: 'installed', info: result, error: null });
|
|
481
|
+
} catch (error) {
|
|
482
|
+
setState({ status: 'error', info: null, error: error instanceof Error ? error.message : String(error) });
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
const label = state.status === 'checking' ? t('updateChecking')
|
|
486
|
+
: state.status === 'applying' ? t('updating')
|
|
487
|
+
: state.status === 'current' ? t('updateCurrent', { version: state.info?.current ?? '?' })
|
|
488
|
+
: state.status === 'installed' ? t('updateInstalled', { version: state.info?.to ?? state.info?.latest ?? '?' })
|
|
489
|
+
: t('updateButton');
|
|
490
|
+
const disabled = state.status === 'checking' || state.status === 'applying';
|
|
491
|
+
return h('div', { className: cls('relayHint'), 'data-w2m-update': state.status }, [
|
|
492
|
+
h('button', { key: 'b', type: 'button', className: cls('relayToggle'), disabled, onClick: (event) => { event.stopPropagation(); onClick(); } }, label),
|
|
493
|
+
state.status === 'installed' ? h('div', { key: 'r', className: cls('relayHint') }, t('restartHint')) : null,
|
|
494
|
+
state.status === 'error' ? h('div', { key: 'e', className: cls('relayHint') }, String(state.error ?? 'error')) : null,
|
|
495
|
+
]);
|
|
496
|
+
}
|
|
497
|
+
function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
443
498
|
const [open, setOpen] = React.useState(false);
|
|
444
499
|
// v1.5.1: relay folding. The snapshot carries the pool; when it holds more than three nodes
|
|
445
500
|
// the menu shows a summary and this toggle reveals the folded ones.
|
|
@@ -639,6 +694,7 @@ window.__ModuleLoader__.load({
|
|
|
639
694
|
online: relayFold.server.devices_online ?? '—',
|
|
640
695
|
total: relayFold.server.devices_total ?? '—',
|
|
641
696
|
})),
|
|
697
|
+
h(PluginUpdate, { key: 'update', t }),
|
|
642
698
|
relayFold.hidden_count > 0 && relayOpen === false
|
|
643
699
|
? h('div', { key: 'hint', className: cls('relayHint') }, t('relayFold', { count: relayFold.hidden_count }))
|
|
644
700
|
: null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.8",
|
|
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
|
"exports": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"pack": "node scripts/pack.mjs",
|
|
65
65
|
"reachability": "node scripts/reachability-check.mjs",
|
|
66
66
|
"reachability:classify": "node scripts/reachability-classify.mjs",
|
|
67
|
-
"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 test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs",
|
|
67
|
+
"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 test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs",
|
|
68
68
|
"test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
|
|
69
69
|
"test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
|
|
70
70
|
"test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"test:council-session": "node --test --test-force-exit test/council-session.test.mjs",
|
|
83
83
|
"test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
|
|
84
84
|
"test:crypto-blake2s": "node --test --test-force-exit test/crypto-blake2s.test.mjs",
|
|
85
|
-
"test:e2e": "node --test --test-force-exit 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/p2p-agent.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs",
|
|
85
|
+
"test:e2e": "node --test --test-force-exit 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/p2p-agent.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs",
|
|
86
86
|
"test:exec-env": "node --test --test-force-exit test/exec-env-injection.test.mjs",
|
|
87
87
|
"test:exit-channel": "node --test --test-force-exit test/exit-channel.test.mjs",
|
|
88
88
|
"test:field-authority": "node --test --test-force-exit test/field-authority.test.mjs",
|
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.6.
|
|
4
|
+
"version": "1.6.8",
|
|
5
5
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 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": [
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v1.6.8 — npm update check for the plugin panel's "更新" button.
|
|
3
|
+
*
|
|
4
|
+
* The button's first half is this module: ask the npm registry for `latest`, compare it against the
|
|
5
|
+
* version installed in the profile, and report the result. Nothing here writes or installs — the
|
|
6
|
+
* install half reuses the existing verified updater (`auto-update.mjs` → `update-install.mjs`),
|
|
7
|
+
* which already downloads a tarball, verifies its SHA-256 against the release's SHA256SUMS, swaps the
|
|
8
|
+
* package in the profile, and takes a backup. Keeping the check pure is what makes "NPM 对比" testable
|
|
9
|
+
* without a network or a profile.
|
|
10
|
+
*/
|
|
11
|
+
import { compareVersions } from './update-source.mjs';
|
|
12
|
+
|
|
13
|
+
/** The package this plugin is. */
|
|
14
|
+
export const PLUGIN_PACKAGE_NAME = '@twinsearth/w2m-dsh-plugin';
|
|
15
|
+
/** npm mirrors are fine here: the answer is only "which version is latest", and the tarball is verified separately. */
|
|
16
|
+
export const DEFAULT_NPM_LATEST_URL = `https://registry.npmmirror.com/${encodeURIComponent(PLUGIN_PACKAGE_NAME)}/latest`;
|
|
17
|
+
export const NPM_CHECK_TIMEOUT_MS = 15_000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Normalise the npm `latest` document.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} doc
|
|
23
|
+
* @returns {{ok: true, name: string, version: string, tarball: string|null}|{ok: false, error: string}}
|
|
24
|
+
*/
|
|
25
|
+
export function parseNpmLatest(doc) {
|
|
26
|
+
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
27
|
+
return { ok: false, error: 'the npm latest document is not a JSON object' };
|
|
28
|
+
}
|
|
29
|
+
const version = typeof doc.version === 'string' ? doc.version.trim() : '';
|
|
30
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
|
|
31
|
+
return { ok: false, error: `the npm latest document carries no usable version (got ${JSON.stringify(doc.version ?? null)})` };
|
|
32
|
+
}
|
|
33
|
+
const tarball = typeof doc.dist?.tarball === 'string' && doc.dist.tarball !== '' ? doc.dist.tarball : null;
|
|
34
|
+
const name = typeof doc.name === 'string' && doc.name !== '' ? doc.name : PLUGIN_PACKAGE_NAME;
|
|
35
|
+
return { ok: true, name, version, tarball };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compare the installed version with npm latest.
|
|
40
|
+
*
|
|
41
|
+
* Update availability is `latest > installed`; equal or older is not an update (a profile can hold a
|
|
42
|
+
* beta/newer local build, and downgrading it from a button would be a surprise, not a service).
|
|
43
|
+
*
|
|
44
|
+
* @param {{latest: {ok: boolean, version?: string, error?: string, tarball?: string|null}, currentVersion: string|null}} input
|
|
45
|
+
* @returns {{ok: boolean, current: string|null, latest: string|null, update_available: boolean, tarball: string|null, error?: string}}
|
|
46
|
+
*/
|
|
47
|
+
export function decideNpmUpdate({ latest, currentVersion }) {
|
|
48
|
+
const current = typeof currentVersion === 'string' && currentVersion !== '' ? currentVersion : null;
|
|
49
|
+
if (latest?.ok !== true) {
|
|
50
|
+
return { ok: false, current, latest: null, update_available: false, tarball: null, error: latest?.error ?? 'npm latest could not be read' };
|
|
51
|
+
}
|
|
52
|
+
const available = current !== null && compareVersions(latest.version, current) > 0;
|
|
53
|
+
return { ok: true, current, latest: latest.version, update_available: available, tarball: latest.tarball ?? null };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Ask npm for `latest` and decide. Never throws: a network failure is a value with `error`.
|
|
58
|
+
*
|
|
59
|
+
* @param {{currentVersion: string|null, fetchImpl?: Function, url?: string, timeoutMs?: number}} input
|
|
60
|
+
*/
|
|
61
|
+
export async function checkNpmUpdate(input) {
|
|
62
|
+
const fetchImpl = input?.fetchImpl ?? globalThis.fetch;
|
|
63
|
+
const url = input?.url ?? DEFAULT_NPM_LATEST_URL;
|
|
64
|
+
const timeoutMs = Number.isInteger(input?.timeoutMs) ? input.timeoutMs : NPM_CHECK_TIMEOUT_MS;
|
|
65
|
+
let doc;
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetchImpl(url, {
|
|
68
|
+
headers: { accept: 'application/json' },
|
|
69
|
+
signal: typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function' ? AbortSignal.timeout(timeoutMs) : undefined,
|
|
70
|
+
});
|
|
71
|
+
if (response?.ok !== true) {
|
|
72
|
+
return decideNpmUpdate({ latest: { ok: false, error: `npm registry answered HTTP ${response?.status ?? '?'}` }, currentVersion: input?.currentVersion ?? null });
|
|
73
|
+
}
|
|
74
|
+
doc = await response.json();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return decideNpmUpdate({ latest: { ok: false, error: error instanceof Error ? error.message : String(error) }, currentVersion: input?.currentVersion ?? null });
|
|
77
|
+
}
|
|
78
|
+
return decideNpmUpdate({ latest: parseNpmLatest(doc), currentVersion: input?.currentVersion ?? null });
|
|
79
|
+
}
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -190,6 +190,7 @@ import {
|
|
|
190
190
|
// `w2m_chat` tool shipped first; this is the browser-facing half that was still missing.
|
|
191
191
|
import { registerChatRoutes } from './fleet-chat-route.mjs';
|
|
192
192
|
import { registerExecCapabilityRoutes } from './exec-capability-route.mjs';
|
|
193
|
+
import { registerPluginUpdateRoutes } from './update-route.mjs';
|
|
193
194
|
import { defaultExecCapabilityFile } from '../util/exec-capability.mjs';
|
|
194
195
|
import { createFleetCouncil, fleetCouncilThread } from './council-fleet.mjs';
|
|
195
196
|
import { renderMemoryView } from './memory-view.mjs';
|
|
@@ -281,7 +282,7 @@ const MAX_STAGES = 16;
|
|
|
281
282
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
282
283
|
* overrides it at runtime for tests and packagers.
|
|
283
284
|
*/
|
|
284
|
-
const PLUGIN_VERSION = '1.6.
|
|
285
|
+
const PLUGIN_VERSION = '1.6.8';
|
|
285
286
|
|
|
286
287
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
287
288
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
@@ -3513,6 +3514,9 @@ export async function apply(ctx, config = {}) {
|
|
|
3513
3514
|
const capabilityRoutes = registerExecCapabilityRoutes(ctx, {
|
|
3514
3515
|
capabilityFile: () => defaultExecCapabilityFile(),
|
|
3515
3516
|
});
|
|
3517
|
+
// v1.6.8: the plugin panel's "更新" button: GET checks npm latest, POST installs through the
|
|
3518
|
+
// verified updater (SHA-256 checked, old package backed up, restart required afterwards).
|
|
3519
|
+
const _updateRoutes = registerPluginUpdateRoutes(ctx, { profileDir: () => cfg.profileDir });
|
|
3516
3520
|
if (capabilityRoutes.mounted !== true) {
|
|
3517
3521
|
ctx.logger?.warn?.('w2m: the execution-capability control is inactive — ' + capabilityRoutes.reason);
|
|
3518
3522
|
}
|
|
@@ -7407,6 +7411,19 @@ async function probeRelayHealth(url, timeoutMs) {
|
|
|
7407
7411
|
* relay-failover.mjs and is unit-tested there; this function only supplies health and performs the
|
|
7408
7412
|
* restart.
|
|
7409
7413
|
*/
|
|
7414
|
+
/** Append one line to the failover diagnostics log; failures are swallowed (diagnostics must not break the switch). */
|
|
7415
|
+
async function appendFailoverLog(cfg, line) {
|
|
7416
|
+
try {
|
|
7417
|
+
const { appendFileSync, mkdirSync, statSync, writeFileSync } = await import('node:fs');
|
|
7418
|
+
const { join, dirname } = await import('node:path');
|
|
7419
|
+
const { homedir } = await import('node:os');
|
|
7420
|
+
const dir = cfg.stateDir ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'xclient');
|
|
7421
|
+
const file = join(dir, 'relay-failover.log');
|
|
7422
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
7423
|
+
try { if (statSync(file).size > 256 * 1024) writeFileSync(file, ''); } catch { /* absent is fine */ }
|
|
7424
|
+
appendFileSync(file, new Date().toISOString() + ' ' + line + '\n');
|
|
7425
|
+
} catch { /* diagnostics only */ }
|
|
7426
|
+
}
|
|
7410
7427
|
async function startFailoverStack(ctx, cfg) {
|
|
7411
7428
|
let stop = await startLocalStack(ctx, cfg);
|
|
7412
7429
|
const pool = relayPool(cfg);
|
|
@@ -7422,6 +7439,7 @@ async function startFailoverStack(ctx, cfg) {
|
|
|
7422
7439
|
const health = {};
|
|
7423
7440
|
await Promise.all(pool.map(async (url) => { health[url] = await probeRelayHealth(url, FAILOVER_DEFAULTS.probeTimeoutMs); }));
|
|
7424
7441
|
const decision = failover.assess(health);
|
|
7442
|
+
await appendFailoverLog(cfg, 'probe ' + JSON.stringify(health) + ' -> ' + decision.action + (decision.target ? ' ' + decision.target : ''));
|
|
7425
7443
|
relayFailoverStatus = failover.snapshot();
|
|
7426
7444
|
if (decision.action !== 'switch') return;
|
|
7427
7445
|
switching = true;
|
|
@@ -8077,7 +8095,14 @@ async function startLocalStack(ctx, cfg) {
|
|
|
8077
8095
|
ensureFleetJoin = null;
|
|
8078
8096
|
inProcessAgentStatus = null;
|
|
8079
8097
|
await agent.stop();
|
|
8080
|
-
|
|
8098
|
+
// v1.6.7: `relay` is only a real relay object when THIS stack started a local relay; a remote-
|
|
8099
|
+
// relay stack has null here. The unconditional close() threw, which is what made the first
|
|
8100
|
+
// failover attempt stop the stack and never restart it.
|
|
8101
|
+
if (relay !== null && typeof relay.close === 'function') {
|
|
8102
|
+
try { await relay.close(); } catch (error) {
|
|
8103
|
+
ctx.logger?.warn?.('w2m: local relay close failed: ' + (error instanceof Error ? error.message : String(error)));
|
|
8104
|
+
}
|
|
8105
|
+
}
|
|
8081
8106
|
};
|
|
8082
8107
|
}
|
|
8083
8108
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/api/w2m/plugin-update` — the host half of the plugin panel's "更新" button (v1.6.8).
|
|
3
|
+
*
|
|
4
|
+
* Wire:
|
|
5
|
+
* GET /api/w2m/plugin-update[?check=1] -> { ok, current, latest, update_available, error? }
|
|
6
|
+
* POST /api/w2m/plugin-update { action: 'apply' }
|
|
7
|
+
* -> { ok, outcome, from, to, installed, restart_required, error? }
|
|
8
|
+
*
|
|
9
|
+
* The GET half asks npm for `latest` (src/plugin/npm-update.mjs). The POST half delegates the write
|
|
10
|
+
* to the already-verified updater: `auto-update.mjs` downloads the release tarball, verifies its
|
|
11
|
+
* SHA-256 against SHA256SUMS, installs it over the profile package with a backup, and records the
|
|
12
|
+
* outcome. This module never writes the profile itself — that division is what keeps a button click
|
|
13
|
+
* from inventing a second install path with weaker checks.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { checkNpmUpdate } from './npm-update.mjs';
|
|
19
|
+
|
|
20
|
+
export const PLUGIN_UPDATE_ROUTE_PATH = '/api/w2m/plugin-update';
|
|
21
|
+
const NO_STORE = { 'cache-control': 'no-store' };
|
|
22
|
+
|
|
23
|
+
/** Read the version installed in the profile's node_modules; null when it cannot be read. */
|
|
24
|
+
export function readInstalledVersion(profileDir) {
|
|
25
|
+
try {
|
|
26
|
+
const text = readFileSync(join(profileDir, 'node_modules', '@twinsearth', 'w2m-dsh-plugin', 'package.json'), 'utf8');
|
|
27
|
+
const parsed = JSON.parse(text);
|
|
28
|
+
return typeof parsed.version === 'string' ? parsed.version : null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mount the route on the host's Connection fetch registry.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} ctx Cordis context
|
|
38
|
+
* @param {{profileDir: () => string|null, createUpdater?: (options: object) => object}} deps
|
|
39
|
+
* @returns {{mounted: boolean, reason: string|null}}
|
|
40
|
+
*/
|
|
41
|
+
export function registerPluginUpdateRoutes(ctx, deps) {
|
|
42
|
+
const status = { mounted: false, reason: 'the host has no Connection service' };
|
|
43
|
+
if (typeof ctx?.inject !== 'function') return status;
|
|
44
|
+
ctx.inject(['connection'], (webCtx) => {
|
|
45
|
+
const registry = webCtx?.connection?.fetch;
|
|
46
|
+
if (registry === undefined || typeof registry.register !== 'function') {
|
|
47
|
+
status.reason = 'the connection service carries no fetch registry';
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
registry.register({
|
|
51
|
+
path: PLUGIN_UPDATE_ROUTE_PATH,
|
|
52
|
+
methods: ['GET', 'POST'],
|
|
53
|
+
requestBody: 'buffered',
|
|
54
|
+
fetch: async (request) => {
|
|
55
|
+
if (request.method !== 'GET' && request.method !== 'POST') {
|
|
56
|
+
return Response.json({ ok: false, error: request.method + ' is not a method this route answers', reason_code: 'METHOD_NOT_ALLOWED' }, { status: 405, headers: NO_STORE });
|
|
57
|
+
}
|
|
58
|
+
const profileDir = typeof deps?.profileDir === 'function' ? deps.profileDir() : null;
|
|
59
|
+
if (typeof profileDir !== 'string' || profileDir === '') {
|
|
60
|
+
return Response.json({ ok: false, error: 'no DSH profile directory was found, so there is nothing to update', reason_code: 'NO_PROFILE' }, { status: 400, headers: NO_STORE });
|
|
61
|
+
}
|
|
62
|
+
const current = readInstalledVersion(profileDir);
|
|
63
|
+
if (request.method === 'GET') {
|
|
64
|
+
const checked = await checkNpmUpdate({ currentVersion: current });
|
|
65
|
+
return Response.json({ ...checked, profile_dir: profileDir }, { headers: NO_STORE });
|
|
66
|
+
}
|
|
67
|
+
let body = null;
|
|
68
|
+
try { body = await request.json(); } catch { /* refused below */ }
|
|
69
|
+
if (body === null || typeof body !== 'object' || body.action !== 'apply') {
|
|
70
|
+
return Response.json({ ok: false, error: 'a JSON body with {"action":"apply"} is required', reason_code: 'UPDATE_BODY_INVALID' }, { status: 400, headers: NO_STORE });
|
|
71
|
+
}
|
|
72
|
+
// Refuse before installing when npm says there is nothing newer.
|
|
73
|
+
const checked = await checkNpmUpdate({ currentVersion: current });
|
|
74
|
+
if (checked.ok !== true) {
|
|
75
|
+
return Response.json({ ok: false, outcome: 'check-failed', error: checked.error, current, restart_required: false }, { status: 502, headers: NO_STORE });
|
|
76
|
+
}
|
|
77
|
+
if (checked.update_available !== true) {
|
|
78
|
+
return Response.json({ ok: true, outcome: 'up-to-date', current, latest: checked.latest, installed: false, restart_required: false }, { headers: NO_STORE });
|
|
79
|
+
}
|
|
80
|
+
let updater;
|
|
81
|
+
try {
|
|
82
|
+
updater = deps?.createUpdater !== undefined ? deps.createUpdater({ profileDir, currentVersion: current }) : await defaultUpdater({ profileDir, currentVersion: current });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return Response.json({ ok: false, outcome: 'error', error: error instanceof Error ? error.message : String(error), current, restart_required: false }, { status: 500, headers: NO_STORE });
|
|
85
|
+
}
|
|
86
|
+
const result = await updater.check('manual');
|
|
87
|
+
const installedTo = typeof result?.to === 'string' && result.to !== '' ? result.to : null;
|
|
88
|
+
const installed = result?.outcome === 'installed' || installedTo !== null && installedTo !== current;
|
|
89
|
+
return Response.json({
|
|
90
|
+
ok: result?.ok === true && installed === true,
|
|
91
|
+
outcome: result?.outcome ?? 'unknown',
|
|
92
|
+
from: result?.from ?? current,
|
|
93
|
+
to: installedTo,
|
|
94
|
+
installed,
|
|
95
|
+
reason: result?.reason ?? null,
|
|
96
|
+
error: result?.error ?? null,
|
|
97
|
+
restart_required: installed === true,
|
|
98
|
+
}, { headers: NO_STORE });
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
status.mounted = true;
|
|
102
|
+
status.reason = null;
|
|
103
|
+
});
|
|
104
|
+
return status;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Build the verified updater for a manual install (lazy import keeps this route cheap to load). */
|
|
108
|
+
async function defaultUpdater({ profileDir, currentVersion }) {
|
|
109
|
+
const { createAutoUpdater } = await import('./auto-update.mjs');
|
|
110
|
+
return createAutoUpdater({
|
|
111
|
+
profileDir,
|
|
112
|
+
currentVersion,
|
|
113
|
+
enabled: true,
|
|
114
|
+
// A button click is an explicit request: no daily minimum interval may swallow it.
|
|
115
|
+
intervalDays: 0,
|
|
116
|
+
log: () => {},
|
|
117
|
+
});
|
|
118
|
+
}
|