@twinsearth/w2m-dsh-plugin 1.1.17 → 1.1.19
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +75 -0
- package/README.md +46 -8
- package/codex/mcp-servers.toml +1 -1
- package/lib/client.js +179 -0
- package/lib/client.js.bak2 +640 -0
- package/package.json +6 -4
- package/plugin.json +2 -2
- package/src/plugin/fleet-chat-route.mjs +120 -0
- package/src/plugin/tools.mjs +72 -5
- package/src/plugin/update-channels.mjs +71 -0
- package/src/plugin/update-source.mjs +71 -0
- package/src/plugin/tools.mjs.bak-c7 +0 -7029
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.19",
|
|
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,8 +1,8 @@
|
|
|
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.
|
|
5
|
-
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through
|
|
4
|
+
"version": "1.1.19",
|
|
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": [
|
|
8
8
|
"agent-skills",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/api/w2m/chat` — the browser-facing half of fleet chat (v1.1.17 shipped the tool only; this
|
|
3
|
+
* module is the missing host route the composer's chat panel calls).
|
|
4
|
+
*
|
|
5
|
+
* The route proxies the relay's device-gated `/v1/fleet/messages` endpoints with this machine's
|
|
6
|
+
* device token. The browser never sees the token: it calls this same-origin route, which is inside
|
|
7
|
+
* the host's authentication fence, exactly like `/api/w2m/target`.
|
|
8
|
+
*
|
|
9
|
+
* Wire:
|
|
10
|
+
* GET /api/w2m/chat?session_id=…&since_ms=…&limit=… -> relay list shape passed through
|
|
11
|
+
* POST /api/w2m/chat { session_id, text } -> relay send shape passed through
|
|
12
|
+
*
|
|
13
|
+
* Failure is a value here: `{ok:false, error, reason_code, detail}` with a non-2xx status, so the
|
|
14
|
+
* panel can print the host's own words. A relay that predates the fleet-messages routes answers
|
|
15
|
+
* 404, which is reported as such rather than as an empty conversation.
|
|
16
|
+
*/
|
|
17
|
+
export const CHAT_ROUTE_PATH = '/api/w2m/chat';
|
|
18
|
+
export const MAX_SESSION_ID_LENGTH = 200;
|
|
19
|
+
|
|
20
|
+
const NO_STORE = { 'cache-control': 'no-store' };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Attach the route to one host's fetch registry, recording the outcome on `status`.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} ctx the plugin context (a Cordis fiber)
|
|
26
|
+
* @param {{cfg: () => object, request: Function, resolveToken: () => Promise<string|null>}} deps
|
|
27
|
+
* @returns {{mounted: boolean, reason: string|null}}
|
|
28
|
+
*/
|
|
29
|
+
export function registerChatRoutes(ctx, deps) {
|
|
30
|
+
const status = { mounted: false, reason: 'the host has no Connection service' };
|
|
31
|
+
if (typeof ctx?.inject !== 'function') return status;
|
|
32
|
+
ctx.inject(['connection'], (webCtx) => {
|
|
33
|
+
const registry = webCtx?.connection?.fetch;
|
|
34
|
+
if (registry === undefined || registry === null || typeof registry.register !== 'function') {
|
|
35
|
+
status.reason = 'the connection service carries no fetch registry';
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
registry.register({
|
|
40
|
+
path: CHAT_ROUTE_PATH,
|
|
41
|
+
methods: ['GET', 'POST'],
|
|
42
|
+
requestBody: 'buffered',
|
|
43
|
+
fetch: async (request) => {
|
|
44
|
+
const refuse = (httpStatus, reasonCode, error, detail) =>
|
|
45
|
+
Response.json({ ok: false, error, reason_code: reasonCode, detail }, { status: httpStatus, headers: NO_STORE });
|
|
46
|
+
|
|
47
|
+
if (request.method !== 'GET' && request.method !== 'POST') {
|
|
48
|
+
return refuse(405, 'CHAT_METHOD_NOT_ALLOWED', `${request.method} is not answered here`, 'GET lists a session; POST sends a message');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const url = new URL(request.url);
|
|
52
|
+
const sessionIdOf = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
53
|
+
let sessionId = '';
|
|
54
|
+
let text = null;
|
|
55
|
+
if (request.method === 'GET') {
|
|
56
|
+
sessionId = sessionIdOf(url.searchParams.get('session_id'));
|
|
57
|
+
} else {
|
|
58
|
+
let body;
|
|
59
|
+
try {
|
|
60
|
+
body = await request.json();
|
|
61
|
+
} catch {
|
|
62
|
+
return refuse(400, 'CHAT_BODY_NOT_JSON', 'body is not JSON', 'send {"session_id":"…","text":"…"}');
|
|
63
|
+
}
|
|
64
|
+
sessionId = sessionIdOf(body?.session_id);
|
|
65
|
+
text = typeof body?.text === 'string' ? body.text : null;
|
|
66
|
+
}
|
|
67
|
+
if (sessionId === '') return refuse(400, 'CHAT_SESSION_ID_REQUIRED', '`session_id` is required', 'chat is scoped to one conversation');
|
|
68
|
+
if (sessionId.length > MAX_SESSION_ID_LENGTH) {
|
|
69
|
+
return refuse(400, 'CHAT_SESSION_ID_TOO_LONG', `\`session_id\` must be at most ${MAX_SESSION_ID_LENGTH} characters`, 'send the id the host gave this session');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const cfg = deps.cfg();
|
|
73
|
+
const rabbitUrl = cfg?.rabbitUrl ?? null;
|
|
74
|
+
if (rabbitUrl === null) return refuse(503, 'CHAT_NO_RELAY', 'no relay is configured', 'set rabbitUrl in the profile patch');
|
|
75
|
+
let token;
|
|
76
|
+
try {
|
|
77
|
+
token = await deps.resolveToken();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return refuse(503, 'CHAT_UNPAIRED', `this machine has no device token: ${error instanceof Error ? error.message : String(error)}`, 'pair the machine first (autoStartAgent or a pairing code)');
|
|
80
|
+
}
|
|
81
|
+
if (typeof token !== 'string' || token === '') {
|
|
82
|
+
return refuse(503, 'CHAT_UNPAIRED', 'this machine has no device token', 'pair the machine first (autoStartAgent or a pairing code)');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
if (request.method === 'GET') {
|
|
87
|
+
const query = { session_id: sessionId };
|
|
88
|
+
const sinceMs = url.searchParams.get('since_ms');
|
|
89
|
+
const limit = url.searchParams.get('limit');
|
|
90
|
+
if (sinceMs !== null && /^\d+$/.test(sinceMs)) query.since_ms = sinceMs;
|
|
91
|
+
if (limit !== null && /^\d+$/.test(limit)) query.limit = limit;
|
|
92
|
+
const listed = await deps.request({ rabbitUrl, token, method: 'GET', pathname: '/v1/fleet/messages', query, signal: request.signal });
|
|
93
|
+
if (listed?.ok !== true) {
|
|
94
|
+
const code = listed?.json?.error?.code ?? listed?.error?.code ?? 'CHAT_LIST_REFUSED';
|
|
95
|
+
const message = listed?.json?.error?.message ?? listed?.error?.message ?? `the relay answered HTTP ${listed?.status ?? 0}`;
|
|
96
|
+
return refuse(listed?.status === 404 ? 502 : 502, code, message, 'the relay must run a version with /v1/fleet/messages (>= 1.1.17) and be restarted');
|
|
97
|
+
}
|
|
98
|
+
return Response.json({ ...listed.json, ok: true }, { headers: NO_STORE });
|
|
99
|
+
}
|
|
100
|
+
const posted = await deps.request({ rabbitUrl, token, method: 'POST', pathname: '/v1/fleet/messages', body: { session_id: sessionId, text: text ?? '' }, signal: request.signal });
|
|
101
|
+
if (posted?.ok !== true) {
|
|
102
|
+
const code = posted?.json?.error?.code ?? posted?.error?.code ?? 'CHAT_SEND_REFUSED';
|
|
103
|
+
const message = posted?.json?.error?.message ?? posted?.error?.message ?? `the relay answered HTTP ${posted?.status ?? 0}`;
|
|
104
|
+
return refuse(posted?.status === 404 ? 502 : 400, code, message, 'quota, size and non-text are each refused by name; 404 means the relay predates fleet chat');
|
|
105
|
+
}
|
|
106
|
+
return Response.json({ ...posted.json, ok: true }, { headers: NO_STORE });
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return refuse(502, 'CHAT_RELAY_ERROR', `the relay call failed: ${error instanceof Error ? error.message : String(error)}`, 'check the relay and this machine token');
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
status.mounted = true;
|
|
113
|
+
status.reason = null;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
status.reason = error instanceof Error ? error.message : String(error);
|
|
116
|
+
ctx?.logger?.warn?.(`w2m: the fleet chat route is inactive — ${status.reason}`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return status;
|
|
120
|
+
}
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -89,7 +89,10 @@
|
|
|
89
89
|
*/
|
|
90
90
|
|
|
91
91
|
import { execFile } from 'node:child_process';
|
|
92
|
-
|
|
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';
|
|
@@ -177,6 +180,9 @@ import {
|
|
|
177
180
|
sessionKeyFor,
|
|
178
181
|
TARGET_ROUTE_PATH,
|
|
179
182
|
} from './fleet-target.mjs';
|
|
183
|
+
// v1.1.17 chat: the host route behind the composer's fleet-chat panel. The relay side and the
|
|
184
|
+
// `w2m_chat` tool shipped first; this is the browser-facing half that was still missing.
|
|
185
|
+
import { registerChatRoutes } from './fleet-chat-route.mjs';
|
|
180
186
|
// v0.7.0: the LAN hint layer. Imported here and used in exactly one place (`w2m_devices`); see the
|
|
181
187
|
// module header for why a discovered device is a candidate to *ask* and never an authorisation.
|
|
182
188
|
import { createDiscoverySource } from './mdns-discover.mjs';
|
|
@@ -214,7 +220,7 @@ const MAX_STAGES = 16;
|
|
|
214
220
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
215
221
|
* overrides it at runtime for tests and packagers.
|
|
216
222
|
*/
|
|
217
|
-
const PLUGIN_VERSION = '1.1.
|
|
223
|
+
const PLUGIN_VERSION = '1.1.19';
|
|
218
224
|
|
|
219
225
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
220
226
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
@@ -3398,6 +3404,20 @@ export async function apply(ctx, config = {}) {
|
|
|
3398
3404
|
resolveToken,
|
|
3399
3405
|
});
|
|
3400
3406
|
|
|
3407
|
+
// The composer's fleet-chat panel: `GET/POST /api/w2m/chat`, proxied to the relay's device-gated
|
|
3408
|
+
// `/v1/fleet/messages`. Optional like the target routes — a host without Connection keeps every
|
|
3409
|
+
// tool and the panel is simply not mounted.
|
|
3410
|
+
const chatRoutes = registerChatRoutes(ctx, {
|
|
3411
|
+
cfg: () => cfg,
|
|
3412
|
+
request,
|
|
3413
|
+
resolveToken: () => resolveToken(cfg),
|
|
3414
|
+
});
|
|
3415
|
+
if (chatRoutes.mounted !== true) {
|
|
3416
|
+
// Named rather than silent: a composition without the fetch registry loses the chat panel while every tool still
|
|
3417
|
+
// works, and "the panel is missing" with no line anywhere is the shape this project keeps paying for.
|
|
3418
|
+
ctx.logger?.warn?.('w2m: the fleet-chat composer route is not mounted (' + (chatRoutes.reason ?? 'unknown reason') + ')');
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3401
3421
|
// The LAN discovery source (v0.7.0). Constructing it binds nothing: a socket is opened only when a
|
|
3402
3422
|
// scan is asked for, and only `w2m_devices` asks. A network where multicast is unavailable produces
|
|
3403
3423
|
// one log line and an `available: false` block in the answer - never a failure of the tool.
|
|
@@ -4858,7 +4878,50 @@ export async function apply(ctx, config = {}) {
|
|
|
4858
4878
|
// `jsonOutput` is a renderer, not a serialiser; the tool still owns turning its value into
|
|
4859
4879
|
// text.
|
|
4860
4880
|
if (action === 'status') {
|
|
4861
|
-
|
|
4881
|
+
const described = { ok: true, action, update: updater.describe() };
|
|
4882
|
+
// Opt-in on purpose: the plain status call is local and offline, and making it reach GitHub would slow every
|
|
4883
|
+
// w2m_status and fail behind a proxy. The three channels need two API calls, so they are asked for explicitly.
|
|
4884
|
+
if (args?.channels !== true) return JSON.stringify(described, null, 2);
|
|
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
|
+
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);
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4892
|
+
/**
|
|
4893
|
+
* v1.1.18: install a specific channel. The beta channel exists only as a GitHub prerelease, so it is verified here
|
|
4894
|
+
* rather than trusted: the tarball sha256 must match the SHA256SUMS shipped with the same release, and
|
|
4895
|
+
* downloadVerified removes its temp file on a mismatch, leaving the installed profile untouched.
|
|
4896
|
+
*/
|
|
4897
|
+
if (action === 'apply') {
|
|
4898
|
+
const channel = typeof args?.channel === 'string' ? args.channel : 'stable';
|
|
4899
|
+
if (channel !== 'stable' && channel !== 'beta' && channel !== 'latest') {
|
|
4900
|
+
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' });
|
|
4901
|
+
}
|
|
4902
|
+
const repo = (typeof process.env.W2M_UPDATE_REPO === 'string' && process.env.W2M_UPDATE_REPO.trim() !== '')
|
|
4903
|
+
? process.env.W2M_UPDATE_REPO.trim() : DEFAULT_REPO;
|
|
4904
|
+
const [stableRelease, betaRelease] = await Promise.all([fetchLatestRelease({ repo }), fetchPrereleaseRelease({ repo })]);
|
|
4905
|
+
const currentVersion = resolveCurrentVersion(null, process.env);
|
|
4906
|
+
const resolved = resolveChannels({ stable: stableRelease?.ok === true ? stableRelease : null, beta: betaRelease?.ok === true ? betaRelease : null, currentVersion });
|
|
4907
|
+
const wanted = channel === 'latest' ? resolved.latest.channel : channel;
|
|
4908
|
+
const release = wanted === 'beta' ? betaRelease : stableRelease;
|
|
4909
|
+
if (release?.ok !== true) {
|
|
4910
|
+
throw new W2MError('W2M_NO_CHANNEL', 'W2M_NO_CHANNEL: nothing is available on the ' + channel + ' channel', { hint: release?.code ? release.code + ': ' + release.error : undefined });
|
|
4911
|
+
}
|
|
4912
|
+
const sumsAsset = release.assets?.sums;
|
|
4913
|
+
const tarballAsset = release.assets?.tarball;
|
|
4914
|
+
if (!sumsAsset || !tarballAsset) throw new W2MError('W2M_NO_ASSETS', 'W2M_NO_ASSETS: the release carries no tarball and SHA256SUMS');
|
|
4915
|
+
const sumsResponse = await fetch(sumsAsset.url, { headers: sumsAsset.headers ?? {} });
|
|
4916
|
+
if (sumsResponse?.ok !== true) throw new W2MError('W2M_SUMS', 'W2M_SUMS: could not download SHA256SUMS (HTTP ' + sumsResponse?.status + ')');
|
|
4917
|
+
const sha256 = requireChecksum(await sumsResponse.text(), tarballAsset.name);
|
|
4918
|
+
const profileDir = args?.profile_dir ?? findProfileDir({});
|
|
4919
|
+
const staged = await downloadVerified({ url: tarballAsset.url, sha256, destPath: (args?.staging_dir ?? profileDir) + '/.w2m-update/' + tarballAsset.name });
|
|
4920
|
+
if (staged?.ok !== true) {
|
|
4921
|
+
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' });
|
|
4922
|
+
}
|
|
4923
|
+
const installed = await installUpdate({ tarballPath: staged.path, profileDir, expectedSha256: sha256, dryRun: args?.dry_run === true });
|
|
4924
|
+
return JSON.stringify({ ok: installed?.ok === true, action, channel, version: resolved.channels[wanted]?.available ?? null, sha256, install: installed }, null, 2);
|
|
4862
4925
|
}
|
|
4863
4926
|
const result = await updater.check('manual');
|
|
4864
4927
|
// `ok:false` inside `result` means "the check could not complete", never "the tool failed".
|
|
@@ -6274,10 +6337,14 @@ export async function apply(ctx, config = {}) {
|
|
|
6274
6337
|
const action = typeof args?.action === 'string' ? args.action : 'list';
|
|
6275
6338
|
const sessionId = typeof args?.session_id === 'string' ? args.session_id.trim() : '';
|
|
6276
6339
|
if (sessionId === '') throw new W2MError('W2M_CHAT_SESSION', 'W2M_CHAT_SESSION: session_id is required', { hint: 'chat is scoped to a conversation' });
|
|
6277
|
-
const device = await readDevice(cfg);
|
|
6278
6340
|
const rabbitUrl = cfg.rabbitUrl ?? null;
|
|
6279
6341
|
if (rabbitUrl === null) throw new W2MError('W2M_NO_RELAY', 'W2M_NO_RELAY: no relay is configured, so chat cannot be reached', { hint: 'set rabbitUrl in the profile patch' });
|
|
6280
|
-
|
|
6342
|
+
// resolveToken, not readDevice: readDevice returns an empty identity whenever `stateDir` is
|
|
6343
|
+
// unset, even though this machine's device.json exists at the default DSH_HOME path — every
|
|
6344
|
+
// other device-authenticated tool goes through resolveToken for exactly that reason, and this
|
|
6345
|
+
// one reported W2M_NO_TOKEN on a machine that was paired and answering every other call.
|
|
6346
|
+
const token = await resolveToken(cfg);
|
|
6347
|
+
if (token === null) throw new W2MError('W2M_NO_TOKEN', 'W2M_NO_TOKEN: this machine has no device token, so chat cannot be authenticated', { hint: 'pair this machine first (autoStartAgent or a pairing code), then retry' });
|
|
6281
6348
|
if (action === 'send') {
|
|
6282
6349
|
const posted = await request({ rabbitUrl, token, signal: exec?.signal, method: 'POST', pathname: '/v1/fleet/messages', body: { session_id: sessionId, text: typeof args?.text === 'string' ? args.text : '' } });
|
|
6283
6350
|
if (posted?.ok !== true) {
|
|
@@ -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
|
+
}
|