dsh-m 0.9.92 → 0.9.94
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/cli.js +3 -3
- package/lib/client.js +72 -80
- package/lib/core/host-api.js +3 -7
- package/lib/core/market.js +15 -0
- package/lib/core/profile-transaction.js +31 -2
- package/lib/tools.js +2 -2
- 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.9.94 重构+修复:变更结局描述客户端单源化(op-outcome)+ 补丁映射 YAML 引号宽容与重复键根治
|
|
12
|
+
|
|
13
|
+
- **需求(2026-10-17 架构巡查 + grill 九决议)**:变更结局的呈现无 owning module——守卫拦截文案装配 main.jsx 三处逐字重复(0.9.93 toWire 收敛了线格式、client 侧呈现是已知残尾,`repairBasis` 后加时的多点位手补复发已实证)、升级成功文案两处逐字、builds 后缀四处拼装、superseded 五处;另:事务补偿对 workspace `patchedDependencies` **非双引号在场形态**(pnpm 的单引号/无引号书写)逐字判重失明——逐字判重可自造重复键文件、复读检查可假 committed(同包链经 strip 先摘在生产不可达,本项属防御面收敛与 fail-closed 补洞)。
|
|
14
|
+
- **方案**:新纯模块 `src/client/op-outcome.js`(GLOSSARY「变更结局描述」入册)——工厂 `createOpOutcome(lookup)` 出成品文案(`createMarkdown(h)` 同款注入先例),`guardBlockedText`/`buildsSuffix`/`upgradeOkText`/`mutationFailed` 四函数;六处接线站点(五类投影)一行接线,流程特有分支(兼容弹窗/能力表拒绝/乐观提示)与 ToolCardRow 短版留守注记。D9:`verifyPatchMapping` workspace 支改 yaml 键成员判定(解析失败 fail-closed 报「复读失败」);`restorePatchMapping` 追加前解析去重(同键任意引号形态在场即跳过,解析失败退化逐字判重)。
|
|
15
|
+
- **验证**:全量 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`。
|
|
16
|
+
|
|
17
|
+
### 0.9.93 重构:装后守卫线格式收敛至 InstallGuardError.toWire——加守卫字段编辑点 8 → 2(纯重构,线格式语义零变化)
|
|
18
|
+
|
|
19
|
+
- **需求(2026-10-16 架构巡查摩擦点 3,巡查首推)**:`InstallGuardError` → 线格式的投影在手写 5 处逐字重复(host-api 409 payload / tools install+upgrade 两臂 / cli install+upgrade 两臂)+ client 再拼一次——加一个守卫字段要碰 8 处(类 + 5 投影 + client 读取 + 测试),`repairBasis` 后加时各点手补已实证过一次复发。
|
|
20
|
+
- **方案**:产地方法 `toWire(): InstallGuardWire`(随类导出接口 + `GuardViolation` re-export)成为 6 字段共享线格式单一事实源;五处投影改一行展开(信封差异留在各端:host-api `ok/error`、tools `ok/guard/message`、cli 裸 JSON),`repairBasis` undefined 时键不存在的语义移入方法本体。加字段编辑点降为 2(类+toWire 一处、client 读取一处——浏览器边界不能 import core,防御性拼装留守,如实注记)。自觉差异注记:tools 信封键序 kind/message 交换(JSON 键序对消费方无语义,全 tests/ 无原始 JSON 键序钉)。
|
|
21
|
+
- **验证**:全量 1530 测试全绿(新增 toWire 三钉例:六字段齐/repairBasis 键缺席/键序;新增 host-api dispatcher 409 钉例补 host-api 投影零测试覆盖的缺口;tools 两臂 execute 钉例零迁移)、typecheck 零错误;收敛锚 `repairBasis ? { repairBasis: … }` 三文件 5 → 0(兼证无第 6 处投影);实施计划经两轮独立评审四项收敛 + 执行评审两轮(含 A1 钉例独立成 describe)记录在 `docs/plans/2026-10-17-install-guard-towire-implementation-plan.md`。
|
|
22
|
+
|
|
11
23
|
### 0.9.92 修复+重构:三端市场查询文法统一(market-query 单一入口)——CLI 非法 category 吞错修复
|
|
12
24
|
|
|
13
25
|
- **需求(2026-10-16 实测)**:`dshm search --category <非法>` 静默返回「没有匹配的收录条目」+ exit 0,而同输入 GUI 得 400、agent 工具显式报错——「合并市场」词条承诺的「三端同源同语义」被 CLI 打破;根因是分类 slug 正则、source/offset/limit 解析、精选标签表在三端各持一份拷贝(正则 ×3、解析 ×3、标签表 ×2、deadline 常量 ×2),CLI 的拷贝漏了 category 校验。
|
|
@@ -649,6 +661,18 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
649
661
|
|
|
650
662
|
## English
|
|
651
663
|
|
|
664
|
+
### Refactor + fix in 0.9.94 — client-side mutation-outcome copy single-sourced (op-outcome) + patch-mapping YAML quote tolerance and duplicate-key elimination
|
|
665
|
+
|
|
666
|
+
- **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).
|
|
667
|
+
- **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).
|
|
668
|
+
- **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`.
|
|
669
|
+
|
|
670
|
+
### 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)
|
|
671
|
+
|
|
672
|
+
- **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.
|
|
673
|
+
- **Approach**: a source method `toWire(): InstallGuardWire` (interface exported alongside the class, plus a `GuardViolation` re-export) becomes the single source of the shared 6-field wire format; the five projections become one-line spreads (envelope differences stay per-end: host-api `ok/error`, tools `ok/guard/message`, cli bare JSON), and the "repairBasis key absent when undefined" semantics move into the method body. Adding a field now touches 2 points (class+toWire, and the client read — the browser bundle cannot import core modules, so its defensive assembly stays, honestly noted). Deliberate delta: the tools envelope swaps key order between kind/message (JSON key order carries no consumer semantics; no raw-JSON key-order pins exist across tests/).
|
|
674
|
+
- **Verification**: full suite 1530 green (three new toWire pins: six fields / repairBasis key absence / key order; a new host-api dispatcher 409 pin closing the "host-api projection had zero test coverage" gap; tools-arm execute pins migrated zero), typecheck clean; the convergence anchor `repairBasis ? { repairBasis: … }` went 5 → 0 across the three files (also proving there is no 6th projection); plan review (two rounds, four findings) and execution review (two rounds, including A1 extracting the pin into its own describe) recorded in `docs/plans/2026-10-17-install-guard-towire-implementation-plan.md`.
|
|
675
|
+
|
|
652
676
|
### Fix + refactor in 0.9.92 — unified three-end market query intake (market-query single entry); CLI invalid-category silent swallow fixed
|
|
653
677
|
|
|
654
678
|
- **Need (2026-10-16, reproduced live)**: `dshm search --category <invalid>` silently printed "no matching entries" and exited 0, while the same input gets a 400 from the panel API and an explicit error from the agent tool — breaking the "same source, same semantics across GUI / tools / CLI" promise. Root cause: the category slug regex, source/offset/limit parsing, and the curated label map each had per-end copies (regex ×3, parsing ×3, labels ×2, deadline constants ×2), and the CLI copy lacked category validation.
|
package/lib/cli.js
CHANGED
|
@@ -193,7 +193,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
193
193
|
// metadata-only:不构造全量 latest;0.9.92 三端查询文法:category/source/limit/offset 经
|
|
194
194
|
// market-query 单一入口归一(与 host-api/工具同源同语义);文法违规 throw 直飞 dispatch 统一
|
|
195
195
|
// catch(`错误:${msg}` + exit 1,与此前 source 内联报错输出逐字节同款)——非法 category
|
|
196
|
-
//
|
|
196
|
+
// 自此显式报错(≤0.9.91 静默空结果吞错,回归钉例在 cli-search.test.mjs)。
|
|
197
197
|
// 双源判定:主清单 unavailable 但社区有条目 → 照常出页(Q42);两层皆不可用 → exit 1
|
|
198
198
|
const intake = parseMarketQueryInput({ category: flags.category, source: flags.source, limit: flags.limit, offset: flags.offset }, { limitMax: 80, limitDefault: 10 });
|
|
199
199
|
const result = await d.listMarket(cfg, {
|
|
@@ -332,7 +332,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
332
332
|
}
|
|
333
333
|
catch (e) {
|
|
334
334
|
if (e instanceof InstallGuardError) {
|
|
335
|
-
err('[⛔ 装后守卫拦截] ' + JSON.stringify(
|
|
335
|
+
err('[⛔ 装后守卫拦截] ' + JSON.stringify(e.toWire()));
|
|
336
336
|
err(e.message);
|
|
337
337
|
err(e.restartSafe ? '可以重启 DSH Web:dshm restart --yes' : '修复后再重启(不要现在重启)。');
|
|
338
338
|
return 1;
|
|
@@ -357,7 +357,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
357
357
|
}
|
|
358
358
|
catch (e) {
|
|
359
359
|
if (e instanceof InstallGuardError) {
|
|
360
|
-
err('[⛔ 装后守卫拦截] ' + JSON.stringify(
|
|
360
|
+
err('[⛔ 装后守卫拦截] ' + JSON.stringify(e.toWire()));
|
|
361
361
|
err(e.message);
|
|
362
362
|
err(e.restartSafe ? '可以重启 DSH Web:dshm restart --yes' : '修复后再重启(不要现在重启)。');
|
|
363
363
|
return 1;
|
package/lib/client.js
CHANGED
|
@@ -2005,6 +2005,53 @@ var init_install_cmd = __esm({
|
|
|
2005
2005
|
}
|
|
2006
2006
|
});
|
|
2007
2007
|
|
|
2008
|
+
// src/client/op-outcome.js
|
|
2009
|
+
var op_outcome_exports = {};
|
|
2010
|
+
__export(op_outcome_exports, {
|
|
2011
|
+
createOpOutcome: () => createOpOutcome
|
|
2012
|
+
});
|
|
2013
|
+
function createOpOutcome(lookup2) {
|
|
2014
|
+
const guardBlockedText = (guard) => [
|
|
2015
|
+
lookup2("guard.blocked"),
|
|
2016
|
+
`${lookup2("guard.compstatus")}: ${guard.compensation?.status || "\u2014"}\uFF08${guard.compensation?.note || ""}\uFF09`,
|
|
2017
|
+
guard.repairBasis ? `${lookup2("guard.repairbasis")}: ${guard.repairBasis}` : null,
|
|
2018
|
+
lookup2("guard.noforce"),
|
|
2019
|
+
guard.restartSafe ? lookup2("guard.restartsafenow") : lookup2("guard.restartunsafe")
|
|
2020
|
+
].filter(Boolean).join(" | ");
|
|
2021
|
+
const buildsSuffix = (res) => res.buildApprovals && res.buildApprovals.length ? lookup2("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup2("notify.builds.fallback") : "";
|
|
2022
|
+
const upgradeOkText = (res) => {
|
|
2023
|
+
const note = upgradeNotify(res.activation);
|
|
2024
|
+
return {
|
|
2025
|
+
needsRestart: note.needsRestart,
|
|
2026
|
+
text: lookup2("notify.upgraded", {
|
|
2027
|
+
pkg: res.pkg,
|
|
2028
|
+
from: res.fromVersion ? `v${res.fromVersion}` : "\u2014",
|
|
2029
|
+
to: res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : "latest"
|
|
2030
|
+
}) + buildsSuffix(res) + (note.suffixKey ? lookup2(note.suffixKey) : "")
|
|
2031
|
+
};
|
|
2032
|
+
};
|
|
2033
|
+
const mutationFailed = (e, { verb, target }) => {
|
|
2034
|
+
if (e && e.opSuperseded) return { kind: "ok", text: lookup2("op.superseded.note", { target }) };
|
|
2035
|
+
if (e && e.guard) return { kind: "err", text: guardBlockedText(e.guard) };
|
|
2036
|
+
return { kind: "err", text: lookup2(PLAIN_FAIL_KEY[verb], { err: e && e.message || e }) };
|
|
2037
|
+
};
|
|
2038
|
+
return { guardBlockedText, buildsSuffix, upgradeOkText, mutationFailed };
|
|
2039
|
+
}
|
|
2040
|
+
var PLAIN_FAIL_KEY;
|
|
2041
|
+
var init_op_outcome = __esm({
|
|
2042
|
+
"src/client/op-outcome.js"() {
|
|
2043
|
+
"use strict";
|
|
2044
|
+
init_operations();
|
|
2045
|
+
PLAIN_FAIL_KEY = {
|
|
2046
|
+
install: "failed.install",
|
|
2047
|
+
upgrade: "failed.upgrade",
|
|
2048
|
+
uninstall: "failed.uninstall",
|
|
2049
|
+
toggle: "toggle.failed",
|
|
2050
|
+
self: "notify.selfupgraded.failed"
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2008
2055
|
// src/client/main.jsx
|
|
2009
2056
|
var React = require("react");
|
|
2010
2057
|
var rd = require("react-dom");
|
|
@@ -2028,11 +2075,12 @@ var { pickPayload: pickPayload2, parseToolArgs: parseToolArgs2 } = (init_tool_vi
|
|
|
2028
2075
|
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
2076
|
var { refreshAfterMutation: refreshAfterMutation2 } = (init_view_refresh(), __toCommonJS(view_refresh_exports));
|
|
2030
2077
|
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,
|
|
2078
|
+
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
2079
|
var { groupOpsByDay: groupOpsByDay2, opRowVm: opRowVm2, opUpgradeMeta: opUpgradeMeta2, canRemoveRow: canRemoveRow2, channelLabelKey: channelLabelKey2 } = (init_operations_view(), __toCommonJS(operations_view_exports));
|
|
2033
2080
|
var { createFavoritesStore: createFavoritesStore2, partitionStale: partitionStale2 } = (init_favorites(), __toCommonJS(favorites_exports));
|
|
2034
2081
|
var { readSelfCheckCache: readSelfCheckCache2, writeSelfCheckCache: writeSelfCheckCache2, clearSelfCheckCache: clearSelfCheckCache2, deriveChipState: deriveChipState2 } = (init_self_check(), __toCommonJS(self_check_exports));
|
|
2035
2082
|
var { shouldShowInstallCmd: shouldShowInstallCmd2 } = (init_install_cmd(), __toCommonJS(install_cmd_exports));
|
|
2083
|
+
var { createOpOutcome: createOpOutcome2 } = (init_op_outcome(), __toCommonJS(op_outcome_exports));
|
|
2036
2084
|
var ZH = {
|
|
2037
2085
|
"market.title": "\u63D2\u4EF6\u5E02\u573A",
|
|
2038
2086
|
"tab.market": "\u5E02\u573A",
|
|
@@ -2645,6 +2693,7 @@ function lookup(key, params) {
|
|
|
2645
2693
|
const dict = browserLang() === "en" ? EN : ZH;
|
|
2646
2694
|
return interpolate(dict[key] ?? ZH[key] ?? key, params);
|
|
2647
2695
|
}
|
|
2696
|
+
var oc = createOpOutcome2(lookup);
|
|
2648
2697
|
function communityLabels(data) {
|
|
2649
2698
|
const c = data && data.community && typeof data.community === "object" ? data.community : {};
|
|
2650
2699
|
const zh = c.categoryLabels && typeof c.categoryLabels === "object" ? c.categoryLabels : {};
|
|
@@ -4079,7 +4128,7 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, o
|
|
|
4079
4128
|
h(SearchBox, { key: zone, placeholder: lookup("search.ph"), initial: query.query, onCommit: (v) => updateQuery(v ? { query: v, category: null } : { query: v }) })
|
|
4080
4129
|
) : null;
|
|
4081
4130
|
const zoneBar = zoneChips3;
|
|
4082
|
-
const installResultText = (res) => lookup("notify.installed", { pkg: res.pkg, version: res.version ? ` v${res.version}` : "" }) +
|
|
4131
|
+
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
4132
|
const installDone = (it2, res) => {
|
|
4084
4133
|
notify({ kind: "ok", needsRestart: true, text: installResultText(res) });
|
|
4085
4134
|
setInstallNote({ id: it2.id, kind: "ok", text: installResultText(res) });
|
|
@@ -4099,25 +4148,12 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, o
|
|
|
4099
4148
|
installDone(it, res);
|
|
4100
4149
|
await (onMutation ? onMutation() : reload(false));
|
|
4101
4150
|
} 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) {
|
|
4151
|
+
if (e && e.issue) {
|
|
4116
4152
|
setCompatConfirm({ it, version, issue: e.issue, opId: e.opId });
|
|
4117
4153
|
} else {
|
|
4118
|
-
const
|
|
4119
|
-
notify({ kind:
|
|
4120
|
-
setInstallNote({ id: it.id, kind: "err", text:
|
|
4154
|
+
const m = oc.mutationFailed(e, { verb: "install", target: it.name });
|
|
4155
|
+
notify({ kind: m.kind, text: m.text });
|
|
4156
|
+
setInstallNote({ id: it.id, kind: m.kind === "ok" ? "hint" : "err", text: m.text });
|
|
4121
4157
|
}
|
|
4122
4158
|
}
|
|
4123
4159
|
};
|
|
@@ -4128,31 +4164,14 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, o
|
|
|
4128
4164
|
const m = opUpgradeMeta2(r);
|
|
4129
4165
|
return m ? { ...r, opMeta: m } : r;
|
|
4130
4166
|
});
|
|
4131
|
-
const
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
setInstallNote({ id: it.id, kind: "ok", text });
|
|
4167
|
+
const ok = oc.upgradeOkText(res);
|
|
4168
|
+
notify({ kind: "ok", needsRestart: ok.needsRestart, text: ok.text });
|
|
4169
|
+
setInstallNote({ id: it.id, kind: "ok", text: ok.text });
|
|
4135
4170
|
await (onMutation ? onMutation() : void 0);
|
|
4136
4171
|
} catch (e) {
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
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 });
|
|
4151
|
-
} else {
|
|
4152
|
-
const failText = lookup("failed.upgrade", { err: e && e.message || e });
|
|
4153
|
-
notify({ kind: "err", text: failText });
|
|
4154
|
-
setInstallNote({ id: it.id, kind: "err", text: failText });
|
|
4155
|
-
}
|
|
4172
|
+
const m = oc.mutationFailed(e, { verb: "upgrade", target: it.installedPkg });
|
|
4173
|
+
notify({ kind: m.kind, text: m.text });
|
|
4174
|
+
setInstallNote({ id: it.id, kind: m.kind === "ok" ? "hint" : "err", text: m.text });
|
|
4156
4175
|
}
|
|
4157
4176
|
};
|
|
4158
4177
|
const CompatDialog = compatConfirm ? h(
|
|
@@ -4510,11 +4529,8 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
|
|
|
4510
4529
|
notify({ kind: "ok", needsRestart: note.needsRestart, text: lookup(note.textKey, note.params) + extra });
|
|
4511
4530
|
await reload();
|
|
4512
4531
|
} catch (e) {
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
} else {
|
|
4516
|
-
notify({ kind: "err", text: lookup("toggle.failed", { err: e && e.message || e }) });
|
|
4517
|
-
}
|
|
4532
|
+
const m = oc.mutationFailed(e, { verb: "toggle", target: it.pkg });
|
|
4533
|
+
notify({ kind: m.kind, text: m.text });
|
|
4518
4534
|
} finally {
|
|
4519
4535
|
setPendingToggles((m) => {
|
|
4520
4536
|
if (!(it.pkg in m)) return m;
|
|
@@ -4533,11 +4549,8 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
|
|
|
4533
4549
|
});
|
|
4534
4550
|
await (onMutation ? onMutation() : reload());
|
|
4535
4551
|
} catch (e) {
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
} else {
|
|
4539
|
-
notify({ kind: "err", text: lookup("failed.uninstall", { err: e && e.message || e }) });
|
|
4540
|
-
}
|
|
4552
|
+
const m = oc.mutationFailed(e, { verb: "uninstall", target: it.pkg });
|
|
4553
|
+
notify({ kind: m.kind, text: m.text });
|
|
4541
4554
|
}
|
|
4542
4555
|
};
|
|
4543
4556
|
const doUpgrade = async (it) => {
|
|
@@ -4547,34 +4560,12 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
|
|
|
4547
4560
|
const m = opUpgradeMeta2(r);
|
|
4548
4561
|
return m ? { ...r, opMeta: m } : r;
|
|
4549
4562
|
});
|
|
4550
|
-
const
|
|
4551
|
-
notify({
|
|
4552
|
-
kind: "ok",
|
|
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
|
-
});
|
|
4563
|
+
const ok = oc.upgradeOkText(res);
|
|
4564
|
+
notify({ kind: "ok", needsRestart: ok.needsRestart, text: ok.text });
|
|
4560
4565
|
await (onMutation ? onMutation() : reload());
|
|
4561
4566
|
} catch (e) {
|
|
4562
|
-
|
|
4563
|
-
|
|
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 }) });
|
|
4575
|
-
} else {
|
|
4576
|
-
notify({ kind: "err", text: lookup("failed.upgrade", { err: e && e.message || e }) });
|
|
4577
|
-
}
|
|
4567
|
+
const m = oc.mutationFailed(e, { verb: "upgrade", target: it.pkg });
|
|
4568
|
+
notify({ kind: m.kind, text: m.text });
|
|
4578
4569
|
}
|
|
4579
4570
|
};
|
|
4580
4571
|
if (loading && !data) return h("div", { className: "dshm-empty" }, lookup("installed.loading"), Spin());
|
|
@@ -5211,13 +5202,14 @@ function DshmVersionChip({ version, notify, initialCheck, ops }) {
|
|
|
5211
5202
|
notify({
|
|
5212
5203
|
kind: "ok",
|
|
5213
5204
|
needsRestart: true,
|
|
5214
|
-
text: lookup("notify.selfupgraded", { from: version, to: res.version || "latest" }) +
|
|
5205
|
+
text: lookup("notify.selfupgraded", { from: version, to: res.version || "latest" }) + oc.buildsSuffix(res)
|
|
5215
5206
|
});
|
|
5216
5207
|
} catch (e) {
|
|
5217
5208
|
if (e && e.unsupported) {
|
|
5218
5209
|
notify({ kind: "info", text: e.unsupported.guidance || e && e.message || String(e) });
|
|
5219
5210
|
} else {
|
|
5220
|
-
|
|
5211
|
+
const m = oc.mutationFailed(e, { verb: "self", target: "dsh-m" });
|
|
5212
|
+
notify({ kind: m.kind, text: m.text });
|
|
5221
5213
|
}
|
|
5222
5214
|
} finally {
|
|
5223
5215
|
setBusy(false);
|
package/lib/core/host-api.js
CHANGED
|
@@ -166,18 +166,14 @@ function errorStatus(err) {
|
|
|
166
166
|
return { status, payload: { ok: false, error: err.message, code: err.code } };
|
|
167
167
|
}
|
|
168
168
|
if (err instanceof InstallGuardError) {
|
|
169
|
-
// 装后守卫拦截(M2 Task 3):结构化投影含 needsRestart/restartSafe/kind——GUI 一键重启只读 restartSafe
|
|
169
|
+
// 装后守卫拦截(M2 Task 3):结构化投影含 needsRestart/restartSafe/kind——GUI 一键重启只读 restartSafe;
|
|
170
|
+
// 0.9.93 起线格式单源于 toWire(信封 ok/error 本端自拼)
|
|
170
171
|
return {
|
|
171
172
|
status: 409,
|
|
172
173
|
payload: {
|
|
173
174
|
ok: false,
|
|
174
175
|
error: err.message,
|
|
175
|
-
|
|
176
|
-
violations: err.violations,
|
|
177
|
-
compensation: err.compensation,
|
|
178
|
-
needsRestart: err.needsRestart,
|
|
179
|
-
restartSafe: err.restartSafe,
|
|
180
|
-
...(err.repairBasis ? { repairBasis: err.repairBasis } : {}),
|
|
176
|
+
...err.toWire(),
|
|
181
177
|
},
|
|
182
178
|
};
|
|
183
179
|
}
|
package/lib/core/market.js
CHANGED
|
@@ -822,6 +822,21 @@ export class InstallGuardError extends Error {
|
|
|
822
822
|
this.compensation = fields.compensation;
|
|
823
823
|
this.repairBasis = fields.repairBasis;
|
|
824
824
|
}
|
|
825
|
+
/**
|
|
826
|
+
* 线格式单一事实源(0.9.93):三端共享的 6 字段守卫投影。各端信封自拼(host-api 409 payload 的
|
|
827
|
+
* ok/error、tools 的 ok/guard/message、cli 的裸 JSON),共享部分一律经此方法——加守卫字段只改
|
|
828
|
+
* 本方法 + client 读取一处。repairBasis undefined 时键不存在(今日 5 处投影同语义)。
|
|
829
|
+
*/
|
|
830
|
+
toWire() {
|
|
831
|
+
return {
|
|
832
|
+
kind: this.kind,
|
|
833
|
+
violations: this.violations,
|
|
834
|
+
compensation: this.compensation,
|
|
835
|
+
needsRestart: this.needsRestart,
|
|
836
|
+
restartSafe: this.restartSafe,
|
|
837
|
+
...(this.repairBasis ? { repairBasis: this.repairBasis } : {}),
|
|
838
|
+
};
|
|
839
|
+
}
|
|
825
840
|
}
|
|
826
841
|
/** lockfile packages 段的目标包 resolution 提取(行扫描:键行 `pkg@ver:` → 收集至下一键行;引号/嵌套键兼容)。 */
|
|
827
842
|
function lockResolutionOf(lockText, pkg) {
|
|
@@ -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
|
}
|
package/lib/tools.js
CHANGED
|
@@ -241,7 +241,7 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
|
|
|
241
241
|
catch (err) {
|
|
242
242
|
if (err instanceof InstallGuardError) {
|
|
243
243
|
// 结构化 error result(M2 Task 3 ㉑-㉜):不渲染「已安装」,无 force 通道;restartSafe 随结果携带
|
|
244
|
-
return { ok: false, guard: true,
|
|
244
|
+
return { ok: false, guard: true, message: err.message, ...err.toWire() };
|
|
245
245
|
}
|
|
246
246
|
throw err;
|
|
247
247
|
}
|
|
@@ -403,7 +403,7 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
|
|
|
403
403
|
}
|
|
404
404
|
catch (err) {
|
|
405
405
|
if (err instanceof InstallGuardError) {
|
|
406
|
-
return { ok: false, guard: true,
|
|
406
|
+
return { ok: false, guard: true, message: err.message, ...err.toWire() };
|
|
407
407
|
}
|
|
408
408
|
throw err;
|
|
409
409
|
}
|