@twinsearth/w2m-dsh-plugin 1.6.7 → 1.6.9
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 +37 -0
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/1.6.8/PLUGIN-UPDATE-BUTTON.json +22 -0
- package/docs/acceptance/1.6.9/FAILOVER-MONITOR-INSTALL.json +35 -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 +17 -3
- 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.9"
|
|
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.9",
|
|
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.9"
|
|
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.9",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [1.6.9] — 2026-10-24
|
|
11
|
+
|
|
12
|
+
**修复 failover 监控从未安装的根因:`startLocalStack` 等待了永不 settle 的 `agent.start()`。**
|
|
13
|
+
|
|
14
|
+
阶段日志(macOS,1.6.8,现场实测):
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
W wrap enter relayUrls=[腾讯云, Vultr] ← 配置正确
|
|
18
|
+
A startLocalStack enter
|
|
19
|
+
C createAgent done
|
|
20
|
+
S before agent.start ← 之后没有任何日志
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
agent 实际已上线(编队在线),但 `await agent.start()` 的 Promise 永不返回,导致 `startLocalStack` 不返回 →
|
|
24
|
+
`startFailoverStack` 永远走不到 `relayPool(cfg)` / `setInterval`,**监控循环从未创建**,自动 failover 不可能触发。
|
|
25
|
+
|
|
26
|
+
修复:
|
|
27
|
+
|
|
28
|
+
- `startLocalStack` 不再 await `agent.start()`:后台启动,失败写日志(`w2m: agent start failed`)。
|
|
29
|
+
- mount 只需要返回清理句柄,不需要"观察启动完成";agent 会通过 relay 自报在线。
|
|
30
|
+
- 新增回归测试 `test/agent-start-nonblocking.test.mjs`:读取源码,若 `startLocalStack` 再次 await
|
|
31
|
+
`agent.start()` 即失败(防止该阻塞回归)。
|
|
32
|
+
- 证据:`docs/acceptance/1.6.9/FAILOVER-MONITOR-INSTALL.json`(fleet task `01M5ANYMVMKL55O`)。
|
|
33
|
+
## [1.6.8] — 2026-10-24
|
|
34
|
+
|
|
35
|
+
**插件面板新增「检查插件更新」按钮:npm 对比 → 自动更新 → 提示重启 DSH。**
|
|
36
|
+
|
|
37
|
+
- 客户端:目标机器菜单 relay 区新增按钮。点击后:① GET `/api/w2m/plugin-update` 联网查 npm latest
|
|
38
|
+
并与已装版本比较;② 有新版时 POST `{action:"apply"}` 执行安装;③ 安装完成显示"请重启 DSH 以载入最新插件"。
|
|
39
|
+
- 宿主:新增路由 `/api/w2m/plugin-update`。检查逻辑在 `src/plugin/npm-update.mjs`(纯函数,可单测:只认
|
|
40
|
+
`latest > installed`,网络失败是错误值而不是"已最新")。
|
|
41
|
+
- 安装复用**既有已验证更新器**(`auto-update.mjs` → `update-install.mjs`):下载 tarball、用 release
|
|
42
|
+
SHA256SUMS 校验 SHA-256、备份旧包、替换新版——按钮点击不会引入第二套弱校验安装路径。
|
|
43
|
+
- 测试:npm-update 4/4、client-bundle 26/26;证据 `docs/acceptance/1.6.8/PLUGIN-UPDATE-BUTTON.json`
|
|
44
|
+
(fleet task `01M5ANYMVMI9EHA`)。
|
|
45
|
+
- 边界:DSH 插件管理页的「卸载」按钮属于宿主应用(打包在 app.asar 内),插件包无法直接注入其旁边;
|
|
46
|
+
本按钮位于 w2m 插件自己的面板中。若要放进宿主插件页,需要 DSH 侧提供插件动作扩展点(已列为待评估)。
|
|
10
47
|
## [1.6.7] — 2026-10-24
|
|
11
48
|
|
|
12
49
|
**修复 v1.6.6 failover 的阻塞 bug:远端 relay 模式下清理函数抛错,切换后无法重启 stack。**
|
package/codex/mcp-servers.toml
CHANGED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.6.9",
|
|
3
|
+
"case": "FAILOVER-MONITOR-INSTALL",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b1b19d49-20f1-4cdc-986e-f5630a5cd6f2",
|
|
7
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
8
|
+
],
|
|
9
|
+
"executed_at": "2026-10-24T15:45:00.621Z",
|
|
10
|
+
"executor": "lead",
|
|
11
|
+
"requirement": "startLocalStack returns so startFailoverStack can install the relay health monitor; the agent start is a background action with recorded failure",
|
|
12
|
+
"root_cause": {
|
|
13
|
+
"observed_on": "macOS / plugin 1.6.8",
|
|
14
|
+
"stage_log": [
|
|
15
|
+
"W wrap enter relayUrls=[primary,backup]",
|
|
16
|
+
"A startLocalStack enter",
|
|
17
|
+
"C createAgent done",
|
|
18
|
+
"S before agent.start",
|
|
19
|
+
"(nothing further — await agent.start() never settled, while the machine was online in the fleet)"
|
|
20
|
+
],
|
|
21
|
+
"consequence": "startFailoverStack never reached relayPool(cfg)/setInterval, so the failover monitor was never created and automatic relay failover could not engage on any platform with the same agent.start behaviour"
|
|
22
|
+
},
|
|
23
|
+
"fix": {
|
|
24
|
+
"change": "startLocalStack no longer awaits agent.start(); it starts it in the background and logs a failure instead",
|
|
25
|
+
"guard": "test/agent-start-nonblocking.test.mjs reads the emitted source and fails if startLocalStack awaits the start again"
|
|
26
|
+
},
|
|
27
|
+
"task_id": "01M5ANYMVMKL55O",
|
|
28
|
+
"result": {
|
|
29
|
+
"outcome": "ok",
|
|
30
|
+
"assertions": {
|
|
31
|
+
"relay_failover": "5/5",
|
|
32
|
+
"nonblocking_guard": "2/2"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
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.9",
|
|
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 test/agent-start-nonblocking.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 test/agent-start-nonblocking.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.9",
|
|
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.9';
|
|
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
|
}
|
|
@@ -8007,8 +8011,18 @@ async function startLocalStack(ctx, cfg) {
|
|
|
8007
8011
|
}
|
|
8008
8012
|
}
|
|
8009
8013
|
|
|
8010
|
-
|
|
8011
|
-
|
|
8014
|
+
// v1.6.9: the agent DOES come up, but on macOS its `start()` promise was observed to stay
|
|
8015
|
+
// pending forever (stage log: "before agent.start" was the last line written). Awaiting it blocked
|
|
8016
|
+
// everything after this point — including the failover monitor installed by startFailoverStack,
|
|
8017
|
+
// which is why automatic relay failover never engaged: the monitor was never created.
|
|
8018
|
+
//
|
|
8019
|
+
// The mount does not need to OBSERVE the start to be complete: the agent announces itself through
|
|
8020
|
+
// the relay, and the value startFailoverStack needs from this function is the cleanup handle. So
|
|
8021
|
+
// start it in the background and record a failure instead of hanging the whole stack.
|
|
8022
|
+
agent.start().then(
|
|
8023
|
+
() => ctx.logger?.info?.('w2m: agent started as ' + JSON.stringify(agent.identity?.() ?? null)),
|
|
8024
|
+
(error) => ctx.logger?.warn?.('w2m: agent start failed: ' + (error instanceof Error ? error.message : String(error))),
|
|
8025
|
+
);
|
|
8012
8026
|
|
|
8013
8027
|
/**
|
|
8014
8028
|
* v0.8.12: **keep trying to join after mount.**
|
|
@@ -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
|
+
}
|