dsh-m 0.9.93 → 0.10.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/CHANGELOG.md +24 -0
- package/lib/client.js +136 -84
- package/lib/core/active-profile.js +1 -1
- package/lib/core/host-api.js +69 -16
- package/lib/core/packument.js +11 -0
- package/lib/core/profile-ops.js +216 -6
- package/lib/core/profile-transaction.js +31 -2
- package/lib/core/upgrade-intent.js +108 -0
- package/lib/core/versions.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,18 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
8
8
|
|
|
9
9
|
## 中文
|
|
10
10
|
|
|
11
|
+
### 0.10.0 特性+修复:宿主 0.2.1 already-installed 适配——第三方两步升级 + 兜底回装降级恢复 + 自升级专用通道(D3)
|
|
12
|
+
|
|
13
|
+
- **需求(2026-10-17,主人)**:新版 DSH(0.2.1-rc.1/rc.2)中 dsh-m 无法升级与自升级任何包。根因定谳:宿主 `installBundle()` 新增 already-installed 安装守卫——spec 解析出的包名已在 profile manifest 就先于 pnpm 拒绝,官方升级语义改为「先卸载再安装」;tarball/git 形态 spec 包名在 pnpm 前不可知、不受守卫约束(真机 spike 实证:URL 覆盖安装已装 dsh-m 成功、激活形态 restart-required)。影响面:产品内 desktop 升级/自升级全断;卸载、开关、全新安装、web 路径无恙。跳过 0.9.95+ 直接发 0.10.0。
|
|
14
|
+
- **方案(ADR-0022,grill 决议 R1–R15)**:① **install-first 自适应两步**——不探测宿主版本,首腿收到 `already-installed` 错误码才转 remove+重试(至多一次、无循环);install 路径撞守卫同样静默两步化(已装态 install 即升级),全新安装零行为变化(removeBundle 恒零调用);release-age/build-blocked/cancelled 语义先于守卫判定。② **兜底回装**——两步化后安装腿终态失败自动回装旧版:回装成功 = **降级恢复**新结局(成功形态 + degraded/restoredTo;客户端 warn 投影不入「升级成功」臂、操作泵复用 warned 状态落账、设置页红点沿用既有 failed||warned 口径、× 删除照旧);回装再失败 = rollback-failed 缺席硬错误(回装腿自身 build-blocked 时 details.pendingBuilds 如实透出);cancelled 抛「已恢复旧版/未做升级」不冒充降级,build-blocked 回装兜住后仍抛审批请求。③ **通道分治**——第三方走官方两步;dsh-m 自升级独享 tarball 直覆盖专用通道(精确版本 → 生效 registry `dist.tarball` → intent 预写 → `installBundle(tarballUrl)` 直覆盖 → 就地收口),core 缝单点改道(SELF_PKG='dsh-m'),host-api/Agent 工具/客户端零特判,自升级绝不落入 remove+add;入口全面保留:头部角标新增确认框(不经过卸载、失败时当前版本不受影响、完成后需重启 Desktop 生效),needsRestart 忠实透传宿主 application(applied→无横幅、restart-required→提示重启)。④ **开机对账**——自升级 intent ledger(`$DSH_HOME/dshm/self-upgrade-intent.json`;ADR-0018 面板通道零外部日志不变量不破)作崩溃兜底网:正常路径就地收口,进程死于升级途中时由下一次 self-check 每进程一次比对实际版本收口。⑤ WRITE_GUIDANCE 自升级失配文案修复(不再让用户「回 DSH Web」,改指官方 Settings → Plugins remove + install 精确版本)。GLOSSARY 七词条入册(两步升级/宿主安装守卫/install-first 自适应/兜底回装/降级恢复/自升级专用通道/开机对账)。
|
|
15
|
+
- **验证**:typecheck 零错误;全量 1585 测试(028 本机环境既有失败 18 例零新增——干净树对照为同一集合:symlink EPERM 家族 + client-lightbox ESM scheme + CRLF 源锚 + 网络 fetch);新增钉例覆盖计划 T1–T17(intent ledger 存储五态 / npmTarballUrl 三钉 / 两步化-兜底-降级边界九钉(含 remove-failed 短路、cancelled 文案、回装 build-blocked 透 pendingBuilds)/ D3 与 SELF_PKG 缝改道五钉 / host-api 开机对账与 upgrade(dsh-m) 改道端到端 / client 降级投影-两站点接线-确认框结构序-操作泵落账);ADR-0022 立项、分层 commit(feat core/client + docs adr/glossary + chore release)。
|
|
16
|
+
|
|
17
|
+
### 0.9.94 重构+修复:变更结局描述客户端单源化(op-outcome)+ 补丁映射 YAML 引号宽容与重复键根治
|
|
18
|
+
|
|
19
|
+
- **需求(2026-10-17 架构巡查 + grill 九决议)**:变更结局的呈现无 owning module——守卫拦截文案装配 main.jsx 三处逐字重复(0.9.93 toWire 收敛了线格式、client 侧呈现是已知残尾,`repairBasis` 后加时的多点位手补复发已实证)、升级成功文案两处逐字、builds 后缀四处拼装、superseded 五处;另:事务补偿对 workspace `patchedDependencies` **非双引号在场形态**(pnpm 的单引号/无引号书写)逐字判重失明——逐字判重可自造重复键文件、复读检查可假 committed(同包链经 strip 先摘在生产不可达,本项属防御面收敛与 fail-closed 补洞)。
|
|
20
|
+
- **方案**:新纯模块 `src/client/op-outcome.js`(GLOSSARY「变更结局描述」入册)——工厂 `createOpOutcome(lookup)` 出成品文案(`createMarkdown(h)` 同款注入先例),`guardBlockedText`/`buildsSuffix`/`upgradeOkText`/`mutationFailed` 四函数;六处接线站点(五类投影)一行接线,流程特有分支(兼容弹窗/能力表拒绝/乐观提示)与 ToolCardRow 短版留守注记。D9:`verifyPatchMapping` workspace 支改 yaml 键成员判定(解析失败 fail-closed 报「复读失败」);`restorePatchMapping` 追加前解析去重(同键任意引号形态在场即跳过,解析失败退化逐字判重)。
|
|
21
|
+
- **验证**:全量 1567 测试全绿(新增 client-op-outcome 参数化矩阵 32 例——guardBlockedText/buildsSuffix/upgradeOkText to×activation 3×3 全交叉/mutationFailed × verb 五枚,期望串全手写字面量;事务补丁映射五钉例——单引号去重/无引号去重/模拟 pnpm 解析炸端到端/坏 YAML 假 committed→fail-closed/restore 退化不抛,夹具异包前提钉死)、typecheck 零错误;收敛锚 main.jsx `join(" | ")` 3→0、`mutationFailed` ≥ 6、`upgradeNotify` 解构随调用点清除;实施经计划评审四轮 15 项收敛 + 硬停点执行评审两轮(基线独立树复跑 1530 对照 + TDD 红→绿独立复演)后放行;实施计划见 `docs/plans/2026-10-17-op-outcome-and-verify-quote-fix-implementation-plan.md`。
|
|
22
|
+
|
|
11
23
|
### 0.9.93 重构:装后守卫线格式收敛至 InstallGuardError.toWire——加守卫字段编辑点 8 → 2(纯重构,线格式语义零变化)
|
|
12
24
|
|
|
13
25
|
- **需求(2026-10-16 架构巡查摩擦点 3,巡查首推)**:`InstallGuardError` → 线格式的投影在手写 5 处逐字重复(host-api 409 payload / tools install+upgrade 两臂 / cli install+upgrade 两臂)+ client 再拼一次——加一个守卫字段要碰 8 处(类 + 5 投影 + client 读取 + 测试),`repairBasis` 后加时各点手补已实证过一次复发。
|
|
@@ -655,6 +667,18 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
655
667
|
|
|
656
668
|
## English
|
|
657
669
|
|
|
670
|
+
### Feature + fix in 0.10.0 — host 0.2.1 already-installed adaptation: third-party two-step upgrade + rollback-reinstall degraded restore + dedicated self-upgrade channel (D3)
|
|
671
|
+
|
|
672
|
+
- **Need (2026-10-17, the owner)**: on the new DSH (0.2.1-rc.1/rc.2) dsh-m could no longer upgrade or self-upgrade any package. Root cause: the host's `installBundle()` gained an already-installed guard — when the package name resolved from a registry spec already exists in the profile manifest it is rejected before pnpm runs, and the official upgrade semantics became "uninstall first, then install"; tarball/git specs are exempt because their package name is unknowable before pnpm (verified on a real machine: installing a tarball URL over the installed dsh-m succeeded, activation shape restart-required). Scope: in-product desktop upgrade/self-upgrade fully broken; uninstall, toggle, fresh install, and the web path unaffected. Version 0.9.95+ skipped straight to 0.10.0.
|
|
673
|
+
- **Approach (ADR-0022, grilled decisions R1–R15)**: ① **install-first adaptive two-step** — no host version probing; only when the first leg returns `already-installed` does it convert to remove + retry (at most once, no loops); the install path silently two-steps on the guard too (installing an installed package is an upgrade), while fresh installs keep zero `removeBundle` calls; release-age/build-blocked/cancelled shapes take precedence over the guard check. ② **Rollback reinstall** — when the install leg terminates in failure the old version is reinstalled automatically: success yields the new **degraded restore** outcome (success shape + degraded/restoredTo; the client projects a warn that never enters the "upgraded vX→vY" arm, the operation pump reuses the existing `warned` state, the settings red dot keeps the existing failed||warned rule, and ✕ dismissal is unchanged); a failed reinstall is the hard `rollback-failed` absence error (with `details.pendingBuilds` surfaced truthfully when the reinstall leg itself is build-blocked); cancellation reports "restored the old version / no upgrade was made" and never masquerades as degraded; a build-blocked install leg still raises the approval request after the rollback has secured the old version. ③ **Channel separation** — third-party plugins use the official two-step; dsh-m's own self-upgrade uses the dedicated tarball direct-install channel (exact version → live-registry `dist.tarball` → intent pre-write → `installBundle(tarballUrl)` → in-place settle), rerouted at a single core seam (`SELF_PKG='dsh-m'`) with zero special-casing in host-api/agent tools/client, and never falling into remove+add; all entries preserved: the header chip gains a confirmation dialog (no uninstall involved, the current version stays intact on failure, a Desktop restart is needed afterwards), and `needsRestart` faithfully passes through the host's `application` (applied → no banner, restart-required → restart prompt). ④ **Boot reconciliation** — a self-upgrade intent ledger (`$DSH_HOME/dshm/self-upgrade-intent.json`; ADR-0018's panel-channel-zero-external-logging invariant intact) serves as a crash safety net: the normal path settles in-process, and only a process that died mid-upgrade is reconciled by the next self-check, once per process, against the actually installed version. ⑤ The mismatched self-upgrade WRITE_GUIDANCE copy no longer sends users "back to DSH Web", pointing to the official Settings → Plugins remove + install (exact version) instead. Seven GLOSSARY terms registered (two-step upgrade / already-installed guard / install-first adaptation / rollback reinstall / degraded restore / dedicated self-upgrade channel / boot reconciliation).
|
|
674
|
+
- **Verification**: typecheck clean; full suite 1585 tests (18 pre-existing environment-bound failures per know-how 028, zero new — the clean-tree comparison yields the same set: symlink-EPERM family + client-lightbox ESM scheme + CRLF source anchor + network fetch); new pins cover plan T1–T17 (intent-ledger storage five states / npmTarballUrl three pins / two-step-rollback-degraded boundary nine pins including remove-failed short-circuit, cancelled copy, and reinstall build-blocked pendingBuilds passthrough / D3 and the SELF_PKG seam rerouting five pins / host-api boot reconciliation plus the upgrade(dsh-m) rerouting end-to-end / client degraded projection, two-site wiring, confirmation-dialog ordering, and pump accounting); ADR-0022 filed, layered commits (feat core/client + docs adr/glossary + chore release).
|
|
675
|
+
|
|
676
|
+
### Refactor + fix in 0.9.94 — client-side mutation-outcome copy single-sourced (op-outcome) + patch-mapping YAML quote tolerance and duplicate-key elimination
|
|
677
|
+
|
|
678
|
+
- **Need (2026-10-17 architecture walk + nine grilled decisions)**: the presentation of mutation outcomes had no owning module — the guard-interception copy was assembled verbatim three times in main.jsx (0.9.93's toWire converged the wire format while the client-side presentation remained the known leftover; the multi-point hand-patching recurrence when `repairBasis` was added had already been demonstrated once), the upgrade-success copy twice verbatim, the builds suffix four times, superseded five times; separately: the transaction's compensation was blind to **non-double-quoted in-place forms** of workspace `patchedDependencies` (pnpm's single-quoted/unquoted spellings) via verbatim duplicate checks — the verbatim check could fabricate a duplicate-key file and the read-back check could false-pass (the same-package chain is unreachable in production because strip removes it first; this item is defensive-surface convergence and a fail-closed gap-fill).
|
|
679
|
+
- **Approach**: a new pure module `src/client/op-outcome.js` (GLOSSARY term "Mutation Outcome Copy" registered) — a factory `createOpOutcome(lookup)` producing finished copy (the `createMarkdown(h)` injection precedent) with four functions `guardBlockedText`/`buildsSuffix`/`upgradeOkText`/`mutationFailed`; six wiring sites (five projection families) become one-line calls, while flow-specific branches (compat dialog / capability-rejection banner / optimistic note) and the ToolCardRow short variant stay behind with an annotation. D9: the workspace leg of `verifyPatchMapping` now judges key membership via real yaml parsing (parse failure fails closed with "read-back failed"); `restorePatchMapping` parse-dedupes before appending (skip when the key exists in any quoting form; on parse failure it degrades to today's verbatim line check).
|
|
680
|
+
- **Verification**: full suite 1567 green (new client-op-outcome parametrized matrix of 32 cases — guardBlockedText/buildsSuffix/upgradeOkText to×activation 3×3 full cross/mutationFailed × five verbs, expected strings all hand-written literals; five transaction patch-mapping pins — single-quote dedupe / unquoted dedupe / simulated pnpm parse-explosion end-to-end / malformed-YAML false-committed → fail-closed / restore degradation does not throw, with the cross-package fixture premise pinned), typecheck clean; convergence anchors in main.jsx: `join(" | ")` 3→0, `mutationFailed` ≥ 6, and the `upgradeNotify` destructure removed along with its call sites; released after a four-round 15-finding plan review plus a hard-stopped two-round execution review (baseline independently re-run at 1530 for comparison + TDD red→green independently re-enacted); implementation plan in `docs/plans/2026-10-17-op-outcome-and-verify-quote-fix-implementation-plan.md`.
|
|
681
|
+
|
|
658
682
|
### Refactor in 0.9.93 — post-install guard wire format converged onto InstallGuardError.toWire; adding a guard field now touches 8 → 2 points (pure refactor, zero wire-format semantic change)
|
|
659
683
|
|
|
660
684
|
- **Need (2026-10-16 architecture walk, friction point 3, the walk's top pick)**: the `InstallGuardError` → wire-format projection was hand-written verbatim in 5 places (host-api 409 payload / tools install+upgrade arms / cli install+upgrade arms) plus one more assembly in the client — adding a guard field meant touching 8 points (class + 5 projections + client read + tests); the later addition of `repairBasis` had already demonstrated the recurrence once.
|
package/lib/client.js
CHANGED
|
@@ -1654,10 +1654,12 @@ function createOpsPump(store, getCtx) {
|
|
|
1654
1654
|
if (!err) {
|
|
1655
1655
|
const opMeta = value && typeof value === "object" && value.opMeta && typeof value.opMeta === "object" ? value.opMeta : null;
|
|
1656
1656
|
const cur = opMeta ? store.list().find((x) => x.id === queued.id) : null;
|
|
1657
|
+
const degraded = value && typeof value === "object" && value.degraded === true ? { degraded: true, restoredTo: value.restoredTo } : null;
|
|
1658
|
+
const mergedMeta = opMeta || degraded ? { ...cur && cur.meta || {}, ...opMeta || {}, ...degraded || {} } : null;
|
|
1657
1659
|
finalize({
|
|
1658
|
-
status: value && value.opWarning ? "warned" : "done",
|
|
1660
|
+
status: value && value.opWarning || degraded ? "warned" : "done",
|
|
1659
1661
|
warning: value && value.opWarning,
|
|
1660
|
-
...
|
|
1662
|
+
...mergedMeta ? { meta: mergedMeta } : {}
|
|
1661
1663
|
});
|
|
1662
1664
|
if (waitersEntry) waitersEntry.resolve(value);
|
|
1663
1665
|
} else {
|
|
@@ -2005,6 +2007,54 @@ var init_install_cmd = __esm({
|
|
|
2005
2007
|
}
|
|
2006
2008
|
});
|
|
2007
2009
|
|
|
2010
|
+
// src/client/op-outcome.js
|
|
2011
|
+
var op_outcome_exports = {};
|
|
2012
|
+
__export(op_outcome_exports, {
|
|
2013
|
+
createOpOutcome: () => createOpOutcome
|
|
2014
|
+
});
|
|
2015
|
+
function createOpOutcome(lookup2) {
|
|
2016
|
+
const guardBlockedText = (guard) => [
|
|
2017
|
+
lookup2("guard.blocked"),
|
|
2018
|
+
`${lookup2("guard.compstatus")}: ${guard.compensation?.status || "\u2014"}\uFF08${guard.compensation?.note || ""}\uFF09`,
|
|
2019
|
+
guard.repairBasis ? `${lookup2("guard.repairbasis")}: ${guard.repairBasis}` : null,
|
|
2020
|
+
lookup2("guard.noforce"),
|
|
2021
|
+
guard.restartSafe ? lookup2("guard.restartsafenow") : lookup2("guard.restartunsafe")
|
|
2022
|
+
].filter(Boolean).join(" | ");
|
|
2023
|
+
const buildsSuffix = (res) => res.buildApprovals && res.buildApprovals.length ? lookup2("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup2("notify.builds.fallback") : "";
|
|
2024
|
+
const upgradeOkText = (res) => {
|
|
2025
|
+
const note = upgradeNotify(res.activation);
|
|
2026
|
+
return {
|
|
2027
|
+
needsRestart: note.needsRestart,
|
|
2028
|
+
text: lookup2("notify.upgraded", {
|
|
2029
|
+
pkg: res.pkg,
|
|
2030
|
+
from: res.fromVersion ? `v${res.fromVersion}` : "\u2014",
|
|
2031
|
+
to: res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : "latest"
|
|
2032
|
+
}) + buildsSuffix(res) + (note.suffixKey ? lookup2(note.suffixKey) : "")
|
|
2033
|
+
};
|
|
2034
|
+
};
|
|
2035
|
+
const degradedOutcome = (res) => res && res.degraded === true ? { kind: "warn", text: lookup2("op.degraded", { restoredTo: res.restoredTo || "\u2014" }) } : null;
|
|
2036
|
+
const mutationFailed = (e, { verb, target }) => {
|
|
2037
|
+
if (e && e.opSuperseded) return { kind: "ok", text: lookup2("op.superseded.note", { target }) };
|
|
2038
|
+
if (e && e.guard) return { kind: "err", text: guardBlockedText(e.guard) };
|
|
2039
|
+
return { kind: "err", text: lookup2(PLAIN_FAIL_KEY[verb], { err: e && e.message || e }) };
|
|
2040
|
+
};
|
|
2041
|
+
return { guardBlockedText, buildsSuffix, upgradeOkText, degradedOutcome, mutationFailed };
|
|
2042
|
+
}
|
|
2043
|
+
var PLAIN_FAIL_KEY;
|
|
2044
|
+
var init_op_outcome = __esm({
|
|
2045
|
+
"src/client/op-outcome.js"() {
|
|
2046
|
+
"use strict";
|
|
2047
|
+
init_operations();
|
|
2048
|
+
PLAIN_FAIL_KEY = {
|
|
2049
|
+
install: "failed.install",
|
|
2050
|
+
upgrade: "failed.upgrade",
|
|
2051
|
+
uninstall: "failed.uninstall",
|
|
2052
|
+
toggle: "toggle.failed",
|
|
2053
|
+
self: "notify.selfupgraded.failed"
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2008
2058
|
// src/client/main.jsx
|
|
2009
2059
|
var React = require("react");
|
|
2010
2060
|
var rd = require("react-dom");
|
|
@@ -2028,11 +2078,12 @@ var { pickPayload: pickPayload2, parseToolArgs: parseToolArgs2 } = (init_tool_vi
|
|
|
2028
2078
|
var { RESTART_POLL_MS: RESTART_POLL_MS2, RESTART_DEADLINE_MS: RESTART_DEADLINE_MS2, nextRestartWait: nextRestartWait2, isAmbiguousRestartRequestError: isAmbiguousRestartRequestError2 } = (init_restart_wait(), __toCommonJS(restart_wait_exports));
|
|
2029
2079
|
var { refreshAfterMutation: refreshAfterMutation2 } = (init_view_refresh(), __toCommonJS(view_refresh_exports));
|
|
2030
2080
|
var { applyInstalledUpdates: applyInstalledUpdates2, installedUpdateStats: installedUpdateStats2 } = (init_installed_updates(), __toCommonJS(installed_updates_exports));
|
|
2031
|
-
var { createOperationsStore: createOperationsStore2, restoreRecords: restoreRecords2, createOpsPump: createOpsPump2, opAppliesTo: opAppliesTo2, TERMINAL_CLEARABLE: TERMINAL_CLEARABLE2,
|
|
2081
|
+
var { createOperationsStore: createOperationsStore2, restoreRecords: restoreRecords2, createOpsPump: createOpsPump2, opAppliesTo: opAppliesTo2, TERMINAL_CLEARABLE: TERMINAL_CLEARABLE2, REGISTRY_DEFAULT_TARGET: REGISTRY_DEFAULT_TARGET2 } = (init_operations(), __toCommonJS(operations_exports));
|
|
2032
2082
|
var { groupOpsByDay: groupOpsByDay2, opRowVm: opRowVm2, opUpgradeMeta: opUpgradeMeta2, canRemoveRow: canRemoveRow2, channelLabelKey: channelLabelKey2 } = (init_operations_view(), __toCommonJS(operations_view_exports));
|
|
2033
2083
|
var { createFavoritesStore: createFavoritesStore2, partitionStale: partitionStale2 } = (init_favorites(), __toCommonJS(favorites_exports));
|
|
2034
2084
|
var { readSelfCheckCache: readSelfCheckCache2, writeSelfCheckCache: writeSelfCheckCache2, clearSelfCheckCache: clearSelfCheckCache2, deriveChipState: deriveChipState2 } = (init_self_check(), __toCommonJS(self_check_exports));
|
|
2035
2085
|
var { shouldShowInstallCmd: shouldShowInstallCmd2 } = (init_install_cmd(), __toCommonJS(install_cmd_exports));
|
|
2086
|
+
var { createOpOutcome: createOpOutcome2 } = (init_op_outcome(), __toCommonJS(op_outcome_exports));
|
|
2036
2087
|
var ZH = {
|
|
2037
2088
|
"market.title": "\u63D2\u4EF6\u5E02\u573A",
|
|
2038
2089
|
"tab.market": "\u5E02\u573A",
|
|
@@ -2297,8 +2348,12 @@ var ZH = {
|
|
|
2297
2348
|
"notify.upgraded.clientonly": "\u3002\u7EAF\u5BA2\u6237\u7AEF\u66F4\u65B0\uFF1A\u5237\u65B0\u9875\u9762\u5373\u53EF\u751F\u6548\uFF0C\u65E0\u9700\u91CD\u542F",
|
|
2298
2349
|
"notify.upgraded.activationUnknown": "\u3002\u751F\u6548\u5224\u5B9A\u672A\u5B8C\u6210\uFF1A\u4E3A\u786E\u4FDD\u751F\u6548\u8BF7\u91CD\u542F DSH Web",
|
|
2299
2350
|
"notify.upgradehint": "\uFF08\u6CE8\u610F\uFF1A\u8BE5\u63D2\u4EF6\u6267\u884C\u4E86\u6784\u5EFA\u811A\u672C\uFF09",
|
|
2351
|
+
"op.degraded": "\u5347\u7EA7\u5931\u8D25\uFF1A\u65B0\u7248\u5B89\u88C5\u672A\u6210\u529F\uFF0C\u5DF2\u81EA\u52A8\u6062\u590D\u65E7\u7248 {restoredTo}\u2014\u2014\u63D2\u4EF6\u5F53\u524D\u53EF\u7528\uFF0C\u53EF\u7A0D\u540E\u91CD\u8BD5\u3002",
|
|
2300
2352
|
"notify.selfupgraded": "dsh-m \u5DF2\u5347\u7EA7\uFF08v{from} \u2192 v{to}\uFF09",
|
|
2301
2353
|
"notify.selfupgraded.failed": "dsh-m \u5347\u7EA7\u5931\u8D25\uFF1A{err}",
|
|
2354
|
+
"selfupgrade.confirm": "\u5C06\u901A\u8FC7 tarball \u76F4\u88C5\u901A\u9053\u628A dsh-m \u5347\u7EA7\u5230 {version}\uFF1A\u4E0D\u7ECF\u8FC7\u5378\u8F7D\uFF0C\u5931\u8D25\u65F6\u5F53\u524D\u7248\u672C\u4E0D\u53D7\u5F71\u54CD\uFF1B\u5B8C\u6210\u540E\u9700\u91CD\u542F Desktop \u751F\u6548\u3002",
|
|
2355
|
+
"selfupgrade.confirm.noversion": "\u5C06\u901A\u8FC7 tarball \u76F4\u88C5\u901A\u9053\u5347\u7EA7 dsh-m\uFF1A\u4E0D\u7ECF\u8FC7\u5378\u8F7D\uFF0C\u5931\u8D25\u65F6\u5F53\u524D\u7248\u672C\u4E0D\u53D7\u5F71\u54CD\uFF1B\u5B8C\u6210\u540E\u9700\u91CD\u542F Desktop \u751F\u6548\u3002",
|
|
2356
|
+
"selfupgrade.go": "\u5347\u7EA7",
|
|
2302
2357
|
"failed.install": "\u5B89\u88C5\u5931\u8D25\uFF1A{err}",
|
|
2303
2358
|
"failed.uninstall": "\u5378\u8F7D\u5931\u8D25\uFF1A{err}",
|
|
2304
2359
|
"failed.upgrade": "\u5347\u7EA7\u5931\u8D25\uFF1A{err}",
|
|
@@ -2597,8 +2652,12 @@ var EN = {
|
|
|
2597
2652
|
"notify.upgraded.clientonly": ". Client-only update: refresh the page to take effect \u2014 no restart needed",
|
|
2598
2653
|
"notify.upgraded.activationUnknown": ". Activation classification unavailable: restart DSH Web to ensure the new version is live",
|
|
2599
2654
|
"notify.upgradehint": " (note: this plugin ran build scripts)",
|
|
2655
|
+
"op.degraded": "Upgrade failed: the new version did not install \u2014 rolled back to {restoredTo}. The plugin still works; you can retry later.",
|
|
2600
2656
|
"notify.selfupgraded": "dsh-m upgraded (v{from} \u2192 v{to})",
|
|
2601
2657
|
"notify.selfupgraded.failed": "dsh-m upgrade failed: {err}",
|
|
2658
|
+
"selfupgrade.confirm": "Upgrade dsh-m to {version} via the tarball direct-install channel: no uninstall involved, the current version stays intact on failure; a Desktop restart is needed afterwards.",
|
|
2659
|
+
"selfupgrade.confirm.noversion": "Upgrade dsh-m via the tarball direct-install channel: no uninstall involved, the current version stays intact on failure; a Desktop restart is needed afterwards.",
|
|
2660
|
+
"selfupgrade.go": "Upgrade",
|
|
2602
2661
|
"failed.install": "Install failed: {err}",
|
|
2603
2662
|
"failed.uninstall": "Uninstall failed: {err}",
|
|
2604
2663
|
"failed.upgrade": "Upgrade failed: {err}",
|
|
@@ -2645,6 +2704,7 @@ function lookup(key, params) {
|
|
|
2645
2704
|
const dict = browserLang() === "en" ? EN : ZH;
|
|
2646
2705
|
return interpolate(dict[key] ?? ZH[key] ?? key, params);
|
|
2647
2706
|
}
|
|
2707
|
+
var oc = createOpOutcome2(lookup);
|
|
2648
2708
|
function communityLabels(data) {
|
|
2649
2709
|
const c = data && data.community && typeof data.community === "object" ? data.community : {};
|
|
2650
2710
|
const zh = c.categoryLabels && typeof c.categoryLabels === "object" ? c.categoryLabels : {};
|
|
@@ -4079,7 +4139,7 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, o
|
|
|
4079
4139
|
h(SearchBox, { key: zone, placeholder: lookup("search.ph"), initial: query.query, onCommit: (v) => updateQuery(v ? { query: v, category: null } : { query: v }) })
|
|
4080
4140
|
) : null;
|
|
4081
4141
|
const zoneBar = zoneChips3;
|
|
4082
|
-
const installResultText = (res) => lookup("notify.installed", { pkg: res.pkg, version: res.version ? ` v${res.version}` : "" }) +
|
|
4142
|
+
const installResultText = (res) => lookup("notify.installed", { pkg: res.pkg, version: res.version ? ` v${res.version}` : "" }) + oc.buildsSuffix(res) + (res.bundleWarning === "no-patch-layer" ? lookup("notify.bundlewarning") : "");
|
|
4083
4143
|
const installDone = (it2, res) => {
|
|
4084
4144
|
notify({ kind: "ok", needsRestart: true, text: installResultText(res) });
|
|
4085
4145
|
setInstallNote({ id: it2.id, kind: "ok", text: installResultText(res) });
|
|
@@ -4099,25 +4159,12 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, o
|
|
|
4099
4159
|
installDone(it, res);
|
|
4100
4160
|
await (onMutation ? onMutation() : reload(false));
|
|
4101
4161
|
} catch (e) {
|
|
4102
|
-
if (e && e.
|
|
4103
|
-
notify({ kind: "ok", text: lookup("op.superseded.note", { target: it.name }) });
|
|
4104
|
-
setInstallNote({ id: it.id, kind: "hint", text: lookup("op.superseded.note", { target: it.name }) });
|
|
4105
|
-
} else if (e && e.guard) {
|
|
4106
|
-
const guardText = [
|
|
4107
|
-
lookup("guard.blocked"),
|
|
4108
|
-
`${lookup("guard.compstatus")}: ${e.guard.compensation?.status || "\u2014"}\uFF08${e.guard.compensation?.note || ""}\uFF09`,
|
|
4109
|
-
e.guard.repairBasis ? `${lookup("guard.repairbasis")}: ${e.guard.repairBasis}` : null,
|
|
4110
|
-
lookup("guard.noforce"),
|
|
4111
|
-
e.guard.restartSafe ? lookup("guard.restartsafenow") : lookup("guard.restartunsafe")
|
|
4112
|
-
].filter(Boolean).join(" | ");
|
|
4113
|
-
notify({ kind: "err", text: guardText });
|
|
4114
|
-
setInstallNote({ id: it.id, kind: "err", text: guardText });
|
|
4115
|
-
} else if (e && e.issue) {
|
|
4162
|
+
if (e && e.issue) {
|
|
4116
4163
|
setCompatConfirm({ it, version, issue: e.issue, opId: e.opId });
|
|
4117
4164
|
} else {
|
|
4118
|
-
const
|
|
4119
|
-
notify({ kind:
|
|
4120
|
-
setInstallNote({ id: it.id, kind: "err", text:
|
|
4165
|
+
const m = oc.mutationFailed(e, { verb: "install", target: it.name });
|
|
4166
|
+
notify({ kind: m.kind, text: m.text });
|
|
4167
|
+
setInstallNote({ id: it.id, kind: m.kind === "ok" ? "hint" : "err", text: m.text });
|
|
4121
4168
|
}
|
|
4122
4169
|
}
|
|
4123
4170
|
};
|
|
@@ -4128,31 +4175,20 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, o
|
|
|
4128
4175
|
const m = opUpgradeMeta2(r);
|
|
4129
4176
|
return m ? { ...r, opMeta: m } : r;
|
|
4130
4177
|
});
|
|
4131
|
-
const
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
await (onMutation ? onMutation() : void 0);
|
|
4136
|
-
} catch (e) {
|
|
4137
|
-
if (e && e.opSuperseded) {
|
|
4138
|
-
const sup = lookup("op.superseded.note", { target: it.installedPkg });
|
|
4139
|
-
notify({ kind: "ok", text: sup });
|
|
4140
|
-
setInstallNote({ id: it.id, kind: "hint", text: sup });
|
|
4141
|
-
} else if (e && e.guard) {
|
|
4142
|
-
const guardText = [
|
|
4143
|
-
lookup("guard.blocked"),
|
|
4144
|
-
`${lookup("guard.compstatus")}: ${e.guard.compensation?.status || "\u2014"}\uFF08${e.guard.compensation?.note || ""}\uFF09`,
|
|
4145
|
-
e.guard.repairBasis ? `${lookup("guard.repairbasis")}: ${e.guard.repairBasis}` : null,
|
|
4146
|
-
lookup("guard.noforce"),
|
|
4147
|
-
e.guard.restartSafe ? lookup("guard.restartsafenow") : lookup("guard.restartunsafe")
|
|
4148
|
-
].filter(Boolean).join(" | ");
|
|
4149
|
-
notify({ kind: "err", text: guardText });
|
|
4150
|
-
setInstallNote({ id: it.id, kind: "err", text: guardText });
|
|
4178
|
+
const degraded = oc.degradedOutcome(res);
|
|
4179
|
+
if (degraded) {
|
|
4180
|
+
notify({ kind: "warn", text: degraded.text });
|
|
4181
|
+
setInstallNote({ id: it.id, kind: "warn", text: degraded.text });
|
|
4151
4182
|
} else {
|
|
4152
|
-
const
|
|
4153
|
-
notify({ kind: "
|
|
4154
|
-
setInstallNote({ id: it.id, kind: "
|
|
4183
|
+
const ok = oc.upgradeOkText(res);
|
|
4184
|
+
notify({ kind: "ok", needsRestart: ok.needsRestart, text: ok.text });
|
|
4185
|
+
setInstallNote({ id: it.id, kind: "ok", text: ok.text });
|
|
4155
4186
|
}
|
|
4187
|
+
await (onMutation ? onMutation() : void 0);
|
|
4188
|
+
} catch (e) {
|
|
4189
|
+
const m = oc.mutationFailed(e, { verb: "upgrade", target: it.installedPkg });
|
|
4190
|
+
notify({ kind: m.kind, text: m.text });
|
|
4191
|
+
setInstallNote({ id: it.id, kind: m.kind === "ok" ? "hint" : "err", text: m.text });
|
|
4156
4192
|
}
|
|
4157
4193
|
};
|
|
4158
4194
|
const CompatDialog = compatConfirm ? h(
|
|
@@ -4510,11 +4546,8 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
|
|
|
4510
4546
|
notify({ kind: "ok", needsRestart: note.needsRestart, text: lookup(note.textKey, note.params) + extra });
|
|
4511
4547
|
await reload();
|
|
4512
4548
|
} catch (e) {
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
} else {
|
|
4516
|
-
notify({ kind: "err", text: lookup("toggle.failed", { err: e && e.message || e }) });
|
|
4517
|
-
}
|
|
4549
|
+
const m = oc.mutationFailed(e, { verb: "toggle", target: it.pkg });
|
|
4550
|
+
notify({ kind: m.kind, text: m.text });
|
|
4518
4551
|
} finally {
|
|
4519
4552
|
setPendingToggles((m) => {
|
|
4520
4553
|
if (!(it.pkg in m)) return m;
|
|
@@ -4533,11 +4566,8 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
|
|
|
4533
4566
|
});
|
|
4534
4567
|
await (onMutation ? onMutation() : reload());
|
|
4535
4568
|
} catch (e) {
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
} else {
|
|
4539
|
-
notify({ kind: "err", text: lookup("failed.uninstall", { err: e && e.message || e }) });
|
|
4540
|
-
}
|
|
4569
|
+
const m = oc.mutationFailed(e, { verb: "uninstall", target: it.pkg });
|
|
4570
|
+
notify({ kind: m.kind, text: m.text });
|
|
4541
4571
|
}
|
|
4542
4572
|
};
|
|
4543
4573
|
const doUpgrade = async (it) => {
|
|
@@ -4547,34 +4577,17 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
|
|
|
4547
4577
|
const m = opUpgradeMeta2(r);
|
|
4548
4578
|
return m ? { ...r, opMeta: m } : r;
|
|
4549
4579
|
});
|
|
4550
|
-
const
|
|
4551
|
-
|
|
4552
|
-
kind: "
|
|
4553
|
-
needsRestart: note.needsRestart,
|
|
4554
|
-
text: lookup("notify.upgraded", {
|
|
4555
|
-
pkg: res.pkg,
|
|
4556
|
-
from: res.fromVersion ? `v${res.fromVersion}` : "\u2014",
|
|
4557
|
-
to: res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : "latest"
|
|
4558
|
-
}) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "") + (note.suffixKey ? lookup(note.suffixKey) : "")
|
|
4559
|
-
});
|
|
4560
|
-
await (onMutation ? onMutation() : reload());
|
|
4561
|
-
} catch (e) {
|
|
4562
|
-
if (e && e.guard) {
|
|
4563
|
-
notify({
|
|
4564
|
-
kind: "err",
|
|
4565
|
-
text: [
|
|
4566
|
-
lookup("guard.blocked"),
|
|
4567
|
-
`${lookup("guard.compstatus")}: ${e.guard.compensation?.status || "\u2014"}\uFF08${e.guard.compensation?.note || ""}\uFF09`,
|
|
4568
|
-
e.guard.repairBasis ? `${lookup("guard.repairbasis")}: ${e.guard.repairBasis}` : null,
|
|
4569
|
-
lookup("guard.noforce"),
|
|
4570
|
-
e.guard.restartSafe ? lookup("guard.restartsafenow") : lookup("guard.restartunsafe")
|
|
4571
|
-
].filter(Boolean).join(" | ")
|
|
4572
|
-
});
|
|
4573
|
-
} else if (e && e.opSuperseded) {
|
|
4574
|
-
notify({ kind: "ok", text: lookup("op.superseded.note", { target: it.pkg }) });
|
|
4580
|
+
const degraded = oc.degradedOutcome(res);
|
|
4581
|
+
if (degraded) {
|
|
4582
|
+
notify({ kind: "warn", text: degraded.text });
|
|
4575
4583
|
} else {
|
|
4576
|
-
|
|
4584
|
+
const ok = oc.upgradeOkText(res);
|
|
4585
|
+
notify({ kind: "ok", needsRestart: ok.needsRestart, text: ok.text });
|
|
4577
4586
|
}
|
|
4587
|
+
await (onMutation ? onMutation() : reload());
|
|
4588
|
+
} catch (e) {
|
|
4589
|
+
const m = oc.mutationFailed(e, { verb: "upgrade", target: it.pkg });
|
|
4590
|
+
notify({ kind: m.kind, text: m.text });
|
|
4578
4591
|
}
|
|
4579
4592
|
};
|
|
4580
4593
|
if (loading && !data) return h("div", { className: "dshm-empty" }, lookup("installed.loading"), Spin());
|
|
@@ -5178,11 +5191,12 @@ var TABS = [
|
|
|
5178
5191
|
["installed", "tab.installed", "installed"],
|
|
5179
5192
|
["settings", "tab.settings", null]
|
|
5180
5193
|
];
|
|
5181
|
-
function DshmVersionChip({ version, notify, initialCheck, ops }) {
|
|
5194
|
+
function DshmVersionChip({ version, notify, initialCheck, ops, initialConfirm }) {
|
|
5182
5195
|
const [check, setCheck] = useState(
|
|
5183
5196
|
() => initialCheck !== void 0 ? initialCheck : typeof window !== "undefined" && window.localStorage ? readSelfCheckCache2(window.localStorage, { version }) : null
|
|
5184
5197
|
);
|
|
5185
5198
|
const [busy, setBusy] = useState(false);
|
|
5199
|
+
const [confirming, setConfirming] = useState(initialConfirm === true);
|
|
5186
5200
|
useEffect(() => {
|
|
5187
5201
|
if (initialCheck !== void 0) return;
|
|
5188
5202
|
let live = true;
|
|
@@ -5200,6 +5214,11 @@ function DshmVersionChip({ version, notify, initialCheck, ops }) {
|
|
|
5200
5214
|
}, [initialCheck]);
|
|
5201
5215
|
const doUpgrade = async () => {
|
|
5202
5216
|
if (busy) return;
|
|
5217
|
+
setConfirming(true);
|
|
5218
|
+
};
|
|
5219
|
+
const performUpgrade = async () => {
|
|
5220
|
+
if (busy) return;
|
|
5221
|
+
setConfirming(false);
|
|
5203
5222
|
setBusy(true);
|
|
5204
5223
|
try {
|
|
5205
5224
|
const res = await (ops ? ops.runOp("upgrade", "dsh-m", async () => {
|
|
@@ -5210,14 +5229,17 @@ function DshmVersionChip({ version, notify, initialCheck, ops }) {
|
|
|
5210
5229
|
setCheck({ current: version, latest: null, outdated: false, ahead: false });
|
|
5211
5230
|
notify({
|
|
5212
5231
|
kind: "ok",
|
|
5213
|
-
|
|
5214
|
-
|
|
5232
|
+
// 0.10.0 R10:生效语义忠实透传宿主 application——applied/overridden → false(不弹重启横幅)、
|
|
5233
|
+
// restart-required → true;响应缺席时保守 true(旧行为兜底)
|
|
5234
|
+
needsRestart: res.needsRestart ?? true,
|
|
5235
|
+
text: lookup("notify.selfupgraded", { from: version, to: res.version || "latest" }) + oc.buildsSuffix(res)
|
|
5215
5236
|
});
|
|
5216
5237
|
} catch (e) {
|
|
5217
5238
|
if (e && e.unsupported) {
|
|
5218
5239
|
notify({ kind: "info", text: e.unsupported.guidance || e && e.message || String(e) });
|
|
5219
5240
|
} else {
|
|
5220
|
-
|
|
5241
|
+
const m = oc.mutationFailed(e, { verb: "self", target: "dsh-m" });
|
|
5242
|
+
notify({ kind: m.kind, text: m.text });
|
|
5221
5243
|
}
|
|
5222
5244
|
} finally {
|
|
5223
5245
|
setBusy(false);
|
|
@@ -5225,6 +5247,36 @@ function DshmVersionChip({ version, notify, initialCheck, ops }) {
|
|
|
5225
5247
|
};
|
|
5226
5248
|
const state = deriveChipState2(check, version);
|
|
5227
5249
|
if (state.kind === "outdated") {
|
|
5250
|
+
if (confirming) {
|
|
5251
|
+
return h(
|
|
5252
|
+
"div",
|
|
5253
|
+
{ className: "dshm-compat-overlay", ...backdropCloseHandlers2(() => setConfirming(false)) },
|
|
5254
|
+
h(
|
|
5255
|
+
"div",
|
|
5256
|
+
{ className: "dshm-compat-dialog", onClick: (e) => e.stopPropagation() },
|
|
5257
|
+
h("div", { className: "dshm-compat-title" }, lookup("selfupdate.available", { v: state.latest || "?" })),
|
|
5258
|
+
h(
|
|
5259
|
+
"div",
|
|
5260
|
+
{ className: "dshm-compat-body" },
|
|
5261
|
+
state.latest ? lookup("selfupgrade.confirm", { version: `v${state.latest}` }) : lookup("selfupgrade.confirm.noversion")
|
|
5262
|
+
),
|
|
5263
|
+
h(
|
|
5264
|
+
"div",
|
|
5265
|
+
{ className: "dshm-actions" },
|
|
5266
|
+
h(
|
|
5267
|
+
"button",
|
|
5268
|
+
{ className: "dshm-btn", onClick: () => setConfirming(false) },
|
|
5269
|
+
lookup("common.later")
|
|
5270
|
+
),
|
|
5271
|
+
h(
|
|
5272
|
+
"button",
|
|
5273
|
+
{ className: "dshm-btn primary", onClick: performUpgrade, disabled: busy },
|
|
5274
|
+
lookup("selfupgrade.go")
|
|
5275
|
+
)
|
|
5276
|
+
)
|
|
5277
|
+
)
|
|
5278
|
+
);
|
|
5279
|
+
}
|
|
5228
5280
|
return h(
|
|
5229
5281
|
"button",
|
|
5230
5282
|
{
|
|
@@ -30,7 +30,7 @@ export const WRITE_GUIDANCE = {
|
|
|
30
30
|
'set-enabled': '',
|
|
31
31
|
upgrade: 'Desktop 暂不支持在市场内升级插件:请在官方 Desktop 的插件管理(Settings → Plugins)检查更新,或回到 DSH Web 使用 dsh-m 升级。',
|
|
32
32
|
uninstall: 'Desktop 暂不支持在市场内卸载插件:请使用官方 Desktop 的插件管理(Settings → Plugins)卸载。',
|
|
33
|
-
'self-upgrade': '
|
|
33
|
+
'self-upgrade': '当前宿主形态不支持面板内自更新:请在官方 Settings → Plugins 对 dsh-m 执行 remove + install(精确版本),或完全退出 Desktop 后用 `dsh plugin --profile desktop add dsh-m@<版本>`。',
|
|
34
34
|
restart: 'Desktop 不由 dsh-m 重启:请退出并重新打开 Desktop 应用(关闭窗口可能只是隐藏),让新安装的插件生效。',
|
|
35
35
|
};
|
|
36
36
|
export class ProfileUnsupportedError extends Error {
|
package/lib/core/host-api.js
CHANGED
|
@@ -13,9 +13,10 @@ import { runDoctor } from './doctor.js';
|
|
|
13
13
|
import { listInstalledWithMeta, listMarket, existingMarketIds, installFromRegistry, selfUpgrade, uninstallPlugin, upgradePlugin, InstallGuardError, } from './market.js';
|
|
14
14
|
import { runProfileTransaction, TransactionError } from './profile-transaction.js';
|
|
15
15
|
import { listExternalOperations, clearExternalOperations, removeExternalOperation } from './external-journal.js';
|
|
16
|
-
import {
|
|
16
|
+
import { readUpgradeIntent, resolveUpgradeIntent, writeUpgradeIntent } from './upgrade-intent.js';
|
|
17
|
+
import { readInstalledPluginReadme, listInstalledPlugins } from './installed.js';
|
|
17
18
|
import { loadPrimaryScreenshots } from './primary-screenshots.js';
|
|
18
|
-
import { isNewerVersion, npmLatest, npmPackumentReadme } from './versions.js';
|
|
19
|
+
import { isNewerVersion, npmLatest, npmTarballUrl, npmPackumentReadme } from './versions.js';
|
|
19
20
|
import { getCommunitySummary } from './community.js';
|
|
20
21
|
import { RegistryConfigError } from './registry-controller.js';
|
|
21
22
|
import { checkRegistryEntries } from './registry-check.js';
|
|
@@ -24,7 +25,7 @@ import { servingPort, scheduleRestart } from './restart.js';
|
|
|
24
25
|
import { togglePlugin, ToggleError } from './toggle.js';
|
|
25
26
|
import { IncompatibleError } from './compat-check.js';
|
|
26
27
|
import { ProfileUnsupportedError, assertWriteAllowed } from './active-profile.js';
|
|
27
|
-
import { DesktopOpsError, desktopInstallFromRegistry, desktopToggle, desktopUninstall, desktopUpgradeFromRegistry } from './profile-ops.js';
|
|
28
|
+
import { DesktopOpsError, desktopInstallFromRegistry, desktopToggle, desktopUninstall, desktopUpgradeFromRegistry, desktopSelfUpgradeViaTarball } from './profile-ops.js';
|
|
28
29
|
export class BadJsonError extends Error {
|
|
29
30
|
}
|
|
30
31
|
export class BodyTooLargeError extends Error {
|
|
@@ -226,12 +227,20 @@ export function createApiDispatcher(ctx) {
|
|
|
226
227
|
desktopToggle: ctx.deps?.desktopToggle ?? desktopToggle,
|
|
227
228
|
desktopUninstall: ctx.deps?.desktopUninstall ?? desktopUninstall,
|
|
228
229
|
desktopUpgrade: ctx.deps?.desktopUpgrade ?? desktopUpgradeFromRegistry,
|
|
230
|
+
desktopSelfUpgradeViaTarball: ctx.deps?.desktopSelfUpgradeViaTarball ?? desktopSelfUpgradeViaTarball,
|
|
231
|
+
readUpgradeIntent: ctx.deps?.readUpgradeIntent ?? readUpgradeIntent,
|
|
232
|
+
resolveUpgradeIntent: ctx.deps?.resolveUpgradeIntent ?? resolveUpgradeIntent,
|
|
233
|
+
writeUpgradeIntent: ctx.deps?.writeUpgradeIntent ?? writeUpgradeIntent,
|
|
234
|
+
npmTarballUrl: ctx.deps?.npmTarballUrl ?? npmTarballUrl,
|
|
235
|
+
listInstalled: ctx.deps?.listInstalled ?? listInstalledPlugins,
|
|
229
236
|
ensureService: ctx.deps?.ensureService,
|
|
230
237
|
listExternalOperations: ctx.deps?.listExternalOperations ?? listExternalOperations,
|
|
231
238
|
clearExternalOperations: ctx.deps?.clearExternalOperations ?? clearExternalOperations,
|
|
232
239
|
removeExternalOperation: ctx.deps?.removeExternalOperation ?? removeExternalOperation,
|
|
233
240
|
};
|
|
234
241
|
const cfg = () => ctx.controller.config;
|
|
242
|
+
// 0.10.0:开机对账每进程一次(dispatcher 生命周期 = 宿主一次 apply)
|
|
243
|
+
let selfCheckIntentDone = false;
|
|
235
244
|
// 0.9.0:宿主信任检查委派(含 ping 与未知 method,fail-closed);
|
|
236
245
|
// profile 为 host 生命周期内不可变对象(host.ts apply 期解析一次)
|
|
237
246
|
const rejectRequest = ctx.deps?.rejectRequest ?? (() => 403);
|
|
@@ -261,30 +270,55 @@ export function createApiDispatcher(ctx) {
|
|
|
261
270
|
};
|
|
262
271
|
break;
|
|
263
272
|
case 'self-check': {
|
|
273
|
+
// 0.10.0 开机对账(崩溃兜底网,每进程一次):上进程若死于自升级 promise 悬垂,delegated
|
|
274
|
+
// intent 遗留至此——与本进程实际版本比对收口;本进程内 D3 已就地 resolve 的(或无悬垂)
|
|
275
|
+
// 幂等 no-op。ADR-0018:读写独立 intent ledger,零外部日志。对账自身故障零涟漪。
|
|
276
|
+
let intentResolved;
|
|
277
|
+
if (!selfCheckIntentDone) {
|
|
278
|
+
selfCheckIntentDone = true;
|
|
279
|
+
try {
|
|
280
|
+
const rec = await d.readUpgradeIntent();
|
|
281
|
+
if (rec && rec.status === 'delegated') {
|
|
282
|
+
const match = rec.version === ctx.pkg.version;
|
|
283
|
+
await d.resolveUpgradeIntent({ ok: match, installedVersion: match ? ctx.pkg.version : undefined });
|
|
284
|
+
intentResolved = { ok: match, target: rec.target, ...(match ? { installedVersion: ctx.pkg.version } : {}) };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// delegated 残留下次进程再收口;绝不阻塞 self-check
|
|
289
|
+
}
|
|
290
|
+
}
|
|
264
291
|
try {
|
|
265
292
|
const latest = await d.npmLatest(ctx.pkg.name, cfg().timeoutMs ?? 20_000);
|
|
266
293
|
const outdated = isNewerVersion(latest.version, ctx.pkg.version);
|
|
267
294
|
// ahead(0.8.0):本地 dev 版领先 npm 发布——设置页改显「本地开发版」而非误导性的旧「npm 最新」
|
|
268
295
|
const ahead = !outdated && latest.version !== ctx.pkg.version;
|
|
269
|
-
payload = { current: ctx.pkg.version, latest: latest.version, outdated, ahead };
|
|
296
|
+
payload = { current: ctx.pkg.version, latest: latest.version, outdated, ahead, ...(intentResolved ? { intentResolved } : {}) };
|
|
270
297
|
}
|
|
271
298
|
catch (err) {
|
|
272
|
-
payload = { current: ctx.pkg.version, latest: null, outdated: false, ahead: false, error: err instanceof Error ? err.message : String(err) };
|
|
299
|
+
payload = { current: ctx.pkg.version, latest: null, outdated: false, ahead: false, error: err instanceof Error ? err.message : String(err), ...(intentResolved ? { intentResolved } : {}) };
|
|
273
300
|
}
|
|
274
301
|
break;
|
|
275
302
|
}
|
|
276
303
|
case 'self-upgrade': {
|
|
277
304
|
assertWriteAllowed(profile, 'self-upgrade');
|
|
278
|
-
// 0.
|
|
279
|
-
//
|
|
305
|
+
// 0.10.0 D3 自升级专用通道(desktop):tarball 直覆盖。desktopInstall 的 registry spec 在
|
|
306
|
+
// 0.2.1 宿主必撞 already-installed 守卫,而 remove+add 对调用者自身是破坏性路径(R6:自升级
|
|
307
|
+
// 绝不落入通用两步——改道唯一执行点在 core 缝,此处调 D3 入口即语义同源);tarball spec
|
|
308
|
+
// 不受守卫约束(spike 实证)。恒 panel 语义零外部日志(ADR-0018),intent 预写/收口在 core 层。
|
|
280
309
|
if (profile.kind !== 'web') {
|
|
281
|
-
const managed = await d.
|
|
282
|
-
|
|
283
|
-
profile: profile.name,
|
|
284
|
-
profileDir: profile.dir,
|
|
310
|
+
const managed = await d.desktopSelfUpgradeViaTarball(ctx.pkg.name, ctx.pkg.version, {
|
|
311
|
+
timeoutMs: cfg().timeoutMs,
|
|
285
312
|
signal,
|
|
286
|
-
|
|
287
|
-
|
|
313
|
+
}, {
|
|
314
|
+
getService: d.getService,
|
|
315
|
+
ensureService: d.ensureService,
|
|
316
|
+
npmLatest: d.npmLatest,
|
|
317
|
+
npmTarballUrl: d.npmTarballUrl,
|
|
318
|
+
writeUpgradeIntent: d.writeUpgradeIntent,
|
|
319
|
+
resolveUpgradeIntent: d.resolveUpgradeIntent,
|
|
320
|
+
listInstalled: d.listInstalled,
|
|
321
|
+
});
|
|
288
322
|
if ('needsBuildApproval' in managed) {
|
|
289
323
|
// build-blocked(自升级罕见):结构化透出,不冒充成功
|
|
290
324
|
payload = { ...managed };
|
|
@@ -295,7 +329,8 @@ export function createApiDispatcher(ctx) {
|
|
|
295
329
|
version: managed.version,
|
|
296
330
|
buildApprovals: managed.buildApprovals ?? [],
|
|
297
331
|
fallbackAllBuilds: false,
|
|
298
|
-
|
|
332
|
+
// R10:忠实透传宿主 application——applied/overridden → false;restart-required → true
|
|
333
|
+
needsRestart: managed.needsRestart,
|
|
299
334
|
};
|
|
300
335
|
break;
|
|
301
336
|
}
|
|
@@ -514,7 +549,16 @@ export function createApiDispatcher(ctx) {
|
|
|
514
549
|
profileDir: profile.dir,
|
|
515
550
|
signal,
|
|
516
551
|
channel: 'panel',
|
|
517
|
-
}, {
|
|
552
|
+
}, {
|
|
553
|
+
getService: d.getService,
|
|
554
|
+
ensureService: d.ensureService,
|
|
555
|
+
// 0.10.0:R5 previousVersion / SELF_PKG 缝改道注入透传(生产缺省 = 真实现;测试面经 overrides)
|
|
556
|
+
npmLatest: d.npmLatest,
|
|
557
|
+
listInstalled: d.listInstalled,
|
|
558
|
+
npmTarballUrl: d.npmTarballUrl,
|
|
559
|
+
writeUpgradeIntent: d.writeUpgradeIntent,
|
|
560
|
+
resolveUpgradeIntent: d.resolveUpgradeIntent,
|
|
561
|
+
})) };
|
|
518
562
|
break;
|
|
519
563
|
}
|
|
520
564
|
const version = typeof body.version === 'string' ? body.version : undefined;
|
|
@@ -572,7 +616,16 @@ export function createApiDispatcher(ctx) {
|
|
|
572
616
|
profileDir: profile.dir,
|
|
573
617
|
signal,
|
|
574
618
|
channel: 'panel',
|
|
575
|
-
}, {
|
|
619
|
+
}, {
|
|
620
|
+
getService: d.getService,
|
|
621
|
+
ensureService: d.ensureService,
|
|
622
|
+
// 0.10.0:两步化 previousVersion / SELF_PKG 缝改道 → D3 全链注入透传(生产缺省 = 真实现)
|
|
623
|
+
npmLatest: d.npmLatest,
|
|
624
|
+
listInstalled: d.listInstalled,
|
|
625
|
+
npmTarballUrl: d.npmTarballUrl,
|
|
626
|
+
writeUpgradeIntent: d.writeUpgradeIntent,
|
|
627
|
+
resolveUpgradeIntent: d.resolveUpgradeIntent,
|
|
628
|
+
})) };
|
|
576
629
|
break;
|
|
577
630
|
}
|
|
578
631
|
const result = await d.upgradePlugin(target, cfg(), {
|
package/lib/core/packument.js
CHANGED
|
@@ -255,6 +255,17 @@ export async function npmVersion(pkg, version, timeoutMs = 20_000, signal, regis
|
|
|
255
255
|
throw firstErr;
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* 精确版本 tarball URL(0.10.0,D3 自升级专用通道的取 URL 腿):
|
|
260
|
+
* `npmVersion` 薄封装——复用同一 L2① 履约阶梯(ADR-0012,含镜像 sync 腿与 npmjs 兜底),
|
|
261
|
+
* 不新增 packument 面。tarball 缺失 / 版本不存在 → 可读错误上抛。
|
|
262
|
+
*/
|
|
263
|
+
export async function npmTarballUrl(pkg, version, timeoutMs = 20_000, signal, registry, deps) {
|
|
264
|
+
const detail = await npmVersion(pkg, version, timeoutMs, signal, registry, deps);
|
|
265
|
+
if (!detail.tarball)
|
|
266
|
+
throw new Error(`${pkg}@${version} packument 缺 dist.tarball`);
|
|
267
|
+
return detail.tarball;
|
|
268
|
+
}
|
|
258
269
|
/**
|
|
259
270
|
* 升级探测(检测读)。L3 权威链(ADR-0012):新鲜度敏感——npmjs 权威源优先
|
|
260
271
|
* (首腿超时收紧 `Math.min(timeoutMs, 5_000)`),失败降级生效源(≠npmjs 时);
|
package/lib/core/profile-ops.js
CHANGED
|
@@ -28,7 +28,8 @@ import { loadRegistry as defaultLoadRegistry } from './registry.js';
|
|
|
28
28
|
import { fetchCommunityCatalog as defaultFetchCommunityCatalog } from './community.js';
|
|
29
29
|
import { listInstalledPlugins as defaultListInstalled } from './installed.js';
|
|
30
30
|
import { precheckNpmCompat, IncompatibleError } from './compat-check.js';
|
|
31
|
-
import { npmLatest as defaultNpmLatest, githubLatestTag as defaultGithubLatestTag } from './versions.js';
|
|
31
|
+
import { npmLatest as defaultNpmLatest, npmTarballUrl as defaultNpmTarballUrl, githubLatestTag as defaultGithubLatestTag } from './versions.js';
|
|
32
|
+
import { writeUpgradeIntent as defaultWriteUpgradeIntent, resolveUpgradeIntent as defaultResolveUpgradeIntent } from './upgrade-intent.js';
|
|
32
33
|
import { classifyUpgradeActivation } from './activation.js';
|
|
33
34
|
import { togglePlugin, ToggleError } from './toggle.js';
|
|
34
35
|
import { describeReleaseAgeFailure, releaseAgePrecheck } from './release-age.js';
|
|
@@ -177,12 +178,37 @@ async function desktopInstallLocked(id, cfg, opts, deps) {
|
|
|
177
178
|
if (gate.notice)
|
|
178
179
|
releaseAgeNotice = `;${gate.notice}`;
|
|
179
180
|
}
|
|
181
|
+
// 0.10.0 R5:install 目标已装(0.2.1 宿主 already-installed 守卫必拦)→ 预查旧版本号,
|
|
182
|
+
// 使 runManagedInstall 在守卫命中时可静默两步化(remove+add);全新包(未命中)不注入——
|
|
183
|
+
// T6 钉零 removeBundle。探测失败 fail-open(守卫命中且无 previousVersion 时走既有失败形态)。
|
|
184
|
+
let previousVersion;
|
|
185
|
+
if (opts.profileDir) {
|
|
186
|
+
try {
|
|
187
|
+
const { items } = await (deps.listInstalled ?? defaultListInstalled)(opts.profileDir);
|
|
188
|
+
previousVersion = items.find((it) => it.pkg === pkg)?.version;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
previousVersion = undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// 0.10.0 R6(install 臂):dsh-m 已装态下的 install 调用实为升级(守卫必拦)→ 改道 D3;
|
|
195
|
+
// 全新安装(未装)走既有单步——守卫不触发,R1「新装路径不动」。
|
|
196
|
+
if (pkg === SELF_PKG && previousVersion !== undefined) {
|
|
197
|
+
return desktopSelfUpgradeViaTarballLocked(pkg, previousVersion, { timeoutMs, signal: opts.signal }, {
|
|
198
|
+
getService: () => service,
|
|
199
|
+
npmLatest: deps.npmLatest,
|
|
200
|
+
npmTarballUrl: deps.npmTarballUrl,
|
|
201
|
+
writeUpgradeIntent: deps.writeUpgradeIntent,
|
|
202
|
+
resolveUpgradeIntent: deps.resolveUpgradeIntent,
|
|
203
|
+
listInstalled: deps.listInstalled,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
180
206
|
let change;
|
|
181
207
|
let bundleName;
|
|
182
208
|
let registerNote;
|
|
183
209
|
let managed;
|
|
184
210
|
try {
|
|
185
|
-
managed = await runManagedInstall(service, { pkg, spec, version, profileDir: opts.profileDir, approvedBuilds: opts.approvedBuilds, listInstalled: deps.listInstalled }, { governExclude: deps.governExclude, registerExclude: deps.registerExclude, packumentTimes: deps.packumentTimes });
|
|
211
|
+
managed = await runManagedInstall(service, { pkg, spec, version, ...(previousVersion !== undefined ? { previousVersion } : {}), profileDir: opts.profileDir, approvedBuilds: opts.approvedBuilds, listInstalled: deps.listInstalled }, { governExclude: deps.governExclude, registerExclude: deps.registerExclude, packumentTimes: deps.packumentTimes });
|
|
186
212
|
change = managed.change;
|
|
187
213
|
bundleName = managed.bundleName;
|
|
188
214
|
registerNote = managed.registerNote;
|
|
@@ -200,6 +226,9 @@ async function desktopInstallLocked(id, cfg, opts, deps) {
|
|
|
200
226
|
throw err;
|
|
201
227
|
}
|
|
202
228
|
const retryNote = managed.governRetryNote !== undefined && !governanceNote.includes(managed.governRetryNote) ? managed.governRetryNote : '';
|
|
229
|
+
const twoStepSuffix = managed.twoStepNote !== undefined ? `;${managed.twoStepNote}` : '';
|
|
230
|
+
const degradedRestoredTo = managed.degraded === true ? managed.restoredTo : undefined;
|
|
231
|
+
const degradedSuffix = degradedRestoredTo !== undefined ? `;升级失败已回装旧版 ${degradedRestoredTo}(降级恢复)` : '';
|
|
203
232
|
return {
|
|
204
233
|
id: entry.id,
|
|
205
234
|
pkg,
|
|
@@ -212,9 +241,12 @@ async function desktopInstallLocked(id, cfg, opts, deps) {
|
|
|
212
241
|
...(compat !== null ? { compat } : {}),
|
|
213
242
|
...(compatSkipped ? { compatSkipped } : {}),
|
|
214
243
|
needsRestart: true,
|
|
215
|
-
output: `official pluginManager: application=${change.application ?? 'applied'}, bundle=${bundleName}${change.changed === false ? ', changed=false' : ''}${governanceNote}${retryNote}${releaseAgeNotice}${registerNote ?? ''}`,
|
|
244
|
+
output: `official pluginManager: application=${change.application ?? 'applied'}, bundle=${bundleName}${change.changed === false ? ', changed=false' : ''}${governanceNote}${retryNote}${releaseAgeNotice}${registerNote ?? ''}${twoStepSuffix}${degradedSuffix}`,
|
|
216
245
|
via: 'desktop-manager',
|
|
217
246
|
...(change.application === 'overridden' ? { overridden: true } : {}),
|
|
247
|
+
...(degradedRestoredTo !== undefined ? { degraded: true, restoredTo: degradedRestoredTo } : {}),
|
|
248
|
+
// 0.10.0(评审 R1-2):降级恢复的 version 落账为回装后实际版本(账实相符);spec 字段保留尝试目标
|
|
249
|
+
...(degradedRestoredTo !== undefined ? { version: degradedRestoredTo } : {}),
|
|
218
250
|
};
|
|
219
251
|
}
|
|
220
252
|
/**
|
|
@@ -284,8 +316,27 @@ function isGovableFailure(change) {
|
|
|
284
316
|
return false;
|
|
285
317
|
return change.application === 'failed' || change.error !== undefined;
|
|
286
318
|
}
|
|
319
|
+
/** 官方 pendingBuilds 的鸭子读投影(strings-only)。 */
|
|
320
|
+
function pendingOf(change) {
|
|
321
|
+
return Array.isArray(change.pendingBuilds) ? change.pendingBuilds.filter((n) => typeof n === 'string') : [];
|
|
322
|
+
}
|
|
323
|
+
/** 终态失败(与 runManagedInstall 既有失败闸门同口径)。 */
|
|
324
|
+
function isTerminalFailure(change) {
|
|
325
|
+
return change.application === 'failed' || change.application === 'cancelled' || change.error !== undefined;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 宿主 0.2.1 already-installed 安装守卫命中(0.10.0,R2):application=failed +
|
|
329
|
+
* error.code 精确匹配,且 release-age / build-blocked 形态先于本判定(等待期与审批语义优先)。
|
|
330
|
+
*/
|
|
331
|
+
function isAlreadyInstalledGuard(change) {
|
|
332
|
+
if (change.application !== 'failed' || change.error?.code !== 'already-installed')
|
|
333
|
+
return false;
|
|
334
|
+
if (change.packageResult?.kind === 'build-blocked' && pendingOf(change).length > 0)
|
|
335
|
+
return false;
|
|
336
|
+
return !/ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|minimumReleaseAge/i.test(`${change.error?.diagnostic ?? ''}\n${change.error?.code ?? ''}`);
|
|
337
|
+
}
|
|
287
338
|
async function runManagedInstall(service, ctx, deps = {}) {
|
|
288
|
-
const installOnce = async () => changeOf(await service.installBundle(
|
|
339
|
+
const installOnce = async (spec = ctx.spec) => changeOf(await service.installBundle(spec, {
|
|
289
340
|
enabled: true,
|
|
290
341
|
...(ctx.approvedBuilds && ctx.approvedBuilds.length > 0 ? { approvedBuilds: ctx.approvedBuilds } : {}),
|
|
291
342
|
}));
|
|
@@ -364,6 +415,55 @@ async function runManagedInstall(service, ctx, deps = {}) {
|
|
|
364
415
|
change = await installOnce();
|
|
365
416
|
}
|
|
366
417
|
}
|
|
418
|
+
// 0.10.0(宿主 0.2.1 already-installed 守卫适配,R2/R3):首腿守卫命中 → 静默两步化
|
|
419
|
+
// (removeBundle 旧版 → 重试安装同一 spec,至多一次、无循环);第二腿终态失败 → 兜底回装旧版
|
|
420
|
+
// (remove 后旧名已不在 manifest,守卫不适用):回装成功 = 降级恢复结局(degraded,R4);
|
|
421
|
+
// cancelled / build-blocked 语义优先(不冒充降级);回装失败 = 缺席硬错误(rollback-failed)。
|
|
422
|
+
// 无 previousVersion 不两步化——R3 兜底网的存在前提(无旧版即无回装能力)。
|
|
423
|
+
let twoStepNote;
|
|
424
|
+
if (isAlreadyInstalledGuard(change) && ctx.previousVersion && typeof service.removeBundle === 'function') {
|
|
425
|
+
twoStepNote = 'already-installed → two-step';
|
|
426
|
+
const removeChange = changeOf(await service.removeBundle(ctx.pkg));
|
|
427
|
+
if (isTerminalFailure(removeChange)) {
|
|
428
|
+
// remove 腿失败短路(不回装):旧版尚未移除,状态以官方插件页为准
|
|
429
|
+
throw new DesktopOpsError('remove-failed', `两步升级第一步(卸载旧版)失败(${ctx.pkg};application=${removeChange.application ?? 'failed'}${removeChange.error?.code ? `、error=${removeChange.error.code}` : ''}):未执行升级,请到官方 Desktop 插件页确认 ${ctx.pkg} 状态`);
|
|
430
|
+
}
|
|
431
|
+
change = await installOnce();
|
|
432
|
+
if (isTerminalFailure(change)) {
|
|
433
|
+
const pendingAdd = pendingOf(change);
|
|
434
|
+
const addDiagnostic = `${change.error?.diagnostic ?? ''}${change.error?.code ? `\n${change.error.code}` : ''}`.trim();
|
|
435
|
+
const addCancelled = change.application === 'cancelled';
|
|
436
|
+
const addBuildBlocked = change.packageResult?.kind === 'build-blocked' && pendingAdd.length > 0;
|
|
437
|
+
// 兜底回装旧版(R3):回装腿失败(含其自身 build-blocked)= 缺席硬错误,降级失败真相优先
|
|
438
|
+
const rollbackChange = await installOnce(`${ctx.pkg}@${ctx.previousVersion}`);
|
|
439
|
+
const pendingRollback = pendingOf(rollbackChange);
|
|
440
|
+
if (isTerminalFailure(rollbackChange) || (rollbackChange.packageResult?.kind === 'build-blocked' && pendingRollback.length > 0)) {
|
|
441
|
+
throw new DesktopOpsError('rollback-failed', `升级失败且自动回装旧版也失败(${ctx.pkg}@${ctx.previousVersion}):插件当前可能缺席,请在官方 Desktop 插件页手动重装 ${ctx.pkg}@${ctx.previousVersion}${addDiagnostic !== '' ? `;升级失败原因:${addDiagnostic}` : ''}`, {
|
|
442
|
+
pkg: ctx.pkg,
|
|
443
|
+
previousVersion: ctx.previousVersion,
|
|
444
|
+
...(addDiagnostic !== '' ? { addDiagnostic } : {}),
|
|
445
|
+
...(pendingRollback.length > 0 ? { pendingBuilds: pendingRollback } : {}),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
const verifiedRollback = await verifyAndBundle(rollbackChange);
|
|
449
|
+
if (addBuildBlocked) {
|
|
450
|
+
// build-blocked 语义优先:旧版已兜住,仍抛审批请求(不冒充降级成功)
|
|
451
|
+
throw new DesktopBuildApprovalNeeded(pendingAdd);
|
|
452
|
+
}
|
|
453
|
+
if (addCancelled) {
|
|
454
|
+
// 取消不冒充降级成功:已恢复旧版、未做升级
|
|
455
|
+
throw new DesktopOpsError('install-refused', `升级已被取消(${ctx.pkg}):已恢复旧版 ${ctx.previousVersion},未做升级`);
|
|
456
|
+
}
|
|
457
|
+
// 降级恢复(R4):成功形态 + degraded 标记;跳过挂点2 登记——实际在装的是旧版,登记新版本会是假账
|
|
458
|
+
return {
|
|
459
|
+
...verifiedRollback,
|
|
460
|
+
...(governRetryNote !== undefined ? { governRetryNote } : {}),
|
|
461
|
+
degraded: true,
|
|
462
|
+
restoredTo: ctx.previousVersion,
|
|
463
|
+
twoStepNote,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
367
467
|
if (change.application === 'failed' || change.application === 'cancelled' || change.error) {
|
|
368
468
|
try {
|
|
369
469
|
await mapFailure(change);
|
|
@@ -391,7 +491,11 @@ async function runManagedInstall(service, ctx, deps = {}) {
|
|
|
391
491
|
// fail-open:登记失败不影响安装/升级成功态
|
|
392
492
|
}
|
|
393
493
|
}
|
|
394
|
-
return {
|
|
494
|
+
return {
|
|
495
|
+
...verified,
|
|
496
|
+
...(registerNote !== undefined || governRetryNote !== undefined ? { registerNote, governRetryNote } : {}),
|
|
497
|
+
...(twoStepNote !== undefined ? { twoStepNote } : {}),
|
|
498
|
+
};
|
|
395
499
|
}
|
|
396
500
|
/**
|
|
397
501
|
* Desktop 卸载(0.9.8,dsh-market 同策略):removeBundle 委派 + ChangeResult 判定 +
|
|
@@ -469,12 +573,112 @@ export async function desktopUpgradeFromRegistry(pkg, cfg = {}, opts = {}, deps
|
|
|
469
573
|
throw err;
|
|
470
574
|
}
|
|
471
575
|
}
|
|
576
|
+
/** 0.10.0 R6:自升级专用通道的目标包(core 缝改道唯一判据;host-api/tools/客户端零特判)。 */
|
|
577
|
+
export const SELF_PKG = 'dsh-m';
|
|
578
|
+
/**
|
|
579
|
+
* Desktop 自升级专用通道(0.10.0,R6/R7/D3):npmLatest 精确版本 → 生效 registry packument 的
|
|
580
|
+
* dist.tarball → 预写 intent → installBundle(tarballUrl, {enabled:true}) 直覆盖——tarball spec
|
|
581
|
+
* 的包名在 pnpm 前不可知,不受宿主 0.2.1 already-installed 守卫约束(本机 spike 实证)。
|
|
582
|
+
* - 恒 panel 语义零外部日志(ADR-0018):intent 预写落独立 ledger(upgrade-intent.ts),不入 external journal;
|
|
583
|
+
* - 同版本 → install-refused no-op(T10);URL 构造失败发生在预写之前 → 零副作用失败(T12);
|
|
584
|
+
* - install 终态失败 → 旧版不受影响(spike 实证)+ intent 收口 failed;
|
|
585
|
+
* - 成功(applied/restart-required/overridden)→ intent 收口 ok + installedVersion;
|
|
586
|
+
* - promise 悬垂(进程死于活交换)→ intent 保持 delegated,由 self-check 对账收口(崩溃兜底网)。
|
|
587
|
+
* 导出层自包 withMutationSession(market.ts 同惯例);core 缝改道发生在宿主 upgrade/install 的
|
|
588
|
+
* session 之内,直接调 Locked 内层避免嵌套。
|
|
589
|
+
*/
|
|
590
|
+
export async function desktopSelfUpgradeViaTarball(pkgName, currentVersion, opts = {}, deps = {}) {
|
|
591
|
+
return withMutationSession(() => desktopSelfUpgradeViaTarballLocked(pkgName, currentVersion, opts, deps));
|
|
592
|
+
}
|
|
593
|
+
async function desktopSelfUpgradeViaTarballLocked(pkgName, currentVersion, opts, deps) {
|
|
594
|
+
const service = await resolveManager(deps);
|
|
595
|
+
if (!service || typeof service.installBundle !== 'function') {
|
|
596
|
+
throw new DesktopOpsError('no-manager', '官方 pluginManager 服务不可用(desktop profile 的自升级必须由官方管理器执行);拒绝自升级(fail-closed)');
|
|
597
|
+
}
|
|
598
|
+
const timeoutMs = opts.timeoutMs ?? 20_000;
|
|
599
|
+
const latest = await (deps.npmLatest ?? defaultNpmLatest)(pkgName, timeoutMs, opts.signal);
|
|
600
|
+
if (latest.version === currentVersion) {
|
|
601
|
+
throw new DesktopOpsError('install-refused', `已是最新版本(${pkgName}@${currentVersion}),无需升级`);
|
|
602
|
+
}
|
|
603
|
+
const spec = await (deps.npmTarballUrl ?? defaultNpmTarballUrl)(pkgName, latest.version, timeoutMs, opts.signal);
|
|
604
|
+
// 预写 intent(崩溃兜底网):URL 构造失败已在此前抛出,零落盘(T12)
|
|
605
|
+
await (deps.writeUpgradeIntent ?? defaultWriteUpgradeIntent)({
|
|
606
|
+
target: pkgName,
|
|
607
|
+
version: latest.version,
|
|
608
|
+
...(currentVersion !== '' ? { fromVersion: currentVersion } : {}),
|
|
609
|
+
at: Date.now(),
|
|
610
|
+
});
|
|
611
|
+
const resolveIntent = deps.resolveUpgradeIntent ?? defaultResolveUpgradeIntent;
|
|
612
|
+
try {
|
|
613
|
+
const change = changeOf(await service.installBundle(spec, { enabled: true }));
|
|
614
|
+
if (isTerminalFailure(change)) {
|
|
615
|
+
const pending = pendingOf(change);
|
|
616
|
+
if (change.application === 'cancelled') {
|
|
617
|
+
throw new DesktopOpsError('install-refused', `自升级已被取消(${pkgName}):当前版本 ${currentVersion} 不受影响`);
|
|
618
|
+
}
|
|
619
|
+
if (change.packageResult?.kind === 'build-blocked' && pending.length > 0) {
|
|
620
|
+
throw new DesktopBuildApprovalNeeded(pending);
|
|
621
|
+
}
|
|
622
|
+
throw new DesktopOpsError('install-refused', `自升级失败(${pkgName}@${latest.version};application=${change.application ?? 'failed'}${change.error?.code ? `、error=${change.error.code}` : ''}):当前版本 ${currentVersion} 不受影响,可稍后重试`);
|
|
623
|
+
}
|
|
624
|
+
const bundleName = change.bundle ?? pkgName;
|
|
625
|
+
if (typeof service.listBundles === 'function') {
|
|
626
|
+
const bundles = await service.listBundles();
|
|
627
|
+
const found = (bundles ?? []).find((b) => b && b.name === bundleName);
|
|
628
|
+
if (!found || found.installed === false) {
|
|
629
|
+
throw new DesktopOpsError('verify-failed', `自升级复读失败(${pkgName}):官方结果为 ${change.application ?? 'unknown'} 但 listBundles 未見目标 bundle 在装——不冒充升级成功`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
await resolveIntent({ ok: true, installedVersion: latest.version });
|
|
633
|
+
return {
|
|
634
|
+
id: pkgName,
|
|
635
|
+
pkg: pkgName,
|
|
636
|
+
spec,
|
|
637
|
+
version: latest.version,
|
|
638
|
+
buildApprovals: Array.isArray(change.approvedBuilds) ? change.approvedBuilds : [],
|
|
639
|
+
fallbackAllBuilds: false,
|
|
640
|
+
// R10 忠实透传宿主 application:restart-required → true;applied/overridden → false
|
|
641
|
+
needsRestart: change.application === 'restart-required',
|
|
642
|
+
output: `official pluginManager (self-upgrade via tarball): application=${change.application ?? 'applied'}, bundle=${bundleName}`,
|
|
643
|
+
via: 'desktop-manager',
|
|
644
|
+
...(change.application === 'overridden' ? { overridden: true } : {}),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
catch (err) {
|
|
648
|
+
// 未竟路径统一收口 intent 为 failed(成功臂已 resolve 时幂等 no-op);
|
|
649
|
+
// 收口失败不遮蔽原始错误——delegated 残留由 self-check 对账兜底。
|
|
650
|
+
try {
|
|
651
|
+
await resolveIntent({ ok: false });
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
/* 对账兜底仍在 */
|
|
655
|
+
}
|
|
656
|
+
throw err;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
472
659
|
async function desktopUpgradeLocked(pkg, cfg, opts, deps) {
|
|
473
660
|
const service = await resolveManager(deps);
|
|
474
661
|
if (!service || typeof service.installBundle !== 'function') {
|
|
475
662
|
throw new DesktopOpsError('no-manager', '官方 pluginManager 服务不可用(desktop profile 的升级必须由官方管理器执行覆盖安装);拒绝升级(fail-closed)');
|
|
476
663
|
}
|
|
477
664
|
const timeoutMs = cfg.timeoutMs ?? 20_000;
|
|
665
|
+
// 0.10.0 R6 自升级通道分治(唯一改道点):desktop 上「升级 dsh-m 自身」一律 D3 tarball 直覆盖——
|
|
666
|
+
// remove+add 对调用者自身是破坏性路径,绝不落入通用两步;host-api `case 'upgrade'`、tools
|
|
667
|
+
// `dshm_upgrade` 与一切经 core 的入口在此自动获得同语义(客户端与工具层零特判)。
|
|
668
|
+
if (pkg === SELF_PKG) {
|
|
669
|
+
const { items } = await (deps.listInstalled ?? defaultListInstalled)(opts.profileDir);
|
|
670
|
+
const selfTarget = items.find((it) => it.pkg === pkg);
|
|
671
|
+
if (!selfTarget)
|
|
672
|
+
throw new DesktopOpsError('install-refused', `desktop profile 未安装该插件: ${pkg}(升级目标必须是已装插件)`);
|
|
673
|
+
return desktopSelfUpgradeViaTarballLocked(pkg, selfTarget.version, { timeoutMs, signal: opts.signal }, {
|
|
674
|
+
getService: () => service,
|
|
675
|
+
npmLatest: deps.npmLatest,
|
|
676
|
+
npmTarballUrl: deps.npmTarballUrl,
|
|
677
|
+
writeUpgradeIntent: deps.writeUpgradeIntent,
|
|
678
|
+
resolveUpgradeIntent: deps.resolveUpgradeIntent,
|
|
679
|
+
listInstalled: deps.listInstalled,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
478
682
|
const loaded = await (deps.loadRegistry ?? defaultLoadRegistry)(cfg, { namespace: opts.namespace ?? 'host', profile: opts.profile });
|
|
479
683
|
if (loaded.status === 'unavailable') {
|
|
480
684
|
throw new DesktopOpsError('install-refused', `收录清单不可用,无法升级 ${pkg};请检查 registry 配置或网络后重试`);
|
|
@@ -594,6 +798,9 @@ async function desktopUpgradeLocked(pkg, cfg, opts, deps) {
|
|
|
594
798
|
throw err;
|
|
595
799
|
}
|
|
596
800
|
const retryNote = managed.governRetryNote !== undefined && !governanceNote.includes(managed.governRetryNote) ? managed.governRetryNote : '';
|
|
801
|
+
const twoStepSuffix = managed.twoStepNote !== undefined ? `;${managed.twoStepNote}` : '';
|
|
802
|
+
const degradedRestoredTo = managed.degraded === true ? managed.restoredTo : undefined;
|
|
803
|
+
const degradedSuffix = degradedRestoredTo !== undefined ? `;升级失败已回装旧版 ${degradedRestoredTo}(降级恢复)` : '';
|
|
597
804
|
// 0.9.22 生效判定:npm 源升级在成功点分类;任何异常 fail-open 到 unknown,绝不影响升级成功态
|
|
598
805
|
let activation;
|
|
599
806
|
let needsRestart = true;
|
|
@@ -619,9 +826,12 @@ async function desktopUpgradeLocked(pkg, cfg, opts, deps) {
|
|
|
619
826
|
...(compat !== null ? { compat } : {}),
|
|
620
827
|
...(compatSkipped ? { compatSkipped } : {}),
|
|
621
828
|
needsRestart,
|
|
622
|
-
output: `official pluginManager: application=${change.application ?? 'applied'}, bundle=${bundleName}${change.changed === false ? ', changed=false' : ''}${governanceNote}${retryNote}${releaseAgeNotice}${registerNote ?? ''}`,
|
|
829
|
+
output: `official pluginManager: application=${change.application ?? 'applied'}, bundle=${bundleName}${change.changed === false ? ', changed=false' : ''}${governanceNote}${retryNote}${releaseAgeNotice}${registerNote ?? ''}${twoStepSuffix}${degradedSuffix}`,
|
|
623
830
|
via: 'desktop-manager',
|
|
624
831
|
...(change.application === 'overridden' ? { overridden: true } : {}),
|
|
832
|
+
...(degradedRestoredTo !== undefined ? { degraded: true, restoredTo: degradedRestoredTo } : {}),
|
|
833
|
+
// 0.10.0(评审 R1-2):降级恢复的 version 落账为回装后实际版本(账实相符);spec 字段保留尝试目标
|
|
834
|
+
...(degradedRestoredTo !== undefined ? { version: degradedRestoredTo } : {}),
|
|
625
835
|
fromVersion: target.version,
|
|
626
836
|
...(activation ? { activation } : {}),
|
|
627
837
|
};
|
|
@@ -40,6 +40,7 @@ import { setLivePluginDisabled } from './live-plugin.js';
|
|
|
40
40
|
import { warmPackumentFor } from './packument.js';
|
|
41
41
|
import { syncNpmmirrorPackage } from './npm-route.js';
|
|
42
42
|
import { clearAllLatestCache } from './latest-cache.js';
|
|
43
|
+
import { parse as yamlParse } from 'yaml';
|
|
43
44
|
import { assertNpmIntegrity, atomicWriteFile, readPnpmLockIntegrity, readPnpmLockOverrides, restoreSnapshots, snapshotFiles, verifySnapshots, } from './npm-integrity.js';
|
|
44
45
|
import { isSafePluginTarget, makeDshRunner, removePatchedDependencyEntries, PNPM_OUTCOME_CODES, } from './dsh-cli.js';
|
|
45
46
|
export const TX_HEAL_CODES = [
|
|
@@ -811,6 +812,25 @@ async function uninstall(req, d, snapshots) {
|
|
|
811
812
|
}
|
|
812
813
|
}
|
|
813
814
|
// ---------- compensate-install 门(M2 Task 3:专用补偿事务,四边界见 DESIGN §3) ----------
|
|
815
|
+
/** 解析 pnpm-workspace.yaml 的 patchedDependencies 键集合(0.9.94 D9)。
|
|
816
|
+
* 文本不可解析(坏 YAML / 重复键)→ null(保守,不抛)——调用方据此走退化或 fail-closed;
|
|
817
|
+
* 空值/缺省(parse 得 null/undefined)与数组形态一律按空集处理(本机实证 parse('') === null;
|
|
818
|
+
* 防 typeof null === 'object' 朴素实现令 Object.keys(null) 抛错走 INTERNAL_ERROR——评审 N5)。 */
|
|
819
|
+
function workspacePatchedKeys(text) {
|
|
820
|
+
let doc;
|
|
821
|
+
try {
|
|
822
|
+
doc = yamlParse(text);
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
if (doc === null || typeof doc !== 'object' || Array.isArray(doc))
|
|
828
|
+
return new Set();
|
|
829
|
+
const patched = doc.patchedDependencies;
|
|
830
|
+
if (patched === null || typeof patched !== 'object' || Array.isArray(patched))
|
|
831
|
+
return new Set();
|
|
832
|
+
return new Set(Object.keys(patched));
|
|
833
|
+
}
|
|
814
834
|
/** patchMapping 结构化双落点恢复:workspace(pnpm-workspace.yaml patchedDependencies)与 manifest(pnpm.patchedDependencies)。 */
|
|
815
835
|
async function restorePatchMapping(profileDir, mapping, heal) {
|
|
816
836
|
const wsFile = join(profileDir, 'pnpm-workspace.yaml');
|
|
@@ -826,9 +846,13 @@ async function restorePatchMapping(profileDir, mapping, heal) {
|
|
|
826
846
|
if (!lines.some((l) => l.trim() === 'patchedDependencies:')) {
|
|
827
847
|
lines.push('patchedDependencies:');
|
|
828
848
|
}
|
|
849
|
+
// 0.9.94 D9:解析去重——同键任意引号形态(pnpm 的单引号/无引号书写)在场即跳过,杜绝对重复键文件的自造;
|
|
850
|
+
// 解析失败(坏 YAML/重复键)退化为今日逐字判重(保守不收紧)
|
|
851
|
+
const existingKeys = workspacePatchedKeys(text);
|
|
829
852
|
for (const entry of mapping.workspace) {
|
|
830
853
|
const line = ` ${JSON.stringify(entry.key)}: ${entry.patchPath}`;
|
|
831
|
-
|
|
854
|
+
const present = existingKeys !== null ? existingKeys.has(entry.key) : lines.includes(line);
|
|
855
|
+
if (!present)
|
|
832
856
|
lines.push(line);
|
|
833
857
|
}
|
|
834
858
|
await atomicWriteFile(wsFile, Buffer.from(lines.join('\n'), 'utf8'));
|
|
@@ -860,8 +884,13 @@ async function verifyPatchMapping(profileDir, mapping) {
|
|
|
860
884
|
if (mapping.workspace.length > 0) {
|
|
861
885
|
try {
|
|
862
886
|
const text = await readFile(join(profileDir, 'pnpm-workspace.yaml'), 'utf8');
|
|
887
|
+
// 0.9.94 D9:yaml 键成员判定替代双引号子串扫描——单引号/无引号在场形态不再被误判「复读缺失」;
|
|
888
|
+
// 解析失败(坏 YAML/重复键)fail-closed 报「复读失败」(与 manifest 支 JSON 解析失败同款纪律)
|
|
889
|
+
const keys = workspacePatchedKeys(text);
|
|
890
|
+
if (keys === null)
|
|
891
|
+
return 'pnpm-workspace.yaml 复读失败:YAML 无法解析';
|
|
863
892
|
for (const entry of mapping.workspace) {
|
|
864
|
-
if (!
|
|
893
|
+
if (!keys.has(entry.key))
|
|
865
894
|
return `workspace 补丁映射复读缺失:${entry.key}`;
|
|
866
895
|
}
|
|
867
896
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自升级 intent ledger(0.10.0,ADR-0018 合规):panel 通道零外部日志 ⇒ 自升级 delegated
|
|
3
|
+
* 预写不进 external journal,落独立 ledger——定位为**崩溃兜底网**:D3 正常路径在本进程内
|
|
4
|
+
* 就地 resolve,仅当进程死于 promise 悬垂(活交换中进程死亡)时由下一次 self-check 对账
|
|
5
|
+
* 收口(host-api 挂载)。记录机器本地、单条覆盖写、不含凭据。
|
|
6
|
+
*
|
|
7
|
+
* - 存储文件:`$DSH_HOME/dshm/self-upgrade-intent.json`(`DSHM_UPGRADE_INTENT_FILE`
|
|
8
|
+
* 可整文件覆盖供测试;与 external-operations.json 同基址、不带 profile 维度)。
|
|
9
|
+
* - write:mkdir recursive 先行 → tmp+rename 原子替换(external-journal 同款惯例)。
|
|
10
|
+
* - read:文件缺失 = undefined;损坏 → 改名 `.corrupt-<ts>` 隔离留证后按空处理
|
|
11
|
+
* (best-effort,隔离失败同样不抛——读路径永不炸)。
|
|
12
|
+
* - resolve:仅收口悬垂(status='delegated')记录并落盘终态;无悬垂返回 undefined(幂等)。
|
|
13
|
+
*/
|
|
14
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { dshHome } from './env.js';
|
|
17
|
+
/** ledger 文件路径:env 整文件覆盖优先(测试隔离),缺省 $DSH_HOME/dshm/self-upgrade-intent.json。 */
|
|
18
|
+
export function upgradeIntentFile() {
|
|
19
|
+
return process.env.DSHM_UPGRADE_INTENT_FILE || join(dshHome(), 'dshm', 'self-upgrade-intent.json');
|
|
20
|
+
}
|
|
21
|
+
/** 显式字段白名单重组(超集字段一律不落盘)。 */
|
|
22
|
+
function normalizeRecord(record) {
|
|
23
|
+
return {
|
|
24
|
+
target: record.target,
|
|
25
|
+
version: record.version,
|
|
26
|
+
...(record.fromVersion !== undefined ? { fromVersion: record.fromVersion } : {}),
|
|
27
|
+
at: record.at,
|
|
28
|
+
status: record.status,
|
|
29
|
+
...(record.outcome !== undefined ? { outcome: record.outcome } : {}),
|
|
30
|
+
...(record.resolvedAt !== undefined ? { resolvedAt: record.resolvedAt } : {}),
|
|
31
|
+
...(record.installedVersion !== undefined ? { installedVersion: record.installedVersion } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function atomicWrite(path, text) {
|
|
35
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
36
|
+
await writeFile(tmp, text);
|
|
37
|
+
await rename(tmp, path);
|
|
38
|
+
}
|
|
39
|
+
/** 损坏文件隔离留证(best-effort,与 external-journal 同款两级重试;失败则放弃隔离按空继续)。 */
|
|
40
|
+
async function quarantine(file) {
|
|
41
|
+
try {
|
|
42
|
+
await rename(file, `${file}.corrupt-${Date.now()}`);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
try {
|
|
46
|
+
// 同毫秒重入等极小概率命名冲突:换一个带随机尾缀的名字再试一次;仍败则放弃隔离
|
|
47
|
+
await rename(file, `${file}.corrupt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* 放弃隔离:按空继续 */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** 读当前记录;文件缺失 → undefined;损坏/形态非法 → 隔离后按空处理(不抛)。 */
|
|
55
|
+
export async function readUpgradeIntent() {
|
|
56
|
+
const file = upgradeIntentFile();
|
|
57
|
+
let text;
|
|
58
|
+
try {
|
|
59
|
+
text = await readFile(file, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err.code === 'ENOENT')
|
|
63
|
+
return undefined;
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(text);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
await quarantine(file);
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
75
|
+
await quarantine(file);
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return normalizeRecord(parsed);
|
|
79
|
+
}
|
|
80
|
+
/** 预写一条 delegated 意图(status 由 writer 盖章;调用方不传 status/outcome 等终态字段)。 */
|
|
81
|
+
export async function writeUpgradeIntent(input) {
|
|
82
|
+
const file = upgradeIntentFile();
|
|
83
|
+
await mkdir(dirname(file), { recursive: true });
|
|
84
|
+
const record = normalizeRecord({ ...input, status: 'delegated' });
|
|
85
|
+
await atomicWrite(file, `${JSON.stringify(record, null, 2)}\n`);
|
|
86
|
+
return record;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 收口悬垂记录:status → resolved、outcome 按 ok 盖章、resolvedAt 就地取值、
|
|
90
|
+
* installedVersion 可选回填;落盘后返回被收口的记录。无悬垂(缺文件/已 resolved)→
|
|
91
|
+
* undefined(幂等,不冒充收口)。
|
|
92
|
+
*/
|
|
93
|
+
export async function resolveUpgradeIntent(input) {
|
|
94
|
+
const current = await readUpgradeIntent();
|
|
95
|
+
if (!current || current.status !== 'delegated')
|
|
96
|
+
return undefined;
|
|
97
|
+
const resolved = normalizeRecord({
|
|
98
|
+
...current,
|
|
99
|
+
status: 'resolved',
|
|
100
|
+
outcome: input.ok ? 'ok' : 'failed',
|
|
101
|
+
resolvedAt: Date.now(),
|
|
102
|
+
...(input.installedVersion !== undefined ? { installedVersion: input.installedVersion } : {}),
|
|
103
|
+
});
|
|
104
|
+
const file = upgradeIntentFile();
|
|
105
|
+
await mkdir(dirname(file), { recursive: true });
|
|
106
|
+
await atomicWrite(file, `${JSON.stringify(resolved, null, 2)}\n`);
|
|
107
|
+
return resolved;
|
|
108
|
+
}
|
package/lib/core/versions.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { gt } from 'semver';
|
|
12
12
|
import { fetchJsonLimited, HttpError } from './httpx.js';
|
|
13
13
|
import { isExactVersion } from './packument.js';
|
|
14
|
-
export { npmVersion, npmPackument, npmPackumentReadme, npmLatest, extractGithubRepo, isExactVersion, } from './packument.js';
|
|
14
|
+
export { npmVersion, npmTarballUrl, npmPackument, npmPackumentReadme, npmLatest, extractGithubRepo, isExactVersion, } from './packument.js';
|
|
15
15
|
/** GitHub 匿名限额(60 次/小时/IP)用尽时返回可读提示(含重置等待分钟数),否则 null。 */
|
|
16
16
|
function githubRateLimitMessage(err) {
|
|
17
17
|
if (err instanceof HttpError && err.status === 403 && err.headers?.get('x-ratelimit-remaining') === '0') {
|