@bolloon/bolloon-agent 0.4.32 → 0.5.0
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/dist/agents/chain/explorer.js +52 -0
- package/dist/agents/chain/index.js +2 -0
- package/dist/agents/chain/onchain-trade.js +15 -1
- package/dist/agents/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +493 -35
- package/dist/agents/external-events.js +35 -6
- package/dist/agents/gateway-group.js +100 -12
- package/dist/agents/goal-criteria.js +10 -3
- package/dist/agents/goal-flywheel/block-executor.js +346 -0
- package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
- package/dist/agents/goal-flywheel/goal-change.js +754 -0
- package/dist/agents/goal-flywheel/index.js +26 -0
- package/dist/agents/goal-flywheel/memory-layers.js +227 -0
- package/dist/agents/goal-flywheel/run-closure.js +809 -0
- package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
- package/dist/agents/goal-flywheel/types.js +329 -0
- package/dist/agents/goal-flywheel/wiring/change.js +288 -0
- package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
- package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
- package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
- package/dist/agents/goal-flywheel/wiring/index.js +26 -0
- package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
- package/dist/agents/goal-flywheel/work-contract.js +478 -0
- package/dist/agents/goal-flywheel/work-monitor.js +488 -0
- package/dist/agents/goal-flywheel-wiring.js +1870 -0
- package/dist/agents/goal-state-reducer.js +406 -0
- package/dist/agents/goal-store.js +35 -1
- package/dist/agents/mobile-flywheel-view.js +144 -0
- package/dist/agents/mobile-task-actions-verify.js +74 -0
- package/dist/agents/mobile-task-actions.js +178 -0
- package/dist/agents/mobile-task-views.js +199 -0
- package/dist/agents/network-pulse.js +525 -27
- package/dist/agents/pi-sdk.js +75 -20
- package/dist/agents/run-store.js +18 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-onchain-runner.js +19 -1
- package/dist/agents/task/task-runner.js +36 -6
- package/dist/agents/task-board.js +702 -0
- package/dist/agents/task-group.js +524 -0
- package/dist/agents/task-public-text.js +89 -0
- package/dist/cli/commands/index.js +3 -0
- package/dist/cli/commands/tasks.js +977 -8
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/protocol-envelope.js +4 -0
- package/dist/cli/setup-wizard.js +71 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +12 -2
- package/dist/electron-build/agents/chain/explorer.js +59 -0
- package/dist/electron-build/agents/chain/explorer.js.map +1 -0
- package/dist/electron-build/agents/contacts/chain.js +26 -6
- package/dist/electron-build/agents/contacts/chain.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +492 -34
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/external-events.js +34 -5
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-criteria.js +10 -3
- package/dist/electron-build/agents/goal-criteria.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
- package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
- package/dist/electron-build/agents/goal-state-reducer.js +414 -0
- package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
- package/dist/electron-build/agents/goal-store.js +35 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/local-signer.js +101 -0
- package/dist/electron-build/agents/local-signer.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +531 -28
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +74 -19
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +20 -5
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/skill-readiness.js +52 -9
- package/dist/electron-build/agents/skill-readiness.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +35 -5
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/task-board.js +761 -0
- package/dist/electron-build/agents/task-board.js.map +1 -0
- package/dist/electron-build/agents/task-contract.js +382 -0
- package/dist/electron-build/agents/task-contract.js.map +1 -0
- package/dist/electron-build/orbitdb/cid-database.js +83 -17
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/utils/dual-source.js +471 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +331 -57
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +16 -1
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-info.js +82 -2
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +103 -12
- package/dist/ios/chain-explorer.js +617 -0
- package/dist/ios/explorer.html +163 -0
- package/dist/ios/index.html +96 -0
- package/dist/ios/mobile-agent.js +2 -2
- package/dist/ios/mobile-contacts.js +329 -0
- package/dist/ios/mobile-core.js +1379 -226
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +706 -7
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +762 -0
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/utils/dual-source.js +426 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +329 -57
- package/dist/utils/update-state.js +16 -1
- package/dist/utils/version-info.js +75 -1
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +1197 -513
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +531 -1
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/package.json +3 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dual-source.ts — 双源 (npm registry + GitHub) 的**源事实** (2026-09-25, update-protocol §12)
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独一个模块: §1.4 冻结的是 "npm 是唯一稳定渠道", §12 把它改成**双源但权威顺序不变**。
|
|
5
|
+
* 这件事不该塞进 update-manager (那会让"唯一决策者"同时变成"网络客户端"), 也不该让
|
|
6
|
+
* CLI / 健康检查各自造一份 GitHub 事实。所以:
|
|
7
|
+
*
|
|
8
|
+
* - 源事实 (GitHub 可达性 / tag / Release / master HEAD) 只在本模块查
|
|
9
|
+
* - 错误分类 `classifyGithubError` 与 §3 的 `classifyRegistryError` **并列, 不合并**
|
|
10
|
+
* - dev 快照的**取源与构建**也在这里 (update-manager 只负责"切换 + 验证 + 回滚", 不另造一套)
|
|
11
|
+
*
|
|
12
|
+
* 硬口径 (§12.5, 验收核心):
|
|
13
|
+
* - 源不可达 / 版本不存在 → **拒绝并说清** (带具体分类), 绝不静默装回旧版
|
|
14
|
+
* - 不编造不存在的路径: 本仓 GitHub 上**真的没有 Release** (0 个), 所以 "Release 缺" 时
|
|
15
|
+
* 如实报 "发布记录缺 Tag / Release" (提醒级), **不是** fake 一个 Release 出来
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as fsp from 'fs/promises';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import * as http from 'http';
|
|
21
|
+
import * as https from 'https';
|
|
22
|
+
import { spawnSync } from 'child_process';
|
|
23
|
+
import { GITHUB_API_BASE, GITHUB_UPSTREAM_SLUG, DEV_BRANCH, devIdentity, baseVersionOf, readPackageAt, DEV_CHANNEL_WARNING, } from './version-info.js';
|
|
24
|
+
export { DEV_CHANNEL_WARNING };
|
|
25
|
+
export function toGithubReport(r) {
|
|
26
|
+
if (!r.ok) {
|
|
27
|
+
return {
|
|
28
|
+
reachable: false, reason: r.reason, detail: r.detail, retryAt: r.retryAt,
|
|
29
|
+
headSha: r.facts?.headSha ?? null, ref: r.facts?.ref ?? `refs/heads/${DEV_BRANCH}`,
|
|
30
|
+
releases: r.facts?.releases.length, tags: r.facts?.tags.length,
|
|
31
|
+
newestVersion: r.facts ? newestGithubVersion(r.facts) : null,
|
|
32
|
+
partial: r.facts?.partial, fetchedAt: r.facts?.fetchedAt,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
reachable: true, headSha: r.facts.headSha, ref: r.facts.ref,
|
|
37
|
+
releases: r.facts.releases.length, tags: r.facts.tags.length,
|
|
38
|
+
newestVersion: newestGithubVersion(r.facts), partial: r.facts.partial, fetchedAt: r.facts.fetchedAt,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function renderGithubReportLine(g) {
|
|
42
|
+
if (!g)
|
|
43
|
+
return 'GitHub 源: 未查询';
|
|
44
|
+
if (!g.reachable) {
|
|
45
|
+
const retry = g.retryAt ? `, 限流重试时间 ${g.retryAt}` : '';
|
|
46
|
+
return `GitHub 源: 不可达 — github_unavailable(${g.reason}) ${g.detail || ''}${retry}`;
|
|
47
|
+
}
|
|
48
|
+
return `GitHub 源: 可达 · master HEAD ${g.headSha ? g.headSha.slice(0, 7) : '未知'} · Release ${g.releases ?? 0} 个 · Tag ${g.tags ?? 0} 个 · 最新版本标签 ${g.newestVersion || '无'}${g.partial ? ' (部分请求失败)' : ''}`;
|
|
49
|
+
}
|
|
50
|
+
// ── HTTP ────────────────────────────────────────────────────────────────────
|
|
51
|
+
const OFFLINE_CODES = new Set(['ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'EHOSTUNREACH', 'ENETUNREACH', 'ENETDOWN', 'EAI_FAIL', 'UND_ERR_CONNECT_TIMEOUT', 'CERT_HAS_EXPIRED']);
|
|
52
|
+
/** 可选 GitHub token (只为提配额: 匿名 60 次/小时, 认证 5000 次/小时)。**永不打印、永不入库**。 */
|
|
53
|
+
function githubToken(env = process.env) {
|
|
54
|
+
const t = env.GITHUB_TOKEN || env.GH_TOKEN || env.BOLLOON_GITHUB_TOKEN;
|
|
55
|
+
return t && String(t).trim() ? String(t).trim() : null;
|
|
56
|
+
}
|
|
57
|
+
function httpGetRaw(url, timeoutMs, token) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const client = url.startsWith('https') ? https : http;
|
|
60
|
+
const req = client.get(url, {
|
|
61
|
+
timeout: timeoutMs,
|
|
62
|
+
headers: {
|
|
63
|
+
Accept: 'application/vnd.github+json, application/json',
|
|
64
|
+
'User-Agent': 'bolloon-update',
|
|
65
|
+
// token 只进请求头, 不进日志/状态/输出 (凭证红线)
|
|
66
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
67
|
+
},
|
|
68
|
+
}, (res) => {
|
|
69
|
+
let data = '';
|
|
70
|
+
res.on('data', (c) => { data += c; });
|
|
71
|
+
res.on('end', () => resolve({ status: res.statusCode || 0, body: data, headers: (res.headers || {}) }));
|
|
72
|
+
});
|
|
73
|
+
req.on('error', reject);
|
|
74
|
+
req.on('timeout', () => { req.destroy(new Error('timeout')); });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** 网络类 → offline; 403/429 → rate_limited; 404 → not_found; 其它 → http_error。 */
|
|
78
|
+
export function classifyGithubError(err, httpStatus, headers = {}) {
|
|
79
|
+
const retryAt = headers['x-ratelimit-reset']
|
|
80
|
+
? new Date(Number(headers['x-ratelimit-reset']) * 1000).toISOString()
|
|
81
|
+
: null;
|
|
82
|
+
const code = String(err?.code || '');
|
|
83
|
+
if (OFFLINE_CODES.has(code) || /timeout|timed out|ENOTFOUND|getaddrinfo|network|socket hang up|超时/i.test(String(err?.message || ''))) {
|
|
84
|
+
return { kind: 'github_unavailable', reason: 'offline', detail: `${code || err?.message || '网络不可达'}`, retryAt: null };
|
|
85
|
+
}
|
|
86
|
+
if (httpStatus === 403 || httpStatus === 429) {
|
|
87
|
+
return { kind: 'github_unavailable', reason: 'rate_limited', detail: `GitHub 返回 HTTP ${httpStatus} (限流/权限)`, retryAt };
|
|
88
|
+
}
|
|
89
|
+
if (httpStatus === 404) {
|
|
90
|
+
return { kind: 'github_unavailable', reason: 'not_found', detail: `GitHub 上没有 ${GITHUB_UPSTREAM_SLUG} 的该项记录 (HTTP 404)`, retryAt: null };
|
|
91
|
+
}
|
|
92
|
+
if (httpStatus && httpStatus >= 400) {
|
|
93
|
+
return { kind: 'github_unavailable', reason: 'http_error', detail: `GitHub 返回 HTTP ${httpStatus}`, retryAt };
|
|
94
|
+
}
|
|
95
|
+
return { kind: 'github_unavailable', reason: 'http_error', detail: code || err?.message || '未知错误', retryAt: null };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 查 GitHub 的三个事实: Releases / Tags / master HEAD。
|
|
99
|
+
* **任何一个请求失败 → 整条 `github_unavailable`** (带分类), 但把已拿到的那部分 facts 一起带回去
|
|
100
|
+
* (上层因此能说清"哪些拿到了、哪些没拿到", 而不是一句"更新失败")。
|
|
101
|
+
*/
|
|
102
|
+
export async function fetchGithubFacts(opts = {}) {
|
|
103
|
+
const base = (opts.apiBase || GITHUB_API_BASE).replace(/\/+$/, '');
|
|
104
|
+
const slug = opts.slug || GITHUB_UPSTREAM_SLUG;
|
|
105
|
+
const timeout = opts.timeoutMs ?? 10000;
|
|
106
|
+
// 有 token 就用 (配额 60/h → 5000/h); 没有也能跑, 只是可能 403 限流
|
|
107
|
+
const token = opts.token !== undefined ? opts.token : githubToken();
|
|
108
|
+
const missing = [];
|
|
109
|
+
let releases = [];
|
|
110
|
+
let tags = [];
|
|
111
|
+
let headSha = null;
|
|
112
|
+
/** 失败累积 (第一个失败决定分类, 但所有失败都记进 missing) */
|
|
113
|
+
const failures = [];
|
|
114
|
+
const note = (e, what) => {
|
|
115
|
+
missing.push(what);
|
|
116
|
+
failures.push({ reason: e.reason, detail: `${what}: ${e.detail}`, retryAt: e.retryAt });
|
|
117
|
+
};
|
|
118
|
+
// 1. Releases (本仓真的没有 → 200 + [] , 不是错误)
|
|
119
|
+
try {
|
|
120
|
+
const r = await httpGetRaw(`${base}/repos/${slug}/releases?per_page=100`, timeout, token);
|
|
121
|
+
if (r.status >= 200 && r.status < 300) {
|
|
122
|
+
const arr = JSON.parse(r.body);
|
|
123
|
+
releases = Array.isArray(arr) ? arr.map((x) => String(x?.tag_name || '')).filter(Boolean) : [];
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
note(classifyGithubError(null, r.status, r.headers), 'releases');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
note(classifyGithubError(e), 'releases');
|
|
131
|
+
}
|
|
132
|
+
// 2. Tags
|
|
133
|
+
try {
|
|
134
|
+
const r = await httpGetRaw(`${base}/repos/${slug}/tags?per_page=100`, timeout, token);
|
|
135
|
+
if (r.status >= 200 && r.status < 300) {
|
|
136
|
+
const arr = JSON.parse(r.body);
|
|
137
|
+
tags = Array.isArray(arr)
|
|
138
|
+
? arr.map((x) => ({ name: String(x?.name || ''), sha: x?.commit?.sha ? String(x.commit.sha) : null })).filter((t) => t.name)
|
|
139
|
+
: [];
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
note(classifyGithubError(null, r.status, r.headers), 'tags');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
note(classifyGithubError(e), 'tags');
|
|
147
|
+
}
|
|
148
|
+
// 3. master HEAD (dev 通道的身份来源)
|
|
149
|
+
try {
|
|
150
|
+
const r = await httpGetRaw(`${base}/repos/${slug}/commits/${DEV_BRANCH}`, timeout, token);
|
|
151
|
+
if (r.status >= 200 && r.status < 300) {
|
|
152
|
+
const j = JSON.parse(r.body);
|
|
153
|
+
headSha = j?.sha ? String(j.sha) : null;
|
|
154
|
+
if (!headSha)
|
|
155
|
+
note({ reason: 'parse_error', detail: 'commits/master 没有 sha 字段', retryAt: null }, 'head');
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// 404 在这里的语义就是"没有 master 分支"(§12.3)
|
|
159
|
+
note(classifyGithubError(null, r.status, r.headers), 'head');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
note(classifyGithubError(e), 'head');
|
|
164
|
+
}
|
|
165
|
+
const facts = {
|
|
166
|
+
slug, ref: `refs/heads/${DEV_BRANCH}`, headSha, releases, tags,
|
|
167
|
+
fetchedAt: new Date().toISOString(), partial: missing.length > 0, missing,
|
|
168
|
+
};
|
|
169
|
+
if (failures.length > 0) {
|
|
170
|
+
const first = failures[0];
|
|
171
|
+
return { ok: false, kind: 'github_unavailable', reason: first.reason, detail: first.detail, retryAt: first.retryAt, facts };
|
|
172
|
+
}
|
|
173
|
+
return { ok: true, facts };
|
|
174
|
+
}
|
|
175
|
+
/** GitHub 上出现过的最高版本号 (Release ∪ Tag, 去掉 v 前缀)。没有 → null。 */
|
|
176
|
+
export function newestGithubVersion(facts) {
|
|
177
|
+
const all = [...facts.releases, ...facts.tags.map((t) => t.name)]
|
|
178
|
+
.map((n) => String(n).trim().replace(/^v/, ''))
|
|
179
|
+
.filter((n) => /^\d+\.\d+\.\d+/.test(n));
|
|
180
|
+
if (all.length === 0)
|
|
181
|
+
return null;
|
|
182
|
+
return all.sort((a, b) => (numericCompare(a, b) < 0 ? 1 : -1))[0];
|
|
183
|
+
}
|
|
184
|
+
/** 只比数值段 (与 update-manager 的 compareVersions 同语义, 这里不反向依赖它)。 */
|
|
185
|
+
function numericCompare(a, b) {
|
|
186
|
+
const p = (v) => v.split('-')[0].split('.').map((x) => parseInt(x.replace(/\D.*$/, ''), 10) || 0);
|
|
187
|
+
const pa = p(a);
|
|
188
|
+
const pb = p(b);
|
|
189
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
190
|
+
const x = pa[i] ?? 0;
|
|
191
|
+
const y = pb[i] ?? 0;
|
|
192
|
+
if (x < y)
|
|
193
|
+
return -1;
|
|
194
|
+
if (x > y)
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
function hasName(names, v) {
|
|
200
|
+
const set = new Set(names.map((n) => String(n).trim().replace(/^v/, '')));
|
|
201
|
+
return set.has(v.replace(/^v/, ''));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* stable 的交叉校验 (§12.1/§12.5): npm 仍是权威, GitHub 只回答"这次发布的记录在不在"。
|
|
205
|
+
*
|
|
206
|
+
* agree npm 的 latest 在 GitHub 上有同名 tag/Release → 两条记录指向同一版
|
|
207
|
+
* mismatch GitHub 记录了**更新的**版本而 registry 上没有 → 拒绝 (不许按 tag 去装)
|
|
208
|
+
* missing_record registry 有该版本但 GitHub 缺这条记录 → **提醒级, 不阻塞** (§12.5 原文:
|
|
209
|
+
* "报发布记录缺 Tag (提醒级, 不阻塞 stable 更新)")
|
|
210
|
+
* no_record_at_all GitHub 上一条版本记录都没有 → 如实报"暂无 tag 可交叉校验"(不编造路径)
|
|
211
|
+
*/
|
|
212
|
+
export function crossCheckStable(registryLatest, facts) {
|
|
213
|
+
const common = {
|
|
214
|
+
registryLatest, githubVersion: newestGithubVersion(facts),
|
|
215
|
+
releases: facts.releases.length, tags: facts.tags.length,
|
|
216
|
+
};
|
|
217
|
+
if (!registryLatest) {
|
|
218
|
+
return { ...common, kind: 'no_record_at_all', hasTag: false, hasRelease: false, blocking: false, detail: 'npm 没有给出 latest, 无从交叉校验' };
|
|
219
|
+
}
|
|
220
|
+
const allNames = [...facts.releases, ...facts.tags.map((t) => t.name)];
|
|
221
|
+
const hasTag = hasName(facts.tags.map((t) => t.name), registryLatest);
|
|
222
|
+
const hasRelease = hasName(facts.releases, registryLatest);
|
|
223
|
+
const ghNewest = common.githubVersion;
|
|
224
|
+
/**
|
|
225
|
+
* **先判"GitHub 记录了比 registry 更新的发布"** —— 这是 §12.5 的 "Release 存在但 registry 上没有该版本 → cross_check_mismatch"。
|
|
226
|
+
* 放在 agree 之前: 即使 registry 的 latest 自己也有 tag, 只要 GitHub 上还多出一条更新的发布记录, 两个源对
|
|
227
|
+
* "当前最新是哪一版"就是不一致的 —— 这种不一致必须拦住, 不能因为"latest 有 tag"就宣布一致。
|
|
228
|
+
*/
|
|
229
|
+
if (ghNewest && numericCompare(ghNewest, registryLatest) > 0) {
|
|
230
|
+
return {
|
|
231
|
+
...common, kind: 'mismatch', hasTag, hasRelease, blocking: true,
|
|
232
|
+
detail: `GitHub 上有 ${ghNewest} 的发布记录, 但 npm registry 的 latest 还是 ${registryLatest} — 拒绝按 GitHub 的记录安装`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (hasTag || hasRelease) {
|
|
236
|
+
return {
|
|
237
|
+
...common, kind: 'agree', hasTag, hasRelease, blocking: false,
|
|
238
|
+
detail: `npm latest=${registryLatest} 在 GitHub 上有同名记录 (${hasRelease ? `Release v${registryLatest}` : `Tag v${registryLatest}`}) — 两个源指向同一版`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (allNames.length === 0) {
|
|
242
|
+
return {
|
|
243
|
+
...common, kind: 'no_record_at_all', hasTag: false, hasRelease: false, blocking: false,
|
|
244
|
+
detail: `GitHub 上暂无任何 tag / Release (0 个) — 暂无可交叉校验的记录, npm 仍是权威 (latest=${registryLatest})`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
...common, kind: 'missing_record', hasTag, hasRelease, blocking: false,
|
|
249
|
+
detail: `npm latest=${registryLatest} 在 GitHub 上没有对应记录 (GitHub 最新版本标签 ${ghNewest || '无'}; Release ${facts.releases.length} 个 / Tag ${facts.tags.length} 个) — 发布记录缺 Tag, 不阻塞 stable 更新`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
export function compareDevSnapshots(installedSha, headSha, ref = `refs/heads/${DEV_BRANCH}`) {
|
|
253
|
+
const short = (s) => (s ? s.slice(0, 7) : '未知');
|
|
254
|
+
if (!headSha) {
|
|
255
|
+
return { ref, headSha, installedSha, same: false, behind: false, detail: `${ref} 的 HEAD 读不到 — 无法判定 dev 快照新旧` };
|
|
256
|
+
}
|
|
257
|
+
if (!installedSha) {
|
|
258
|
+
return { ref, headSha, installedSha, same: false, behind: false, detail: `当前不是 dev 快照 (stable 安装) — 切到 dev 将得到 ${ref} @ ${short(headSha)}` };
|
|
259
|
+
}
|
|
260
|
+
const same = installedSha.slice(0, 7) === headSha.slice(0, 7);
|
|
261
|
+
return {
|
|
262
|
+
ref, headSha, installedSha, same, behind: !same,
|
|
263
|
+
detail: same
|
|
264
|
+
? `已是最新的 dev 快照 (${ref} @ ${short(headSha)}) — 同一个 commit`
|
|
265
|
+
: `dev 快照落后: 已装 ${short(installedSha)} → master HEAD ${short(headSha)} (按 commit sha 判定, 不看版本号)`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function defaultRunNpm(args, cwd) {
|
|
269
|
+
const r = spawnSync('npm', args, { cwd, encoding: 'utf-8', timeout: 900_000, maxBuffer: 32 * 1024 * 1024 });
|
|
270
|
+
return { code: r.status ?? 1, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
271
|
+
}
|
|
272
|
+
/** 从 GitHub 取 master 的源码快照 (codeload tarball); 解到 destParent。 */
|
|
273
|
+
async function downloadGithubSnapshot(sha, destParent, env, timeoutMs) {
|
|
274
|
+
const template = env.BOLLOON_DEV_TARBALL_URL
|
|
275
|
+
|| `https://codeload.github.com/${GITHUB_UPSTREAM_SLUG}/tar.gz/{sha}`;
|
|
276
|
+
const url = template.replace('{sha}', sha).replace('{branch}', DEV_BRANCH);
|
|
277
|
+
await fsp.mkdir(destParent, { recursive: true });
|
|
278
|
+
const file = path.join(destParent, `dev-${sha.slice(0, 7)}.tar.gz`);
|
|
279
|
+
try {
|
|
280
|
+
const buf = await new Promise((resolve, reject) => {
|
|
281
|
+
const client = url.startsWith('https') ? https : http;
|
|
282
|
+
const tk = githubToken(env);
|
|
283
|
+
const req = client.get(url, { timeout: timeoutMs, headers: { 'User-Agent': 'bolloon-update', ...(tk ? { Authorization: `Bearer ${tk}` } : {}) } }, (res) => {
|
|
284
|
+
if ((res.statusCode || 0) >= 400) {
|
|
285
|
+
reject(classifyGithubError(null, res.statusCode, res.headers));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const chunks = [];
|
|
289
|
+
res.on('data', (c) => chunks.push(c));
|
|
290
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
291
|
+
});
|
|
292
|
+
req.on('error', reject);
|
|
293
|
+
req.on('timeout', () => { req.destroy(new Error('timeout')); });
|
|
294
|
+
});
|
|
295
|
+
await fsp.writeFile(file, buf);
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
const cls = e?.kind === 'github_unavailable' ? e : classifyGithubError(e);
|
|
299
|
+
return { ok: false, kind: 'github_unavailable', reason: cls.reason, detail: `取 ${url} 失败: ${cls.detail}` };
|
|
300
|
+
}
|
|
301
|
+
const extract = path.join(destParent, 'src');
|
|
302
|
+
await fsp.mkdir(extract, { recursive: true });
|
|
303
|
+
const tar = spawnSync('tar', ['-xzf', file, '-C', extract], { encoding: 'utf-8', timeout: 300_000 });
|
|
304
|
+
if (tar.status !== 0)
|
|
305
|
+
return { ok: false, kind: 'github_unavailable', reason: 'http_error', detail: `解压快照失败: ${String(tar.stderr || '').slice(0, 200)}` };
|
|
306
|
+
const kids = (await fsp.readdir(extract)).filter((f) => !f.startsWith('.'));
|
|
307
|
+
if (kids.length !== 1)
|
|
308
|
+
return { ok: false, kind: 'github_unavailable', reason: 'http_error', detail: `快照解压后目录数不是 1 (${kids.length})` };
|
|
309
|
+
return { ok: true, dir: path.join(extract, kids[0]), source: 'github-tarball' };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 把 master HEAD 的一个 commit 变成**可安装的 dev 快照 tarball**:
|
|
313
|
+
* 取源码 → 校验 sha (本地源码树必须对得上) → 版本改成 `<base>+dev.<sha7>` → 需要时构建 → npm pack
|
|
314
|
+
*
|
|
315
|
+
* 复用原则 (§5 的刻意偏差): **不另造一套替换机制** —— 产物仍是 tarball, 仍交给 npm 替换,
|
|
316
|
+
* 仍然"切换后验证 + 失败回滚"。这里只负责"从哪个源取、装出来是什么身份"。
|
|
317
|
+
*/
|
|
318
|
+
export async function prepareDevSnapshot(opts) {
|
|
319
|
+
const env = opts.env ?? process.env;
|
|
320
|
+
const runNpm = opts.runNpm || defaultRunNpm;
|
|
321
|
+
const stage = (s, d) => { try {
|
|
322
|
+
opts.onStage?.(s, d);
|
|
323
|
+
}
|
|
324
|
+
catch { /* 回调不许影响流程 */ } };
|
|
325
|
+
const sha = String(opts.sha || '').trim();
|
|
326
|
+
if (!/^[0-9a-f]{7,40}$/i.test(sha)) {
|
|
327
|
+
return { ok: false, kind: 'github_unavailable', reason: 'not_found', detail: `master HEAD 的 sha 不合法 (${sha || '空'}) — 拒绝按未知 commit 安装` };
|
|
328
|
+
}
|
|
329
|
+
let dir = '';
|
|
330
|
+
let source = '';
|
|
331
|
+
const envDir = env.BOLLOON_DEV_SOURCE_DIR;
|
|
332
|
+
if (envDir) {
|
|
333
|
+
if (!fs.existsSync(path.join(envDir, 'package.json'))) {
|
|
334
|
+
return { ok: false, kind: 'dev_source_failed', reason: 'source_dir_missing', detail: `BOLLOON_DEV_SOURCE_DIR=${envDir} 下没有 package.json` };
|
|
335
|
+
}
|
|
336
|
+
// 本地源码树必须**真的**是这个 commit —— 否则不许把它当 master HEAD 的快照
|
|
337
|
+
const local = spawnSync('git', ['-C', envDir, 'rev-parse', 'HEAD'], { encoding: 'utf-8', timeout: 10_000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
338
|
+
const localSha = (local.stdout || '').trim();
|
|
339
|
+
if (local.status !== 0 || !localSha) {
|
|
340
|
+
if (env.BOLLOON_DEV_ALLOW_UNVERIFIED_SOURCE !== '1') {
|
|
341
|
+
return { ok: false, kind: 'dev_source_failed', reason: 'sha_unverifiable', detail: `${envDir} 不是 git 检出, 无法核对 sha=${sha.slice(0, 7)} (要强制使用请设 BOLLOON_DEV_ALLOW_UNVERIFIED_SOURCE=1)` };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (!localSha.startsWith(sha.slice(0, 7)) && !sha.startsWith(localSha.slice(0, 7))) {
|
|
345
|
+
return { ok: false, kind: 'dev_source_failed', reason: 'sha_mismatch', detail: `${envDir} 的 HEAD 是 ${localSha.slice(0, 7)}, 与 master HEAD ${sha.slice(0, 7)} 不是同一个 commit — 拒绝用错的源码做 dev 快照` };
|
|
346
|
+
}
|
|
347
|
+
dir = envDir;
|
|
348
|
+
source = `local-tree@${(localSha || sha).slice(0, 7)}`;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
stage('downloading', `取 GitHub master @ ${sha.slice(0, 7)}`);
|
|
352
|
+
const dl = await downloadGithubSnapshot(sha, opts.workDir, env, opts.timeoutMs ?? 120_000);
|
|
353
|
+
if (!dl.ok)
|
|
354
|
+
return dl;
|
|
355
|
+
dir = dl.dir;
|
|
356
|
+
source = dl.source;
|
|
357
|
+
}
|
|
358
|
+
// 身份 = 源码树自己的版本号 + dev.<sha>
|
|
359
|
+
const pkg = readPackageAt(dir);
|
|
360
|
+
if (!pkg?.version)
|
|
361
|
+
return { ok: false, kind: 'dev_source_failed', reason: 'no_version', detail: `${dir}/package.json 读不到版本` };
|
|
362
|
+
const baseVersion = baseVersionOf(String(pkg.version));
|
|
363
|
+
const identity = devIdentity(baseVersion, sha);
|
|
364
|
+
stage('staged', `打上 dev 身份 ${identity}`);
|
|
365
|
+
// 需要时构建 (GitHub tarball 里没有 dist/; 本地树通常已构建)
|
|
366
|
+
let built = false;
|
|
367
|
+
if (!fs.existsSync(path.join(dir, 'dist', 'cli-entry.js'))) {
|
|
368
|
+
stage('staged', '源码树缺少 dist/, 先装依赖再构建 (npm run build:main)');
|
|
369
|
+
if (!fs.existsSync(path.join(dir, 'node_modules'))) {
|
|
370
|
+
/**
|
|
371
|
+
* 验收加速开关: 把已有 node_modules 直接软链进来, 跳过 `npm install` (本仓依赖 889MB)。
|
|
372
|
+
* **只省"装依赖"这一步**, 构建与打包仍然真跑 —— 刻意留给验收脚本用, 别在生产路径上设。
|
|
373
|
+
*/
|
|
374
|
+
const reuse = env.BOLLOON_DEV_REUSE_NODE_MODULES;
|
|
375
|
+
if (reuse && fs.existsSync(path.join(reuse, '.bin'))) {
|
|
376
|
+
stage('staged', `复用现成依赖 (${reuse}) 跳过 npm install`);
|
|
377
|
+
try {
|
|
378
|
+
fs.symlinkSync(reuse, path.join(dir, 'node_modules'), 'dir');
|
|
379
|
+
}
|
|
380
|
+
catch { /* 已存在就跳过 */ }
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
const inst = runNpm(['install', '--no-audit', '--no-fund', '--loglevel=error', '--fetch-retries=5'], dir);
|
|
384
|
+
if (inst.code !== 0) {
|
|
385
|
+
return { ok: false, kind: 'dev_source_failed', reason: 'npm_install_failed', detail: `dev 快照装依赖失败: ${(inst.stderr || inst.stdout).slice(0, 400)}` };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* 从 git 源码树构建要**两步** (真跑踩到的坑):
|
|
391
|
+
* ① `npm run build --workspaces --if-present` —— 先建 workspace 依赖 (@bolloon/constraint-runtime),
|
|
392
|
+
* 否则根包 tsc 会因为 `../constraint-runtime/dist/tools/...` 不存在而直接报 TS2307;
|
|
393
|
+
* ② `npm run build:main` —— 再建根包 (tsc + copy-constraint-runtime)。
|
|
394
|
+
* 原来只跑 ②, 在**干净源码树**上必失败 —— 这个坑只有真从 git 建一次才会暴露。
|
|
395
|
+
*/
|
|
396
|
+
const buildWs = runNpm(['run', 'build', '--workspaces', '--if-present', '--loglevel=error'], dir);
|
|
397
|
+
if (buildWs.code !== 0) {
|
|
398
|
+
return { ok: false, kind: 'dev_source_failed', reason: 'build_failed', detail: `dev 快照构建 workspace 依赖失败: ${(buildWs.stderr || buildWs.stdout).slice(0, 400)}` };
|
|
399
|
+
}
|
|
400
|
+
const build = runNpm(['run', 'build:main'], dir);
|
|
401
|
+
if (build.code !== 0) {
|
|
402
|
+
return { ok: false, kind: 'dev_source_failed', reason: 'build_failed', detail: `dev 快照构建失败: ${(build.stderr || build.stdout).slice(0, 400)}` };
|
|
403
|
+
}
|
|
404
|
+
built = true;
|
|
405
|
+
}
|
|
406
|
+
// 写身份 (改 package.json 的版本身份 + 一个可核对的标记文件)
|
|
407
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
408
|
+
const raw = JSON.parse(await fsp.readFile(pkgPath, 'utf8'));
|
|
409
|
+
raw.version = identity;
|
|
410
|
+
await fsp.writeFile(pkgPath, JSON.stringify(raw, null, 2) + '\n', 'utf8');
|
|
411
|
+
await fsp.writeFile(path.join(dir, '.bolloon-dev-snapshot.json'), JSON.stringify({
|
|
412
|
+
channel: 'dev', ref: `refs/heads/${DEV_BRANCH}`, sha, identity, source, built,
|
|
413
|
+
builtAt: new Date().toISOString(),
|
|
414
|
+
warning: DEV_CHANNEL_WARNING,
|
|
415
|
+
}, null, 2) + '\n', 'utf8');
|
|
416
|
+
// 打包 (产物仍是 tarball → 交给 npm 替换, 不另造一套)
|
|
417
|
+
const pack = runNpm(['pack', dir, '--pack-destination', opts.workDir, '--loglevel=error'], opts.workDir);
|
|
418
|
+
if (pack.code !== 0) {
|
|
419
|
+
return { ok: false, kind: 'dev_source_failed', reason: 'pack_failed', detail: `dev 快照打包失败: ${(pack.stderr || pack.stdout).slice(0, 400)}` };
|
|
420
|
+
}
|
|
421
|
+
const tgzs = (await fsp.readdir(opts.workDir)).filter((f) => f.endsWith('.tgz') && f.includes('bolloon-agent'));
|
|
422
|
+
if (tgzs.length === 0)
|
|
423
|
+
return { ok: false, kind: 'dev_source_failed', reason: 'no_tarball', detail: 'npm pack 没有产出 tarball' };
|
|
424
|
+
const tarball = path.join(opts.workDir, tgzs.sort()[tgzs.length - 1]);
|
|
425
|
+
return { ok: true, dir, tarball, identity, sha, baseVersion, source, built };
|
|
426
|
+
}
|
package/dist/utils/update-cli.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* 退出码与 `bolloon update` 完全一致: 0 正常 / 1 执行失败 / 2 检查不可用。
|
|
16
16
|
*/
|
|
17
17
|
import { resolveBolloonHome } from '../setup/setup-store.js';
|
|
18
|
-
import { checkForUpdate, readUpdateStatus } from './update-manager.js';
|
|
19
|
-
import { renderCheckResult, runDoctorCommand, runUpdateCommand, runVersionCommand } from '../cli/update-commands.js';
|
|
18
|
+
import { checkForUpdate, readUpdateStatus, checkExitCode } from './update-manager.js';
|
|
19
|
+
import { renderCheckResult, renderStatusReport, runDoctorCommand, runUpdateCommand, runVersionCommand, parseChannelArg } from '../cli/update-commands.js';
|
|
20
20
|
import { runDoctor, renderDoctor } from './update-health.js';
|
|
21
21
|
function out(s) { process.stdout.write(s + '\n'); }
|
|
22
22
|
async function main() {
|
|
@@ -25,11 +25,17 @@ async function main() {
|
|
|
25
25
|
const args = argv.slice(1);
|
|
26
26
|
const json = args.includes('json') || args.includes('--json');
|
|
27
27
|
const home = resolveBolloonHome();
|
|
28
|
+
// 双源: `--channel stable|dev` 只覆盖本次进程 (§12.2); 给了不认识的值就直接拒
|
|
29
|
+
const ch = parseChannelArg(args);
|
|
30
|
+
if (ch.error) {
|
|
31
|
+
process.stderr.write(`✗ ${ch.error}\n`);
|
|
32
|
+
return 2;
|
|
33
|
+
}
|
|
28
34
|
switch (cmd) {
|
|
29
35
|
case 'check': {
|
|
30
|
-
const force = args.includes('force') || args.includes('--force') || args.includes('--fresh');
|
|
36
|
+
const force = args.includes('force') || args.includes('--force') || args.includes('--fresh') || !!ch.channel;
|
|
31
37
|
const offline = args.includes('offline') || args.includes('--offline');
|
|
32
|
-
const r = await checkForUpdate({ home, force, offline });
|
|
38
|
+
const r = await checkForUpdate({ home, force, offline, channel: ch.channel });
|
|
33
39
|
if (json)
|
|
34
40
|
out(JSON.stringify(r, null, 2));
|
|
35
41
|
else if (r.status === 'up_to_date' || r.status === 'check_skipped') {
|
|
@@ -40,28 +46,17 @@ async function main() {
|
|
|
40
46
|
else {
|
|
41
47
|
out(renderCheckResult(r));
|
|
42
48
|
}
|
|
43
|
-
return (r.status
|
|
49
|
+
return checkExitCode(r.status);
|
|
44
50
|
}
|
|
45
51
|
case 'status': {
|
|
46
|
-
const s = await readUpdateStatus({ home });
|
|
52
|
+
const s = await readUpdateStatus({ home, channel: ch.channel });
|
|
47
53
|
if (json)
|
|
48
54
|
out(JSON.stringify(s, null, 2));
|
|
49
|
-
else
|
|
50
|
-
|
|
51
|
-
`当前版本: ${s.currentVersion}`,
|
|
52
|
-
`最新版本: ${s.latestVersion || '未知'}`,
|
|
53
|
-
`检查结论: ${s.lastCheckStatus || '尚未检查'}${s.lastCheckReason ? ` (${s.lastCheckReason})` : ''}`,
|
|
54
|
-
`最近检查: ${s.lastCheckAt || '从未'}`,
|
|
55
|
-
`安装方式: ${s.installMethod}`,
|
|
56
|
-
`安装目录: ${s.installDir}`,
|
|
57
|
-
`最近更新: ${s.lastUpdate ? `${s.lastUpdate.at} ${s.lastUpdate.from} → ${s.lastUpdate.to} [${s.lastUpdate.status}]` : '无记录'}`,
|
|
58
|
-
`需要重启: ${s.needsRestart ? '是' : '否'}`,
|
|
59
|
-
];
|
|
60
|
-
out(L.join('\n'));
|
|
61
|
-
}
|
|
55
|
+
else
|
|
56
|
+
out(renderStatusReport(s));
|
|
62
57
|
return 0;
|
|
63
58
|
}
|
|
64
|
-
case 'plan': return runUpdateCommand(['plan', ...(json ? ['json'] : [])]);
|
|
59
|
+
case 'plan': return runUpdateCommand(['plan', ...(ch.channel ? ['--channel', ch.channel] : []), ...(json ? ['json'] : [])]);
|
|
65
60
|
case 'history': return runUpdateCommand(['history', ...args.filter((a) => /^\d+$/.test(a)), ...(json ? ['json'] : [])]);
|
|
66
61
|
case 'version': return runVersionCommand(args);
|
|
67
62
|
case 'doctor': {
|
|
@@ -72,7 +67,7 @@ async function main() {
|
|
|
72
67
|
return rep.grade === 'failed' ? 1 : 0;
|
|
73
68
|
}
|
|
74
69
|
default:
|
|
75
|
-
process.stderr.write(`未知子命令: ${cmd}\n用法: update-cli check|status|plan|history|version|doctor [json]\n`);
|
|
70
|
+
process.stderr.write(`未知子命令: ${cmd}\n用法: update-cli check|status|plan|history|version|doctor [json] [--channel stable|dev]\n`);
|
|
76
71
|
return 64;
|
|
77
72
|
}
|
|
78
73
|
}
|
|
@@ -20,6 +20,8 @@ import { spawnSync } from 'child_process';
|
|
|
20
20
|
import { detectInstallation, readPackageAt, PKG_NAME } from './version-info.js';
|
|
21
21
|
import { readUpdateState, readUpdateLock, lockIsStale, reclaimStaleLockIfAny, IN_FLIGHT_RUN_STATUSES, } from './update-state.js';
|
|
22
22
|
import { queryRegistryDoc, parseJsonFromStdout } from './update-manager.js';
|
|
23
|
+
import { fetchGithubFacts, toGithubReport, renderGithubReportLine } from './dual-source.js';
|
|
24
|
+
import { devShaFromIdentity, channelKindOf, DEV_CHANNEL_WARNING } from './version-info.js';
|
|
23
25
|
import { detectRuntimeReport, verifyRuntimesDeep, evaluateCapabilities, RUNTIME_MIN, readIncompleteMarker, } from './runtime-bootstrap.js';
|
|
24
26
|
function worst(items) {
|
|
25
27
|
if (items.some((i) => i.grade === 'failed'))
|
|
@@ -333,6 +335,25 @@ export async function runDoctor(opts) {
|
|
|
333
335
|
}
|
|
334
336
|
// 6. 上次更新是否异常中断
|
|
335
337
|
const st = await readUpdateState(home);
|
|
338
|
+
// 6.5 双源身份 (§12.5): doctor 也必须能回答"我装的是哪个源 / 哪个 commit / 能切回哪"
|
|
339
|
+
{
|
|
340
|
+
const devSha = devShaFromIdentity(version);
|
|
341
|
+
const prefsChannel = st.channel || 'stable';
|
|
342
|
+
checks.push(devSha
|
|
343
|
+
? {
|
|
344
|
+
id: 'update_source', label: '安装来源 (双源)', grade: 'degraded',
|
|
345
|
+
detail: `dev 快照: GitHub master ${st.devRef || 'refs/heads/master'} @ commit ${devSha} (比较语义 ${channelKindOf('dev')}, 非 semver)`
|
|
346
|
+
+ `${st.switchableTo ? ` · 可切回 stable (${st.switchableTo.source}${st.switchableTo.target ? ` @ ${st.switchableTo.target}` : ''})` : ''}`
|
|
347
|
+
+ ` · ${DEV_CHANNEL_WARNING}`,
|
|
348
|
+
action: 'bolloon update now --channel stable',
|
|
349
|
+
}
|
|
350
|
+
: {
|
|
351
|
+
id: 'update_source', label: '安装来源 (双源)', grade: 'ok',
|
|
352
|
+
detail: `stable: npm registry 权威 (比较语义 ${channelKindOf('stable')})`
|
|
353
|
+
+ `${st.devSha ? ` · 上次用过 dev 快照 commit ${st.devSha}${st.devCheckedAt ? ` (${st.devCheckedAt})` : ''}, 已切回` : ''}`
|
|
354
|
+
+ ` · 默认通道 ${prefsChannel} · 切 dev 用: bolloon update now --channel dev`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
336
357
|
const inFlight = st.lastUpdate && IN_FLIGHT_RUN_STATUSES.includes(st.lastUpdate.status);
|
|
337
358
|
checks.push(inFlight
|
|
338
359
|
? { id: 'last_update', label: '上次更新', grade: 'degraded', detail: `上次更新停在 ${st.lastUpdate.status} (${st.lastUpdate.at}) — 异常中断, 建议 bolloon update --plan 后再更新一次`, action: 'bolloon update --plan' }
|
|
@@ -342,15 +363,24 @@ export async function runDoctor(opts) {
|
|
|
342
363
|
if (st.needsRestart) {
|
|
343
364
|
checks.push({ id: 'needs_restart', label: '待重启', grade: 'degraded', detail: '新版本已就位但还没重启, 当前进程仍在跑旧代码', action: '重启 bolloon' });
|
|
344
365
|
}
|
|
345
|
-
// 7. 版本检查源是否可达 (可跳过)
|
|
366
|
+
// 7. 版本检查源是否可达 (可跳过) —— 双源: npm 是权威, GitHub 是交叉校验/dev 的源
|
|
346
367
|
if (opts.skipNetwork) {
|
|
347
368
|
checks.push({ id: 'version_source', label: '版本源可达', grade: 'ok', detail: '已跳过 (离线)' });
|
|
348
369
|
}
|
|
349
370
|
else {
|
|
350
|
-
const reg = await queryRegistryDoc();
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
371
|
+
const [reg, ghRes] = await Promise.all([queryRegistryDoc(), fetchGithubFacts()]);
|
|
372
|
+
const gh = toGithubReport(ghRes);
|
|
373
|
+
const npmPart = reg.ok
|
|
374
|
+
? `npm registry 可达, latest=${reg.doc.latest}`
|
|
375
|
+
: `npm ${reg.kind}: ${reg.detail}`;
|
|
376
|
+
checks.push({
|
|
377
|
+
id: 'version_source', label: '版本源可达 (双源)',
|
|
378
|
+
// npm 是 stable 的权威 (坏了就是 degraded); GitHub 坏了只影响交叉校验与 dev 通道 → 也只 degraded
|
|
379
|
+
grade: reg.ok ? (gh.reachable ? 'ok' : 'degraded') : 'degraded',
|
|
380
|
+
detail: `${npmPart} · ${renderGithubReportLine(gh)}`,
|
|
381
|
+
action: !reg.ok ? '检查网络/代理后重跑 bolloon doctor'
|
|
382
|
+
: (!gh.reachable ? 'GitHub 交叉校验不可用 (stable 仍以 npm 为权威); dev 通道此时会直接拒绝' : undefined),
|
|
383
|
+
});
|
|
354
384
|
}
|
|
355
385
|
// 7.5 安装完整性标记 (postinstall 发现缺 Git/Python 时留下的)
|
|
356
386
|
const marker = await readIncompleteMarker(home).catch(() => null);
|