@bolloon/bolloon-agent 0.4.33 → 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.
Files changed (143) hide show
  1. package/dist/agents/contacts/chain.js +27 -7
  2. package/dist/agents/execution-supervisor.js +493 -35
  3. package/dist/agents/external-events.js +35 -6
  4. package/dist/agents/gateway-group.js +100 -12
  5. package/dist/agents/goal-criteria.js +10 -3
  6. package/dist/agents/goal-flywheel/block-executor.js +346 -0
  7. package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
  8. package/dist/agents/goal-flywheel/goal-change.js +754 -0
  9. package/dist/agents/goal-flywheel/index.js +26 -0
  10. package/dist/agents/goal-flywheel/memory-layers.js +227 -0
  11. package/dist/agents/goal-flywheel/run-closure.js +809 -0
  12. package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
  13. package/dist/agents/goal-flywheel/types.js +329 -0
  14. package/dist/agents/goal-flywheel/wiring/change.js +288 -0
  15. package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
  16. package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
  17. package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
  18. package/dist/agents/goal-flywheel/wiring/index.js +26 -0
  19. package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
  20. package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
  21. package/dist/agents/goal-flywheel/work-contract.js +478 -0
  22. package/dist/agents/goal-flywheel/work-monitor.js +488 -0
  23. package/dist/agents/goal-flywheel-wiring.js +1870 -0
  24. package/dist/agents/goal-state-reducer.js +406 -0
  25. package/dist/agents/goal-store.js +35 -1
  26. package/dist/agents/mobile-flywheel-view.js +144 -0
  27. package/dist/agents/mobile-task-actions-verify.js +74 -0
  28. package/dist/agents/mobile-task-actions.js +178 -0
  29. package/dist/agents/mobile-task-views.js +199 -0
  30. package/dist/agents/network-pulse.js +304 -18
  31. package/dist/agents/pi-sdk.js +75 -20
  32. package/dist/agents/run-store.js +18 -5
  33. package/dist/agents/skill-readiness.js +52 -10
  34. package/dist/agents/subagent-manager.js +209 -3
  35. package/dist/agents/task/task-runner.js +9 -3
  36. package/dist/agents/task-group.js +8 -51
  37. package/dist/agents/task-public-text.js +89 -0
  38. package/dist/cli/commands/index.js +1 -0
  39. package/dist/cli/commands/tasks.js +350 -2
  40. package/dist/cli/identity-command.js +177 -0
  41. package/dist/cli/setup-wizard.js +71 -0
  42. package/dist/cli/update-commands.js +150 -24
  43. package/dist/cli-entry.js +12 -2
  44. package/dist/electron-build/agents/contacts/chain.js +26 -6
  45. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  46. package/dist/electron-build/agents/execution-supervisor.js +492 -34
  47. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  48. package/dist/electron-build/agents/external-events.js +34 -5
  49. package/dist/electron-build/agents/external-events.js.map +1 -1
  50. package/dist/electron-build/agents/goal-criteria.js +10 -3
  51. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  52. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  53. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  54. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  55. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  56. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  57. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  58. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  59. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  60. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  61. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  62. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  63. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  64. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  65. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  66. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  67. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  68. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  69. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  70. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  71. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  72. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  73. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  74. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  75. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  76. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  78. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  79. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  80. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  81. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  82. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  83. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  84. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  85. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  86. package/dist/electron-build/agents/goal-store.js +35 -1
  87. package/dist/electron-build/agents/goal-store.js.map +1 -1
  88. package/dist/electron-build/agents/network-pulse.js +307 -19
  89. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  90. package/dist/electron-build/agents/pi-sdk.js +74 -19
  91. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  92. package/dist/electron-build/agents/run-store.js +20 -5
  93. package/dist/electron-build/agents/run-store.js.map +1 -1
  94. package/dist/electron-build/agents/skill-readiness.js +52 -9
  95. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  96. package/dist/electron-build/agents/task/task-runner.js +8 -2
  97. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  98. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  99. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  100. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  101. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  102. package/dist/electron-build/utils/dual-source.js +471 -0
  103. package/dist/electron-build/utils/dual-source.js.map +1 -0
  104. package/dist/electron-build/utils/update-manager.js +331 -57
  105. package/dist/electron-build/utils/update-manager.js.map +1 -1
  106. package/dist/electron-build/utils/update-state.js +16 -1
  107. package/dist/electron-build/utils/update-state.js.map +1 -1
  108. package/dist/electron-build/utils/version-info.js +82 -2
  109. package/dist/electron-build/utils/version-info.js.map +1 -1
  110. package/dist/index.js +103 -12
  111. package/dist/ios/chain-explorer.js +617 -0
  112. package/dist/ios/explorer.html +163 -0
  113. package/dist/ios/index.html +96 -0
  114. package/dist/ios/mobile-agent.js +2 -2
  115. package/dist/ios/mobile-contacts.js +329 -0
  116. package/dist/ios/mobile-core.js +1379 -226
  117. package/dist/ios/mobile-privacy.js +185 -0
  118. package/dist/ios/mobile.css +19 -0
  119. package/dist/ios/mobile.html +96 -0
  120. package/dist/ios/mobile.js +706 -7
  121. package/dist/ios/routes-chain-index.js +108 -0
  122. package/dist/ios/routes-contacts.js +420 -0
  123. package/dist/ios/routes-onchain-trade.js +99 -0
  124. package/dist/ios/routes-x402-info.js +1 -0
  125. package/dist/ios/server.js +762 -0
  126. package/dist/ios/task-frame-server.js +211 -0
  127. package/dist/orbitdb/cid-database.js +81 -16
  128. package/dist/orbitdb/ipfs-node.js +31 -2
  129. package/dist/utils/dual-source.js +426 -0
  130. package/dist/utils/update-cli.js +16 -21
  131. package/dist/utils/update-health.js +35 -5
  132. package/dist/utils/update-manager.js +329 -57
  133. package/dist/utils/update-state.js +16 -1
  134. package/dist/utils/version-info.js +75 -1
  135. package/dist/web/mobile-contacts.js +44 -11
  136. package/dist/web/mobile-core.js +1197 -513
  137. package/dist/web/mobile-tasks.js +339 -0
  138. package/dist/web/mobile.css +19 -0
  139. package/dist/web/mobile.html +64 -0
  140. package/dist/web/mobile.js +531 -1
  141. package/dist/web/routes-mobile-tasks.js +353 -0
  142. package/dist/web/server.js +163 -4
  143. package/package.json +3 -1
@@ -52,13 +52,14 @@ var __importStar = (this && this.__importStar) || (function () {
52
52
  };
53
53
  })();
54
54
  Object.defineProperty(exports, "__esModule", { value: true });
55
- exports.NPM_FETCH_RETRY_FLAGS = exports.IN_FLIGHT_STAGES = exports.MIN_FREE_BYTES = void 0;
55
+ exports.NPM_FETCH_RETRY_FLAGS = exports.IN_FLIGHT_STAGES = exports.MIN_FREE_BYTES = exports.REFUSED_STATUSES = exports.DEV_BACK_TO_STABLE_HINT = exports.DEV_CHANNEL_WARNING = void 0;
56
56
  exports.parseJsonFromStdout = parseJsonFromStdout;
57
57
  exports.parseVersion = parseVersion;
58
58
  exports.compareVersions = compareVersions;
59
59
  exports.isKnownVersion = isKnownVersion;
60
60
  exports.classifyRegistryError = classifyRegistryError;
61
61
  exports.queryRegistryDoc = queryRegistryDoc;
62
+ exports.checkExitCode = checkExitCode;
62
63
  exports.checkForUpdate = checkForUpdate;
63
64
  exports.freeBytesAt = freeBytesAt;
64
65
  exports.buildUpdatePlan = buildUpdatePlan;
@@ -78,6 +79,9 @@ const https = __importStar(require("https"));
78
79
  const http = __importStar(require("http"));
79
80
  const child_process_1 = require("child_process");
80
81
  const version_info_js_1 = require("./version-info.js");
82
+ Object.defineProperty(exports, "DEV_CHANNEL_WARNING", { enumerable: true, get: function () { return version_info_js_1.DEV_CHANNEL_WARNING; } });
83
+ Object.defineProperty(exports, "DEV_BACK_TO_STABLE_HINT", { enumerable: true, get: function () { return version_info_js_1.DEV_BACK_TO_STABLE_HINT; } });
84
+ const dual_source_js_1 = require("./dual-source.js");
81
85
  const update_state_js_1 = require("./update-state.js");
82
86
  // ── 版本比较 (唯一一份) ─────────────────────────────────────────────────────
83
87
  /**
@@ -186,31 +190,66 @@ async function queryRegistryDoc(pkg = version_info_js_1.PKG_NAME, timeoutMs = 10
186
190
  return { ok: false, ...classifyRegistryError(e) };
187
191
  }
188
192
  }
193
+ /**
194
+ * 检查结论 → 退出码 (稳定约定, §2): 0 正常 / 2 检查不可用。
195
+ *
196
+ * §12.5 的核心就是这一句: **源不可达 / 版本不存在 → 拒绝并说清 (退出码 2)**,
197
+ * 不许静默装回旧版, 也不许打印"✅ 已是最新"。
198
+ */
199
+ function checkExitCode(status) {
200
+ switch (status) {
201
+ case 'offline':
202
+ case 'registry_unavailable':
203
+ case 'local_version_unknown':
204
+ case 'github_unavailable':
205
+ case 'cross_check_mismatch':
206
+ return 2;
207
+ default:
208
+ return 0;
209
+ }
210
+ }
211
+ /**
212
+ * "源不可达 / 版本不存在" 这一组结论 —— 它们必须**拒绝执行并说清**。
213
+ * 这是 §12.5 那一条的代码落点: 不许静默装回旧版, 也不许打印"已是最新"。
214
+ */
215
+ exports.REFUSED_STATUSES = ['offline', 'registry_unavailable', 'local_version_unknown', 'github_unavailable', 'cross_check_mismatch'];
189
216
  /**
190
217
  * 检查更新。**这是唯一的检查入口** —— CLI / 启动后台 / Python 检查器 / 安装脚本都走它。
191
218
  *
192
- * 结论优先级 (刻意定死, 防"模糊行为"):
219
+ * 结论优先级 (刻意定死, 防"模糊行为"; §12.3 在 stable 序列里插入两个双源结论):
193
220
  * 1. 读不到本地版本 → local_version_unknown (绝不默认 0.0.0 后继续)
194
221
  * 2. 安装方式不支持自动更新 → unsupported_installation (仍会带出 latestVersion)
195
222
  * 3. 显式跳过 / 节流 → check_skipped (结论来自缓存)
196
223
  * 4. 网络不可达 → offline (绝不显示"已是最新")
197
- * 5. registry 不可用 / 包不存在 → registry_unavailable
198
- * 6. latest > current → update_available
199
- * 7. 否则 → up_to_date
224
+ * 5. GitHub 不可达 (**dev 通道**) → github_unavailable (dev 只有这一个源, 拒绝)
225
+ * 6. registry 不可用 / 包不存在 → registry_unavailable
226
+ * 7. 两个源指向不同版本 → cross_check_mismatch (stable, 拒绝)
227
+ * 8. 有新版 (semver / dev 比 sha) → update_available
228
+ * 9. 否则 → up_to_date
229
+ *
230
+ * 两套比较语义**显式分开** (§12.1): stable 比 semver; dev 比 commit sha (不碰 semver)。
200
231
  */
201
232
  async function checkForUpdate(opts = {}) {
202
233
  const home = opts.home ?? (await Promise.resolve().then(() => __importStar(require('../setup/setup-store.js')))).resolveBolloonHome();
203
234
  const prefs = await (0, update_state_js_1.readUpdatePrefs)({ home, env: opts.env });
204
235
  const channel = (0, version_info_js_1.resolveUpdateChannel)(opts.channel || prefs.channel);
236
+ const channelKind = (0, version_info_js_1.channelKindOf)(channel);
205
237
  const install = opts.installation || (0, version_info_js_1.detectInstallation)({ home });
206
238
  const pkg = (0, version_info_js_1.readPackageAt)(install.packageRoot);
207
239
  const current = isKnownVersion(pkg?.version) ? String(pkg?.version) : 'unknown';
240
+ const currentBase = (0, version_info_js_1.baseVersionOf)(current);
241
+ /** 磁盘身份里就是 dev 快照的 sha —— 不从状态文件猜 */
242
+ const installedDevSha = (0, version_info_js_1.devShaFromIdentity)(current);
243
+ const installedChannel = installedDevSha ? 'dev' : 'stable';
208
244
  const now = new Date().toISOString();
209
245
  const state = await (0, update_state_js_1.readUpdateState)(home);
210
246
  const persist = opts.persist !== false;
247
+ /** 注入 registry = 受控检查 (GitHub 事实同样只认注入, 不打真网) */
248
+ const controlled = !!opts.registry;
211
249
  const base = {
212
250
  currentVersion: current,
213
251
  channel,
252
+ channelKind,
214
253
  source: install.updateSource,
215
254
  installMethod: install.method,
216
255
  checkedAt: now,
@@ -231,13 +270,22 @@ async function checkForUpdate(opts = {}) {
231
270
  lastCheckAt: r.checkedAt,
232
271
  lastCheckStatus: r.status,
233
272
  lastCheckReason: r.reason,
273
+ // 双源 (§12): 谁装的 / 哪个 sha / 能切回哪 / 两个源各给了什么
274
+ installedChannel: r.installedChannel ?? installedChannel,
275
+ installedDevSha: r.installedDevSha ?? null,
276
+ sourceFacts: r.sourceFacts ?? null,
277
+ switchableTo: r.switchableTo ?? null,
234
278
  }, home);
235
279
  }
236
280
  return r;
237
281
  };
238
282
  // 1. 本地版本读不到 —— 不猜
239
283
  if (!isKnownVersion(current)) {
240
- return commit({ ...base, status: 'local_version_unknown', latestVersion: null, reason: `读不到 ${install.packageRoot}/package.json 的版本` });
284
+ return commit({
285
+ ...base, status: 'local_version_unknown', latestVersion: null,
286
+ installedChannel: null, installedDevSha: null,
287
+ reason: `读不到 ${install.packageRoot}/package.json 的版本`,
288
+ });
241
289
  }
242
290
  // 3. 显式跳过 / 节流
243
291
  const throttled = !opts.force && state.lastCheckAt
@@ -253,6 +301,9 @@ async function checkForUpdate(opts = {}) {
253
301
  status: 'check_skipped',
254
302
  cachedStatus: cached,
255
303
  latestVersion: state.latestVersion,
304
+ installedChannel, installedDevSha,
305
+ switchableTo: state.switchableTo ?? null,
306
+ sourceFacts: state.sourceFacts ?? null,
256
307
  reason: opts.offline
257
308
  ? `离线模式: 使用缓存结论 (${cached})`
258
309
  : `距上次检查不足 ${prefs.checkIntervalHours}h, 使用缓存结论 (${cached})`,
@@ -262,38 +313,124 @@ async function checkForUpdate(opts = {}) {
262
313
  await (0, update_state_js_1.writeUpdateState)({ lastCheckReason: r.reason }, home);
263
314
  return r;
264
315
  }
265
- // 2. 安装方式不支持自动更新 —— 仍把 latest / 可回滚性带出来, 但结论就是"不支持"
266
- const registry = opts.registry || await queryRegistryDoc(version_info_js_1.PKG_NAME);
316
+ // 两个源并行查 (受控检查里 GitHub 事实只认注入)
317
+ const [registry, githubRes] = await Promise.all([
318
+ opts.registry ? Promise.resolve(opts.registry) : queryRegistryDoc(version_info_js_1.PKG_NAME),
319
+ opts.github
320
+ ? Promise.resolve(opts.github)
321
+ : (controlled || opts.offline ? Promise.resolve(null) : (0, dual_source_js_1.fetchGithubFacts)()),
322
+ ]);
267
323
  const latest = registry.ok ? (registry.doc.distTags[(0, version_info_js_1.distTagForChannel)(channel)] || registry.doc.latest) : null;
268
- const targetPublishedOf = (v) => v && registry.ok ? registry.doc.versions.includes(v) : undefined;
269
- const rollbackSupportedOf = registry.ok ? registry.doc.versions.includes(current) : undefined;
324
+ const githubReport = githubRes ? (0, dual_source_js_1.toGithubReport)(githubRes) : null;
325
+ const sourceFacts = {
326
+ npm: registry.ok
327
+ ? { reachable: true, latest }
328
+ : { reachable: false, latest: null, detail: registry.detail },
329
+ github: githubReport
330
+ ? {
331
+ reachable: githubReport.reachable,
332
+ reason: githubReport.reason, detail: githubReport.detail, retryAt: githubReport.retryAt ?? null,
333
+ ref: githubReport.ref, headSha: githubReport.headSha, newestVersion: githubReport.newestVersion,
334
+ releases: githubReport.releases, tags: githubReport.tags,
335
+ }
336
+ : null,
337
+ crossCheck: null,
338
+ };
339
+ const targetPublishedOf = (v) => v && registry.ok ? registry.doc.versions.includes((0, version_info_js_1.baseVersionOf)(v)) : undefined;
340
+ const rollbackSupportedOf = registry.ok ? registry.doc.versions.includes(currentBase) : undefined;
341
+ // 2. 安装方式不支持自动更新 —— 仍把 latest / 可回滚性带出来, 但结论就是"不支持"
270
342
  if (install.method === 'development' || install.method === 'unknown' || install.method === 'release-binary') {
271
343
  return commit({
272
344
  ...base, status: 'unsupported_installation', latestVersion: latest,
345
+ installedChannel, installedDevSha, github: githubReport, sourceFacts,
346
+ switchableTo: state.switchableTo ?? null,
273
347
  reason: `${install.reason}; 这种安装方式不支持自动更新`,
274
348
  targetPublished: targetPublishedOf(latest),
275
349
  rollbackSupported: rollbackSupportedOf,
276
350
  });
277
351
  }
278
- // 4/5. registry 拿不到 —— 绝不显示"已是最新"
352
+ // ── dev 通道: git ref + commit sha (不碰 semver) ─────────────────────────
353
+ if (channelKind === 'git-ref') {
354
+ /** dev 的"一键回 stable"目标 = registry 上的 latest (§12.4 硬约束 3) */
355
+ const backToStable = { channel: 'stable', source: 'npm', target: latest };
356
+ const devBase = {
357
+ ...base, installedChannel, installedDevSha, github: githubReport,
358
+ switchableTo: backToStable, sourceFacts,
359
+ };
360
+ // dev 只有 GitHub 一个源 → 不可达就是**直接拒** (没有第二个源可退, §12.3)
361
+ if (!githubReport || !githubReport.reachable) {
362
+ const reason = githubReport?.reason || (opts.offline ? 'offline' : 'http_error');
363
+ const detail = githubReport?.detail
364
+ || (opts.offline ? '离线模式: 未查询 GitHub 源' : '本次未查询 GitHub 源 (受控检查未注入 GitHub 事实)');
365
+ return commit({
366
+ ...devBase, status: 'github_unavailable', latestVersion: null, dev: null, targetIdentity: null,
367
+ reason: `github_unavailable(${reason}): ${detail} — dev 通道只有 GitHub 一个源, 拒绝安装 (不回落到 stable 装一个 npm 版本)`,
368
+ });
369
+ }
370
+ if (!githubReport.headSha) {
371
+ return commit({
372
+ ...devBase, status: 'github_unavailable', latestVersion: null, dev: null, targetIdentity: null,
373
+ reason: `github_unavailable(not_found): GitHub 上没有 refs/heads/master 或读不到 HEAD — 拒绝安装一个身份不明的快照`,
374
+ });
375
+ }
376
+ const dev = (0, dual_source_js_1.compareDevSnapshots)(installedDevSha, githubReport.headSha, githubReport.ref);
377
+ const targetIdentity = (0, version_info_js_1.devIdentity)(currentBase, githubReport.headSha);
378
+ const common = {
379
+ ...devBase, dev, targetIdentity, latestVersion: targetIdentity, targetPublished: true,
380
+ rollbackSupported: rollbackSupportedOf,
381
+ };
382
+ if (dev.same)
383
+ return commit({ ...common, status: 'up_to_date', reason: dev.detail });
384
+ return commit({
385
+ ...common, status: 'update_available',
386
+ reason: `${dev.detail}${registry.ok ? '' : ` · 注意: registry 不可达 (${registry.detail}) — 回 stable 会需要它`}`,
387
+ });
388
+ }
389
+ // ── stable 通道: npm 是权威, GitHub 是交叉校验 (§12.1) ───────────────────
390
+ // 4/5. registry 拿不到 —— 绝不显示"已是最新" (GitHub 的事实一并摆出来, 方便判断是哪一源的问题)
279
391
  if (!registry.ok) {
280
392
  return commit({
281
393
  ...base,
282
394
  status: registry.kind === 'offline' ? 'offline' : 'registry_unavailable',
283
395
  latestVersion: null,
284
- reason: registry.detail,
396
+ installedChannel, installedDevSha, github: githubReport, sourceFacts,
397
+ switchableTo: state.switchableTo ?? null,
398
+ reason: `${registry.detail}${githubReport ? ` · ${(0, dual_source_js_1.renderGithubReportLine)(githubReport)}` : ''}`,
285
399
  });
286
400
  }
401
+ const crossCheck = githubRes && githubRes.ok ? (0, dual_source_js_1.crossCheckStable)(latest, githubRes.facts) : null;
402
+ sourceFacts.crossCheck = crossCheck ? { kind: crossCheck.kind, blocking: crossCheck.blocking, detail: crossCheck.detail } : null;
403
+ const switchableTo = installedDevSha
404
+ ? { channel: 'stable', source: 'npm', target: latest }
405
+ : { channel: 'dev', source: 'github', target: githubReport?.headSha ? githubReport.headSha.slice(0, 7) : null };
406
+ const stableBase = {
407
+ ...base, installedChannel, installedDevSha, github: githubReport, crossCheck, sourceFacts, switchableTo,
408
+ };
287
409
  const targetPublished = !!latest && registry.doc.versions.includes(latest);
288
410
  const rollbackSupported = rollbackSupportedOf === true;
289
- // 6/7. 比较
290
- if (latest && compareVersions(current, latest) < 0) {
411
+ // 7. 两个源指向不同版本 → 拒绝 (不许按 GitHub 的记录去装)
412
+ if (crossCheck?.kind === 'mismatch') {
291
413
  return commit({
292
- ...base, status: 'update_available', latestVersion: latest, targetPublished, rollbackSupported,
293
- reason: `当前 ${current} → 目标 ${latest}`,
414
+ ...stableBase, status: 'cross_check_mismatch', latestVersion: latest,
415
+ targetPublished, rollbackSupported,
416
+ reason: `cross_check_mismatch: ${crossCheck.detail} (npm latest=${latest} / GitHub ${crossCheck.githubVersion || '无记录'})`,
294
417
  });
295
418
  }
296
- return commit({ ...base, status: 'up_to_date', latestVersion: latest, targetPublished, rollbackSupported });
419
+ // 8/9. 比较: 当前是 dev 快照时**显式**判"切回 stable", 而不是拿 semver 硬比
420
+ if ((0, version_info_js_1.isDevIdentity)(current)) {
421
+ return commit({
422
+ ...stableBase, status: 'update_available', latestVersion: latest,
423
+ targetPublished, rollbackSupported,
424
+ reason: `当前装的是 dev 快照 ${current} → 切回 stable 的 ${latest || '未知'} (一键: ${version_info_js_1.DEV_BACK_TO_STABLE_HINT})`,
425
+ });
426
+ }
427
+ if (latest && compareVersions(currentBase, latest) < 0) {
428
+ return commit({
429
+ ...stableBase, status: 'update_available', latestVersion: latest, targetPublished, rollbackSupported,
430
+ reason: `当前 ${current} → 目标 ${latest}${crossCheck ? ` · 交叉校验: ${crossCheck.kind}` : ''}`,
431
+ });
432
+ }
433
+ return commit({ ...stableBase, status: 'up_to_date', latestVersion: latest, targetPublished, rollbackSupported });
297
434
  }
298
435
  const WILL_NOT_TOUCH = [
299
436
  '~/.bolloon/config.json',
@@ -415,6 +552,7 @@ async function buildUpdatePlan(opts = {}) {
415
552
  const home = opts.home ?? (await Promise.resolve().then(() => __importStar(require('../setup/setup-store.js')))).resolveBolloonHome();
416
553
  const install = opts.installation || (0, version_info_js_1.detectInstallation)({ home });
417
554
  const check = await checkForUpdate({ ...opts, home, persist: false, installation: install });
555
+ const isDev = check.channelKind === 'git-ref';
418
556
  const risk = [
419
557
  ...await collectInstallRisk(install),
420
558
  ...(opts.workloadRiskOverride || await collectWorkloadRisk()),
@@ -429,6 +567,33 @@ async function buildUpdatePlan(opts = {}) {
429
567
  : check.status === 'registry_unavailable' ? `registry 不可用: ${check.reason}`
430
568
  : 'npm registry 可达',
431
569
  });
570
+ /**
571
+ * GitHub 侧风险 (§12.3) —— **dev 通道阻塞, stable 通道只提醒**:
572
+ * dev 只有 GitHub 一个源, 不可达就没有"退回哪去"这件事; stable 的权威是 npm, GitHub 只是交叉校验源。
573
+ */
574
+ const gh = check.github;
575
+ risk.push({
576
+ id: 'github_reachable',
577
+ label: 'GitHub 源可达 (dev 通道的源 / stable 的交叉校验源)',
578
+ ok: !gh ? true : gh.reachable,
579
+ blocking: isDev && (!gh || !gh.reachable),
580
+ advisory: !isDev,
581
+ detail: !gh
582
+ ? '未查询 GitHub 源 (离线或受控检查未注入)'
583
+ : gh.reachable
584
+ ? `可达 · master HEAD ${gh.headSha ? gh.headSha.slice(0, 7) : '未知'} · Release ${gh.releases ?? 0} 个 · Tag ${gh.tags ?? 0} 个`
585
+ : `github_unavailable(${gh.reason}): ${gh.detail || ''}${gh.retryAt ? ` (限流重试 ${gh.retryAt})` : ''}`,
586
+ });
587
+ risk.push({
588
+ id: 'cross_check',
589
+ label: '两个源指向同一版本 (npm ↔ GitHub)',
590
+ ok: check.crossCheck ? check.crossCheck.kind !== 'mismatch' : true,
591
+ blocking: !!check.crossCheck?.blocking,
592
+ advisory: check.crossCheck?.kind === 'missing_record' || check.crossCheck?.kind === 'no_record_at_all',
593
+ detail: !check.crossCheck
594
+ ? (isDev ? 'dev 通道不看 npm (GitHub 是唯一源)' : '未做交叉校验 (GitHub 源未查)')
595
+ : check.crossCheck.detail,
596
+ });
432
597
  risk.push({
433
598
  id: 'target_published',
434
599
  label: '目标版本真实存在',
@@ -444,24 +609,36 @@ async function buildUpdatePlan(opts = {}) {
444
609
  ok: check.rollbackSupported !== false,
445
610
  blocking: false,
446
611
  detail: check.rollbackSupported === false
447
- ? `registry 上找不到当前版本 ${check.currentVersion} — 更新失败将无法用 npm 回滚`
448
- : check.rollbackSupported === true ? `registry 上存在 ${check.currentVersion} (可回滚)` : '未检查 (离线)',
612
+ ? `registry 上找不到当前版本 ${(0, version_info_js_1.baseVersionOf)(check.currentVersion)} — 更新失败将无法用 npm 回滚`
613
+ : check.rollbackSupported === true ? `registry 上存在 ${(0, version_info_js_1.baseVersionOf)(check.currentVersion)} (可回滚)` : '未检查 (离线)',
449
614
  });
450
615
  const blockers = risk.filter((r) => !r.ok && r.blocking).map((r) => `${r.label}: ${r.detail}`);
451
616
  const advisories = risk.filter((r) => !r.ok && !r.blocking).map((r) => `${r.label}: ${r.detail}`);
452
- const target = check.latestVersion && compareVersions(check.currentVersion, check.latestVersion) < 0
453
- ? check.latestVersion
617
+ const target = check.status === 'update_available'
618
+ ? (check.targetIdentity || check.latestVersion || null)
454
619
  : null;
620
+ const warnings = [];
621
+ if (isDev) {
622
+ warnings.push(version_info_js_1.DEV_CHANNEL_WARNING);
623
+ warnings.push(version_info_js_1.DEV_BACK_TO_STABLE_HINT);
624
+ }
625
+ if (check.installedDevSha && !isDev)
626
+ warnings.push(`当前装的是 dev 快照 (commit ${check.installedDevSha}), 本次要切回 stable: 会把 GitHub master 快照换成 registry 上的 latest`);
455
627
  return {
456
628
  ok: blockers.length === 0 && !!target,
457
629
  currentVersion: check.currentVersion,
458
630
  targetVersion: target,
459
631
  channel: check.channel,
632
+ channelKind: check.channelKind || (0, version_info_js_1.channelKindOf)(check.channel),
633
+ targetIdentity: check.targetIdentity ?? null,
634
+ warnings,
460
635
  installMethod: check.installMethod,
461
636
  source: check.source,
462
637
  installDir: install.installDir,
463
638
  needsRestart: !!target,
464
- willUpdate: target && install.method === 'npm-global' ? [version_info_js_1.PKG_NAME, version_info_js_1.CONSTRAINT_PKG_NAME] : [],
639
+ willUpdate: target && install.method === 'npm-global'
640
+ ? (isDev ? [`GitHub master 快照 ${check.github?.headSha?.slice(0, 7) || ''} (身份 ${target})`, version_info_js_1.PKG_NAME] : [version_info_js_1.PKG_NAME, version_info_js_1.CONSTRAINT_PKG_NAME])
641
+ : [],
465
642
  willNotTouch: WILL_NOT_TOUCH,
466
643
  risk, blockers, advisories,
467
644
  strategies: ['now', 'wait', 'cancel'],
@@ -471,10 +648,18 @@ async function buildUpdatePlan(opts = {}) {
471
648
  }
472
649
  function renderUpdatePlan(plan) {
473
650
  const L = [];
474
- L.push(`当前版本: ${plan.currentVersion}`);
651
+ L.push(`当前版本: ${plan.currentVersion}${plan.check.installedDevSha ? ` (dev 快照 commit ${plan.check.installedDevSha})` : ''}`);
475
652
  L.push(`目标版本: ${plan.targetVersion || '(无可用更新)'}`);
476
653
  L.push(`安装方式: ${plan.installMethod}`);
477
- L.push(`更新通道: ${plan.channel} (来源: ${plan.source})`);
654
+ L.push(`更新通道: ${plan.channel} (比较语义: ${plan.channelKind})`);
655
+ L.push(`更新来源: ${plan.source}${plan.channelKind === 'git-ref' ? ' · GitHub master (唯一源)' : ' · npm registry (权威) + GitHub (交叉校验)'}`);
656
+ L.push((0, dual_source_js_1.renderGithubReportLine)(plan.check.github));
657
+ if (plan.check.crossCheck)
658
+ L.push(`交叉校验: ${plan.check.crossCheck.kind}${plan.check.crossCheck.blocking ? ' (阻塞)' : ''} — ${plan.check.crossCheck.detail}`);
659
+ if (plan.check.dev)
660
+ L.push(`dev 快照: ${plan.check.dev.detail}`);
661
+ for (const w of plan.warnings)
662
+ L.push(w);
478
663
  L.push(`安装目录: ${plan.installDir}`);
479
664
  L.push('');
480
665
  L.push('将更新:');
@@ -505,7 +690,9 @@ function renderUpdatePlan(plan) {
505
690
  L.push(` 建议策略: 等当前 Run 结束后更新 (bolloon update --now --wait)`);
506
691
  }
507
692
  L.push('');
508
- L.push('可选: 立即更新 (bolloon update --now) / 等当前 Run 结束 (bolloon update --now --wait) / 取消');
693
+ L.push(`可选: 立即更新 (bolloon update --now --channel ${plan.channel}) / 等当前 Run 结束 (bolloon update --now --wait --channel ${plan.channel}) / 取消`);
694
+ if (plan.channelKind === 'git-ref')
695
+ L.push(`一键回稳定版: bolloon update now --channel stable`);
509
696
  L.push(`默认: ${plan.defaultStrategy === 'wait' ? '等待当前 Run 结束后更新' : '立即更新'}`);
510
697
  return L.join('\n');
511
698
  }
@@ -570,21 +757,44 @@ async function applyUpdate(opts = {}) {
570
757
  };
571
758
  const plan = await buildUpdatePlan({
572
759
  home, env: opts.env, force: true, installation: install, registry: opts.registry,
760
+ github: opts.github, channel: opts.channel,
573
761
  workloadRiskOverride: opts.workloadRiskOverride,
574
762
  });
575
763
  const from = plan.currentVersion;
576
764
  const to = plan.targetVersion || plan.currentVersion;
765
+ const isDev = plan.channelKind === 'git-ref';
766
+ /** dev 的目标 commit (身份就是它, 不是版本号) */
767
+ const devSha = plan.check.dev?.headSha || plan.check.github?.headSha || null;
577
768
  const record = async (r, historyStatus) => {
578
769
  const rec = { at: new Date().toISOString(), from: r.from, to: r.to, status: historyStatus, durationMs: r.durationMs, reason: r.reason };
579
770
  // 记账失败**不能**把"更新其实成功了"变成向上抛异常 (真被测试 teardown 竞态抓到过:
580
771
  // HOME 在写到一半时消失 → rename ENOENT)。失败就如实记进 reason, 结果照常返回。
581
772
  try {
582
773
  await (0, update_state_js_1.appendUpdateHistory)(rec, home);
774
+ /**
775
+ * 双源落盘 (§12.4 硬约束 2): 装成 dev 时记下 `{channel, devSha, devRef, checkedAt}`;
776
+ * 切回 stable 时把安装通道改回 stable 并把 dev 记录保留成"上次那个 dev 快照"。
777
+ */
778
+ const dualSource = isDev && r.ok
779
+ ? {
780
+ installedChannel: 'dev',
781
+ installedDevSha: devSha ? devSha.slice(0, 7) : (0, version_info_js_1.devShaFromIdentity)(r.to),
782
+ devSha: devSha || null,
783
+ devRef: `refs/heads/master`,
784
+ devCheckedAt: rec.at,
785
+ switchableTo: { channel: 'stable', source: 'npm', target: (0, version_info_js_1.baseVersionOf)(r.from) },
786
+ }
787
+ : {};
788
+ const backToStable = !isDev && r.ok
789
+ ? { installedChannel: 'stable', installedDevSha: null, switchableTo: { channel: 'dev', source: 'github', target: devSha ? devSha.slice(0, 7) : null } }
790
+ : {};
583
791
  await (0, update_state_js_1.writeUpdateState)({
584
792
  lastUpdate: rec,
585
793
  lastFailure: r.ok ? null : { at: rec.at, stage: r.failedAt || r.stage, reason: r.reason || '未知原因' },
586
794
  needsRestart: r.ok ? r.needsRestart : false,
587
795
  currentVersion: r.from,
796
+ ...dualSource,
797
+ ...backToStable,
588
798
  }, home);
589
799
  }
590
800
  catch (e) {
@@ -594,8 +804,16 @@ async function applyUpdate(opts = {}) {
594
804
  return r;
595
805
  };
596
806
  if (!plan.targetVersion) {
597
- await stage('blocked', '没有可执行的更新');
598
- return record({ stage: 'blocked', ok: false, from, to, durationMs: Date.now() - started, reason: plan.blockers[0] || '已是最新, 无需更新', needsRestart: false }, 'blocked');
807
+ /**
808
+ * §12.5 的核心分流: **源不可达 / 版本不存在 → 拒绝并说清**,
809
+ * 绝不把"没查到"说成"已是最新"。
810
+ */
811
+ const refused = exports.REFUSED_STATUSES.includes(plan.check.status);
812
+ await stage('blocked', refused ? `拒绝更新 (${plan.check.status})` : '没有可执行的更新');
813
+ const reason = refused
814
+ ? `拒绝更新 (${plan.check.status}): ${plan.check.reason || plan.blockers[0] || '源不可达 / 版本不存在'}`
815
+ : (plan.blockers[0] || '已是最新, 无需更新');
816
+ return record({ stage: 'blocked', ok: false, from, to, durationMs: Date.now() - started, reason, needsRestart: false }, 'blocked');
599
817
  }
600
818
  if (plan.blockers.length > 0) {
601
819
  await stage('blocked', plan.blockers.join('; '));
@@ -617,42 +835,79 @@ async function applyUpdate(opts = {}) {
617
835
  const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'bolloon-update-'));
618
836
  const runNpm = opts.runNpm || defaultRunNpm;
619
837
  let stagedTarball = null;
838
+ /** 切换时交给 npm 的安装目标: stable = `<包>@<semver>`; dev = 本地 dev 快照 tarball */
839
+ let switchSpec = `${version_info_js_1.PKG_NAME}@${to}`;
840
+ /**
841
+ * 实际装入的身份。dev 的身份来自**源码树的 package.json + master sha**, 可能与检查阶段的预测
842
+ * 略有差别 (源码树的版本号才是真的) → 以产物为准, 校验/验证/记账都用它, 不用预测值糊过去。
843
+ */
844
+ let effectiveTo = to;
620
845
  try {
621
- // 1. 下载 tarball 到临时目录 (不动现有安装)
622
- await stage('downloading', `npm pack ${version_info_js_1.PKG_NAME}@${to}`);
623
- const packed = runNpm(['pack', `${version_info_js_1.PKG_NAME}@${to}`, '--pack-destination', tmpRoot, '--loglevel=error'], tmpRoot);
624
- if (packed.code !== 0) {
625
- throw Object.assign(new Error(`下载 ${version_info_js_1.PKG_NAME}@${to} 失败: ${(packed.stderr || packed.stdout).trim().slice(0, 400)}`), { stage: 'downloading' });
846
+ // 1. 取产物到临时目录 (不动现有安装)
847
+ if (isDev) {
848
+ // dev: 源是 GitHub master 的 git ref, 身份是 commit sha (§12.1)
849
+ if (!devSha) {
850
+ throw Object.assign(new Error('dev 通道没有拿到 master HEAD 的 commit sha — 拒绝安装一个身份不明的快照'), { stage: 'downloading' });
851
+ }
852
+ await stage('downloading', `取 GitHub master @ ${devSha.slice(0, 7)} (${version_info_js_1.DEV_BACK_TO_STABLE_HINT})`);
853
+ const snap = opts.devSnapshot
854
+ ? { ok: true, tarball: opts.devSnapshot.tarball, identity: opts.devSnapshot.identity, sha: opts.devSnapshot.sha, source: 'injected', built: false, dir: '' }
855
+ : await (0, dual_source_js_1.prepareDevSnapshot)({
856
+ sha: devSha, workDir: tmpRoot, env: opts.env,
857
+ runNpm,
858
+ onStage: (s, d) => { try {
859
+ opts.onStage?.(s, d);
860
+ }
861
+ catch { /* 回调失败不影响流程 */ } },
862
+ });
863
+ if (!snap.ok) {
864
+ const kind = 'kind' in snap ? String(snap.kind) : 'dev_source_failed';
865
+ const reason = 'reason' in snap ? String(snap.reason) : '';
866
+ const detail = 'detail' in snap ? String(snap.detail) : '未知原因';
867
+ throw Object.assign(new Error(`${kind}/${reason}: ${detail}`), { stage: 'downloading' });
868
+ }
869
+ stagedTarball = snap.tarball;
870
+ switchSpec = snap.tarball;
871
+ effectiveTo = snap.identity;
872
+ await stage('staged', `dev 快照身份 ${snap.identity} (来源 ${snap.source}${snap.built ? ', 已现场构建' : ''})`);
626
873
  }
627
- const tarballs = (await fsp.readdir(tmpRoot)).filter((f) => f.endsWith('.tgz'));
628
- if (tarballs.length === 0)
629
- throw Object.assign(new Error('npm pack 没有产出 tarball'), { stage: 'downloading' });
630
- stagedTarball = path.join(tmpRoot, tarballs[0]);
631
- // 2. 解压 + 校验包内容 (版本 + 入口)
632
- await stage('staged', `解压并校验 ${path.basename(stagedTarball)}`);
874
+ else {
875
+ await stage('downloading', `npm pack ${version_info_js_1.PKG_NAME}@${to}`);
876
+ const packed = runNpm(['pack', `${version_info_js_1.PKG_NAME}@${to}`, '--pack-destination', tmpRoot, '--loglevel=error'], tmpRoot);
877
+ if (packed.code !== 0) {
878
+ throw Object.assign(new Error(`下载 ${version_info_js_1.PKG_NAME}@${to} 失败: ${(packed.stderr || packed.stdout).trim().slice(0, 400)}`), { stage: 'downloading' });
879
+ }
880
+ const tarballs = (await fsp.readdir(tmpRoot)).filter((f) => f.endsWith('.tgz'));
881
+ if (tarballs.length === 0)
882
+ throw Object.assign(new Error('npm pack 没有产出 tarball'), { stage: 'downloading' });
883
+ stagedTarball = path.join(tmpRoot, tarballs[0]);
884
+ }
885
+ // 2. 解压 + 校验包内容 (版本 + 入口) —— 两个源共用同一条校验
886
+ if (!isDev)
887
+ await stage('staged', `解压并校验 ${path.basename(stagedTarball)}`);
633
888
  const extractDir = path.join(tmpRoot, 'extract');
634
889
  await fsp.mkdir(extractDir, { recursive: true });
635
890
  const tar = (0, child_process_1.spawnSync)('tar', ['-xzf', stagedTarball, '-C', extractDir], { encoding: 'utf-8', timeout: 120_000 });
636
891
  if (tar.status !== 0)
637
892
  throw Object.assign(new Error(`解压失败: ${(tar.stderr || '').slice(0, 300)}`), { stage: 'staged' });
638
- const stagedCheck = validateStagedPackage(path.join(extractDir, 'package'), to);
893
+ const stagedCheck = validateStagedPackage(path.join(extractDir, 'package'), effectiveTo);
639
894
  if (!stagedCheck.ok)
640
895
  throw Object.assign(new Error(`目标包校验失败: ${stagedCheck.detail}`), { stage: 'staged' });
641
- // 3. 切换 (npm 完成替换; 旧版本在替换成功前不会消失)
642
- await stage('switching', `npm install -g ${version_info_js_1.PKG_NAME}@${to}`);
643
- const installed = runNpm(['install', '-g', `${version_info_js_1.PKG_NAME}@${to}`, '--no-fund', '--no-audit', '--loglevel=error'], os.tmpdir());
896
+ // 3. 切换 (npm 完成替换; 旧版本在替换成功前不会消失) —— 两个源共用同一条替换路径
897
+ await stage('switching', isDev ? `npm install -g <dev 快照 ${path.basename(switchSpec)}>` : `npm install -g ${version_info_js_1.PKG_NAME}@${to}`);
898
+ const installed = runNpm(['install', '-g', switchSpec, '--no-fund', '--no-audit', '--loglevel=error'], os.tmpdir());
644
899
  if (installed.code !== 0) {
645
900
  throw Object.assign(new Error(`npm 安装失败: ${(installed.stderr || installed.stdout).trim().slice(0, 400)}`), { stage: 'switching' });
646
901
  }
647
902
  // 4. 验证: 磁盘版本 + 新入口真能启动 + 健康检查
648
903
  await stage('verifying', '核对磁盘版本并试启新入口');
649
904
  const verify = opts.verifyInstall || defaultVerifyInstall;
650
- const ok = await verify(to, home);
905
+ const ok = await verify(effectiveTo, home);
651
906
  if (!ok)
652
- throw Object.assign(new Error(`切换后验证失败: 新版本 ${to} 未能正确启动`), { stage: 'verifying' });
653
- await stage('succeeded', `已更新到 ${to}`);
907
+ throw Object.assign(new Error(`切换后验证失败: 新版本 ${effectiveTo} 未能正确启动`), { stage: 'verifying' });
908
+ await stage('succeeded', `已更新到 ${effectiveTo}`);
654
909
  const out = {
655
- stage: 'succeeded', ok: true, from, to, durationMs: Date.now() - started, needsRestart: true, stagedTarball,
910
+ stage: 'succeeded', ok: true, from, to: effectiveTo, durationMs: Date.now() - started, needsRestart: true, stagedTarball,
656
911
  };
657
912
  await record(out, 'succeeded');
658
913
  return out;
@@ -662,25 +917,26 @@ async function applyUpdate(opts = {}) {
662
917
  const reason = e?.message || String(e);
663
918
  await stage(failureStage, reason);
664
919
  // 失败处理: 保留旧版本 → 能回滚就回滚 → 写原因 → 释放锁
920
+ const fromBase = (0, version_info_js_1.baseVersionOf)(from);
665
921
  let finalStage = 'failed';
666
922
  let finalReason = reason;
667
923
  try {
668
924
  const nowVersion = installedVersionOnDisk(install.packageRoot);
669
- if (install.method === 'npm-global' && nowVersion && nowVersion !== from && nowVersion !== to) {
670
- await stage('rolled_back', `检测到半更新 (磁盘版本 ${nowVersion}), 回滚到 ${from}`);
671
- const rb = runNpm(['install', '-g', `${version_info_js_1.PKG_NAME}@${from}`, '--no-fund', '--no-audit', '--loglevel=error'], os.tmpdir());
925
+ if (install.method === 'npm-global' && nowVersion && nowVersion !== from && nowVersion !== effectiveTo) {
926
+ await stage('rolled_back', `检测到半更新 (磁盘版本 ${nowVersion}), 回滚到 ${fromBase}`);
927
+ const rb = runNpm(['install', '-g', `${version_info_js_1.PKG_NAME}@${fromBase}`, '--no-fund', '--no-audit', '--loglevel=error'], os.tmpdir());
672
928
  const after = installedVersionOnDisk(install.packageRoot);
673
- if (rb.code === 0 && after === from) {
929
+ if (rb.code === 0 && after === fromBase) {
674
930
  finalStage = 'rolled_back';
675
- finalReason = `${reason}; 已回滚到 ${from}`;
931
+ finalReason = `${reason}; 已回滚到 ${fromBase}`;
676
932
  }
677
933
  else {
678
- finalReason = `${reason}; 回滚也未成功 (磁盘版本 ${after || 'unknown'}) — 请手动: npm install -g ${version_info_js_1.PKG_NAME}@${from}`;
934
+ finalReason = `${reason}; 回滚也未成功 (磁盘版本 ${after || 'unknown'}) — 请手动: npm install -g ${version_info_js_1.PKG_NAME}@${fromBase}`;
679
935
  }
680
936
  }
681
- else if (nowVersion === to && failureStage === 'verifying') {
937
+ else if (nowVersion === effectiveTo && failureStage === 'verifying') {
682
938
  // 装上了但验证没过: 保留新版本但如实报失败, 并给出回滚指令
683
- finalReason = `${reason}; 磁盘版本已是 ${to}, 如需回退: npm install -g ${version_info_js_1.PKG_NAME}@${from}`;
939
+ finalReason = `${reason}; 磁盘版本已是 ${effectiveTo}, 如需回退: npm install -g ${version_info_js_1.PKG_NAME}@${fromBase}`;
684
940
  }
685
941
  }
686
942
  catch (e2) {
@@ -721,12 +977,26 @@ async function readUpdateStatus(opts = {}) {
721
977
  const st = await (0, update_state_js_1.readUpdateState)(home);
722
978
  const prefs = await (0, update_state_js_1.readUpdatePrefs)({ home, env: opts.env });
723
979
  const lock = (0, update_state_js_1.readUpdateLock)(home);
724
- const install = (0, version_info_js_1.detectInstallation)({ home });
980
+ const install = opts.installation || (0, version_info_js_1.detectInstallation)({ home });
725
981
  const pkg = (0, version_info_js_1.readPackageAt)(install.packageRoot);
982
+ const currentVersion = isKnownVersion(pkg?.version) ? String(pkg?.version) : st.currentVersion;
983
+ const installedDevSha = (0, version_info_js_1.devShaFromIdentity)(currentVersion);
984
+ const channel = (0, version_info_js_1.resolveUpdateChannel)(opts.channel || prefs.channel);
726
985
  return {
727
- currentVersion: isKnownVersion(pkg?.version) ? String(pkg?.version) : st.currentVersion,
986
+ currentVersion,
987
+ // 安装通道从**磁盘身份**判定 (不从状态文件猜); 状态里的 dev 记录另外带出来做历史
988
+ installedChannel: installedDevSha ? 'dev' : 'stable',
989
+ installedDevSha,
990
+ switchableTo: st.switchableTo ?? (installedDevSha
991
+ ? { channel: 'stable', source: 'npm', target: st.latestVersion }
992
+ : { channel: 'dev', source: 'github', target: null }),
993
+ lastDevSha: st.devSha ?? null,
994
+ devRef: st.devRef ?? null,
995
+ devCheckedAt: st.devCheckedAt ?? null,
996
+ sourceFacts: st.sourceFacts ?? null,
728
997
  latestVersion: st.latestVersion,
729
- channel: prefs.channel,
998
+ channel,
999
+ channelKind: (0, version_info_js_1.channelKindOf)(channel),
730
1000
  installMethod: install.method,
731
1001
  installDir: install.installDir,
732
1002
  entryPath: install.entryPath,
@@ -765,6 +1035,10 @@ async function updateSummaryFor(home) {
765
1035
  latestVersion: st.latestVersion,
766
1036
  lastUpdate: st.lastUpdate ? { at: st.lastUpdate.at, from: st.lastUpdate.from, to: st.lastUpdate.to, status: st.lastUpdate.status, reason: st.lastUpdate.reason } : null,
767
1037
  needsRestart: st.needsRestart,
1038
+ // 双源 (§12.5): "我装的是哪个源 / 哪个 commit / 能切回哪" 从同一份状态读
1039
+ installedChannel: st.installedChannel ?? null,
1040
+ installedDevSha: st.installedChannel === 'dev' ? (st.installedDevSha ?? null) : null,
1041
+ switchableTo: st.switchableTo ?? null,
768
1042
  };
769
1043
  }
770
1044
  //# sourceMappingURL=update-manager.js.map