dsh-plugin-manager-companion 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +144 -0
  3. package/README.md +142 -0
  4. package/cordis.patch.yml +9 -0
  5. package/dist/about.d.ts +77 -0
  6. package/dist/about.js +179 -0
  7. package/dist/cli.d.ts +226 -0
  8. package/dist/cli.js +856 -0
  9. package/dist/client/AboutPage.d.ts +75 -0
  10. package/dist/client/ConsolePage.d.ts +79 -0
  11. package/dist/client/KindsPage.d.ts +21 -0
  12. package/dist/client/MarketplacePage.d.ts +36 -0
  13. package/dist/client/OfficialSlots.d.ts +35 -0
  14. package/dist/client/UpgradeRow.d.ts +108 -0
  15. package/dist/client/index.d.ts +26 -0
  16. package/dist/client/locales.d.ts +475 -0
  17. package/dist/client/pmSelect.d.ts +38 -0
  18. package/dist/client/shared.d.ts +928 -0
  19. package/dist/client/upgradeView.d.ts +278 -0
  20. package/dist/client/wire.d.ts +401 -0
  21. package/dist/client.js +9194 -0
  22. package/dist/diagnostics.d.ts +332 -0
  23. package/dist/diagnostics.js +2631 -0
  24. package/dist/envManager.d.ts +1047 -0
  25. package/dist/envManager.js +3214 -0
  26. package/dist/fix.d.ts +60 -0
  27. package/dist/fix.js +168 -0
  28. package/dist/guard.d.ts +133 -0
  29. package/dist/guard.js +232 -0
  30. package/dist/index.d.ts +121 -0
  31. package/dist/index.js +1150 -0
  32. package/dist/installSession.d.ts +111 -0
  33. package/dist/installSession.js +150 -0
  34. package/dist/kinds.d.ts +464 -0
  35. package/dist/kinds.js +1029 -0
  36. package/dist/marketView.d.ts +261 -0
  37. package/dist/marketView.js +406 -0
  38. package/dist/marketplace.d.ts +248 -0
  39. package/dist/marketplace.js +500 -0
  40. package/dist/match.d.ts +67 -0
  41. package/dist/match.js +203 -0
  42. package/dist/net.d.ts +108 -0
  43. package/dist/net.js +163 -0
  44. package/dist/official.d.ts +145 -0
  45. package/dist/official.js +205 -0
  46. package/dist/paths.d.ts +108 -0
  47. package/dist/paths.js +236 -0
  48. package/dist/presets.d.ts +299 -0
  49. package/dist/presets.js +578 -0
  50. package/dist/qualityGate.d.ts +66 -0
  51. package/dist/qualityGate.js +247 -0
  52. package/dist/rank.d.ts +88 -0
  53. package/dist/rank.js +164 -0
  54. package/dist/registry.d.ts +295 -0
  55. package/dist/registry.js +686 -0
  56. package/dist/rest.d.ts +122 -0
  57. package/dist/rest.js +219 -0
  58. package/dist/scan.d.ts +134 -0
  59. package/dist/scan.js +396 -0
  60. package/dist/settings.d.ts +447 -0
  61. package/dist/settings.js +263 -0
  62. package/dist/tags.d.ts +119 -0
  63. package/dist/tags.js +166 -0
  64. package/dist/tools.d.ts +131 -0
  65. package/dist/tools.js +377 -0
  66. package/dist/types.d.ts +651 -0
  67. package/dist/types.js +13 -0
  68. package/dist/upgrade.d.ts +428 -0
  69. package/dist/upgrade.js +1100 -0
  70. package/dist/upgradeView.d.ts +313 -0
  71. package/dist/upgradeView.js +273 -0
  72. package/docs/images/readme/01-console-health.png +0 -0
  73. package/docs/images/readme/02-console-envs.png +0 -0
  74. package/docs/images/readme/03-marketplace.png +0 -0
  75. package/docs/images/readme/04-official-plugin-page.png +0 -0
  76. package/package.json +104 -0
@@ -0,0 +1,313 @@
1
+ /**
2
+ * upgradeView.ts — 升级的**纯决策**:四态怎么显示、注册面收哪些包、结果怎么读。
3
+ *
4
+ * 归属:A 类·重写(新模块,无旧实现可参考:旧仓库的"升级"只是一句提示文案)。
5
+ * 旧实现参考:无(旧 dsh-web-plugin-manager 没有升级功能;其市场页只有"可更新 x.y.z"徽标)。
6
+ * 官方复用:无。本模块**不认识 React、不认识 ctx、不联网、不读盘**——它是可独立测试的
7
+ * 决策层(与 marketView.ts / tags.ts 同一方法论:把判定抽出来,组件只负责画)。
8
+ * 前提检查:四态(update-available / up-to-date / unknown / not-upgradable)由 host 的
9
+ * upgradeCheck op 直接给出(docs/REST-CONTRACT.md「UpgradeCheckResult.units[].state 四态」),
10
+ * 界面**直接渲染,不自己推断**。本模块只回答三个界面问题:
11
+ * ① 这一行该不该画、画什么;② 官方插件页的 keyed 注册面该注册哪些包名;③ 一次升级结果
12
+ * 该被读成成功/失败/回滚/没验证。
13
+ *
14
+ * 为什么必须有这一层(而不是把判断写进组件):
15
+ * · "靠缺席传达状态"是本仓库反复踩过的坑(DESIGN §5.5 / §12.3.3)。四态里 up-to-date
16
+ * 与 unknown 都不画升级入口,但它们的**事实完全不同**:前者是"查过,没有更新",
17
+ * 后者是"查不到"。把两者的可见性判定收进一个纯函数,测试才能逐个钉住
18
+ * "unknown 绝不显示成已是最新"。
19
+ * · 注册对账(§5.5:不留孤儿 key)需要一个可断言的集合运算,而不是散在 apply 里的副作用。
20
+ */
21
+ import type { UpgradeState } from './types.ts';
22
+ /**
23
+ * 本模块读的单元形状(host 的 UpgradeUnitReport 是它的超集,wire 的 UpgradeUnitView 也是)。
24
+ *
25
+ * 刻意用结构类型而不是 import 契约类型:判定只看这几个字段,
26
+ * 于是同一份函数既能吃 host 的原始报告(host 侧测试)也能吃归一后的视图(客户端)。
27
+ */
28
+ export interface UpgradeUnitLike {
29
+ readonly name: string;
30
+ readonly state: string;
31
+ readonly currentVersion: string | null;
32
+ readonly targetVersion: string | null;
33
+ readonly targetTag: string | null;
34
+ readonly tags: readonly UpgradeTagLike[] | null;
35
+ readonly source?: string;
36
+ readonly at?: string;
37
+ }
38
+ /**
39
+ * 升级入口的显示形态。
40
+ *
41
+ * 刻意做成**四态一一对应**的枚举(而不是布尔的 canUpgrade):布尔会把
42
+ * up-to-date 与 unknown 压成同一个 false,而那正是要防的"靠缺席传达"。
43
+ */
44
+ export type UpgradeRowKind =
45
+ /** 有版本事实且比当前新 → 画升级入口(tags 非空时列出让用户挑)。 */
46
+ 'upgrade'
47
+ /** 有版本事实、没有更新的 → 插件页**不画**("已是最新"的口径在市场页/关于页)。 */
48
+ | 'hidden'
49
+ /** 查不到版本事实 → **必须画**「查不到:<原因>」+ 重试。 */
50
+ | 'unknown'
51
+ /** 结构上就升不了(安装方提供的层)→ 画说明 + 命令,**不给按钮**。 */
52
+ | 'command';
53
+ /** 状态值 → 形态(逐态显式表:新增状态时编译期就会在这里暴露,不会静默落进 default)。 */
54
+ export declare const ROW_KIND_OF_STATE: Readonly<Record<UpgradeState, UpgradeRowKind>>;
55
+ /** 四态的稳定顺序(用于断言与文档对照,不用于渲染顺序)。 */
56
+ export declare const ROW_KIND_ORDER: readonly UpgradeRowKind[];
57
+ /**
58
+ * 一个单元在界面上的形态。
59
+ *
60
+ * @param unit - host 给出的单元报告。
61
+ * @returns 形态;状态值未知时按 unknown 处理(读不懂的状态不能染成"已是最新",
62
+ * 也不能给出一个可能无效的升级按钮)。
63
+ */
64
+ export declare function rowKindOf(unit: {
65
+ readonly state: string;
66
+ }): UpgradeRowKind;
67
+ /**
68
+ * 这一行是否要出现在界面上。
69
+ *
70
+ * 判据直接来自 DESIGN §5.5 的表格:只有 up-to-date 缺席,其余三态各自有形态。
71
+ * 这是本模块**唯一**的可见性出口——组件不许再写一份。
72
+ *
73
+ * @param unit - host 给出的单元报告。
74
+ * @returns 要画时 true。
75
+ */
76
+ export declare function rowVisible(unit: {
77
+ readonly state: string;
78
+ }): boolean;
79
+ /**
80
+ * 把一次检查结果折成"包名 → 单元"的查找表。
81
+ *
82
+ * 同名条目后到者不覆盖先到者:host 的 unitFacts 按名字去重过,出现重复说明契约被破坏,
83
+ * 此时保留第一条(并让界面的计数与列表一致)比"悄悄用最后一条"更可解释。
84
+ *
85
+ * @param units - 检查结果的单元列表。
86
+ * @returns 查找表。
87
+ */
88
+ export declare function unitIndex<T extends {
89
+ readonly name: string;
90
+ }>(units: readonly T[]): ReadonlyMap<string, T>;
91
+ /**
92
+ * 注册面的**目标集合**:该为哪些包名持有 plugins.bundle.config 的 key。
93
+ *
94
+ * 判据 = **已装** ∧ (还没查过 ∨ 这一态要显示)。两种情况都在集合里:
95
+ *
96
+ * ① **还没查过**(units 为 undefined,或这一轮检查里没有这个包)→ 注册。
97
+ * 这是必须的,而且有两个理由:
98
+ * · 诚实:此时界面画的是"尚未检查 + 检查按钮",而不是一片安静——
99
+ * "查不到"与"没查"都必须说出来(§12.3.3:不许用缺席表达状态);
100
+ * · 因果:检查正是由**行自己挂载时的 effect** 触发的(§5.5 的"进入即查")。
101
+ * 若先要求"查过才注册",就永远没有东西去触发那次检查(先有鸡还是先有蛋)。
102
+ * ② **查过且要显示**(update-available / unknown / not-upgradable)→ 注册;
103
+ * **查过且 up-to-date** → 不注册,于是那个 key 的 disposer 被释放,
104
+ * 官方 config-ledger 重算,那一节**当场消失**(§5.5 的回收纪律)。
105
+ *
106
+ * 与 {@link rowVisible} 分开的理由:它们回答不同的问题——"这一行要不要画"(渲染期)与
107
+ * "这个 key 要不要注册"(装配期)。合并会让"不画"变成"不注册",而 keyed slot 的 key
108
+ * 一旦撤掉那一节会**当场消失**(§5.5 要的正是这个),所以两者必须能被分别断言。
109
+ *
110
+ * @param installed - 当前已装的包名(官方台账)。
111
+ * @param units - 最新一次检查的单元列表;还没检查时为 undefined。
112
+ * @param keep - 必须**保留**的包名,见下面那段。
113
+ * @returns 要注册的包名(去重、稳定顺序:按名字排序)。
114
+ */
115
+ export declare function registeredNames(installed: Iterable<string>, units: readonly {
116
+ readonly name: string;
117
+ readonly state: string;
118
+ }[] | undefined,
119
+ /**
120
+ * 必须**保留**的包名(即使按上面两条判据该撤掉)。
121
+ *
122
+ * 目前唯一的来源是"这个包有一次还没被用户处置的升级/回滚结果"。
123
+ * 为什么这条必须存在(真机实测的缺陷,不是洁癖):升级成功后版本事实立刻被重查,
124
+ * 那个包通常就变成 up-to-date 了 → key 被释放 → 那一节当场消失 ——
125
+ * **连同刚写下的结果一起消失**。用户点完升级,看到的是界面恢复原样,
126
+ * 完全不知道刚才那次升级是成功了、失败了还是回滚了。结果必须活到用户处置为止。
127
+ */
128
+ keep?: Iterable<string>): string[];
129
+ /**
130
+ * 默认选中的 tag。
131
+ *
132
+ * 口径与 host 的 pickTarget 一致(同线最新优先),但**不重算**:host 已经在
133
+ * unit.targetTag 里给了答案,这里只是"把那个 tag 在列表里找出来"。找不到时退到
134
+ * preferred 标记,再退到第一个——绝不返回一个不在列表里的 tag。
135
+ *
136
+ * @param unit - 单元报告。
137
+ * @returns 选中的 tag 名;没有可选 tag 时 undefined(此时不给"挑版本")。
138
+ */
139
+ export declare function defaultTag(unit: {
140
+ readonly tags: readonly UpgradeTagLike[] | null;
141
+ readonly targetTag: string | null;
142
+ }): string | undefined;
143
+ /**
144
+ * 本模块只读 dist-tag 的这四个字段(host 的 UpgradeTag 是它的超集)。
145
+ *
146
+ * line 是宽松的 string:host 新增一档版本线时,界面要能如实显示,而不是被一个窄联合
147
+ * 挡在编译期之外。判定只对 'other-line' 取真(其余一律按"同线/未知"处理,宁可少提醒一次
148
+ * 也不谎报"会切到另一条线")。
149
+ */
150
+ export interface UpgradeTagLike {
151
+ readonly tag: string;
152
+ readonly version: string;
153
+ readonly line: string;
154
+ readonly preferred: boolean;
155
+ }
156
+ /**
157
+ * 用户挑的 tag 对应的版本。
158
+ *
159
+ * @param unit - 单元报告。
160
+ * @param tag - 选中的 tag 名;undefined 时用 {@link defaultTag}。
161
+ * @returns 版本;没有可选 tag 时退回 host 给的 targetVersion。
162
+ */
163
+ export declare function versionForTag(unit: {
164
+ readonly tags: readonly UpgradeTagLike[] | null;
165
+ readonly targetTag: string | null;
166
+ readonly targetVersion: string | null;
167
+ }, tag?: string): string | null;
168
+ /**
169
+ * 所选版本是不是**切到另一条线**。
170
+ *
171
+ * 判据用 host 给的 UpgradeTag.line,不在这里重算版本线(那是 host 的 versionLine)。
172
+ *
173
+ * @param unit - 单元报告。
174
+ * @param tag - 选中的 tag 名。
175
+ * @returns 会切到另一条线时 true。
176
+ */
177
+ export declare function changesLine(unit: {
178
+ readonly tags: readonly UpgradeTagLike[] | null;
179
+ readonly targetTag: string | null;
180
+ }, tag?: string): boolean;
181
+ /**
182
+ * 所选版本与当前版本是否相同(相同时不给可点按钮,DESIGN §5.5)。
183
+ *
184
+ * @param unit - 单元报告。
185
+ * @param tag - 选中的 tag 名。
186
+ * @returns 相同时 true。
187
+ */
188
+ export declare function sameAsCurrent(unit: {
189
+ readonly tags: readonly UpgradeTagLike[] | null;
190
+ readonly targetTag: string | null;
191
+ readonly currentVersion: string | null;
192
+ readonly targetVersion: string | null;
193
+ }, tag?: string): boolean;
194
+ /**
195
+ * 「当前 x → y | 升级」里的版本对,缺失时给 null 让界面显示"未知"而不是编一个。
196
+ *
197
+ * @param unit - 单元报告。
198
+ * @param tag - 选中的 tag 名。
199
+ * @returns 当前版本与目标版本。
200
+ */
201
+ export declare function versionPair(unit: {
202
+ readonly currentVersion: string | null;
203
+ readonly tags: readonly UpgradeTagLike[] | null;
204
+ readonly targetTag: string | null;
205
+ readonly targetVersion: string | null;
206
+ }, tag?: string): {
207
+ readonly from: string | null;
208
+ readonly to: string | null;
209
+ };
210
+ /**
211
+ * 「来源与时间」这一行要显示什么。
212
+ *
213
+ * 契约要求界面**必须**把事实来源与时间标出来(否则"最新"这个断言没有依据)。
214
+ * 来源未知时返回 undefined(不编一个来源出来)。
215
+ *
216
+ * @param unit - 单元报告。
217
+ * @returns 来源标识与时间;来源未知时 undefined。
218
+ */
219
+ export declare function sourceFacts(unit: {
220
+ readonly source?: string;
221
+ readonly at?: string;
222
+ }): {
223
+ readonly source: 'market-index' | 'registry';
224
+ readonly at: string | undefined;
225
+ } | undefined;
226
+ /**
227
+ * 升级结果的**诚实分类**。
228
+ *
229
+ * 这是本模块最要紧的一条:失败态绝不能被渲染成完成(task-14/18/85 那一类问题的第四次机会)。
230
+ * 分类刻意按"用户该知道什么"切,而不是按 host 的错误码切:
231
+ * · done:真升级成功;
232
+ * · rolled-back:试装没通过 → **没有在真实环境执行升级**(host 返回 canary-not-passed);
233
+ * · failed:官方通道失败或盘上核对没到位;
234
+ * · unverified:金丝雀**没跑**就升级了 —— "没验证"既不是通过也不是失败,
235
+ * 它必须能被看出来(否则用户以为验证过了)。
236
+ */
237
+ export type UpgradeOutcomeKind = 'done' | 'rolled-back' | 'failed' | 'unverified';
238
+ /**
239
+ * 读一次升级结果。
240
+ *
241
+ * @param result - host 的 upgrade op 结果(已归一)。
242
+ * @returns 分类。
243
+ */
244
+ export declare function upgradeOutcome(result: {
245
+ readonly ok: boolean;
246
+ readonly code?: string;
247
+ readonly canary?: {
248
+ readonly ran: boolean;
249
+ readonly conclusion?: string;
250
+ };
251
+ }): UpgradeOutcomeKind;
252
+ /**
253
+ * 金丝雀的读法:**"没验证"与"验证失败"必须分开**。
254
+ *
255
+ * @param canary - 升级结果里的金丝雀报告;没有时为 undefined。
256
+ * @returns 四态;没有金丝雀字段时 absent(宿主没给,界面据此不宣称任何结论)。
257
+ */
258
+ export declare function canaryVerdict(canary: {
259
+ readonly ran: boolean;
260
+ readonly conclusion?: string;
261
+ } | undefined): 'passed' | 'failed' | 'not-run' | 'absent';
262
+ /**
263
+ * 这一次升级结果**能不能**回滚(task-97)。
264
+ *
265
+ * 两个条件同时成立才行:
266
+ * · **刚完成过一次升级**(`done` / `unverified`)——
267
+ * `rolled-back` 是"没有升级"、`failed` 是"没完成",两者都**没有可回滚的东西**;
268
+ * · **知道升级前的版本**(`fromVersion` 非空)——拿不到就**不显示入口**:
269
+ * 回滚会把环境装成那个版本,**猜不得**(§12.10:不许用推断代替事实)。
270
+ *
271
+ * 为什么放在这个纯决策模块(而不是组件文件里):这样它可被单测**直接**钉住,
272
+ * 而不是只能靠渲染结果反推。界面与测试引用同一个函数,不各写一份(写两份必然漂移)。
273
+ *
274
+ * @param action - 升级结果(可能没有)。
275
+ * @returns 可以回滚时 true。
276
+ */
277
+ /** 可回滚的升级结果:outcome 是 done/unverified,且**确实知道**升级前的版本。 */
278
+ export interface RollbackCandidate {
279
+ readonly outcome: string;
280
+ readonly fromVersion: string;
281
+ }
282
+ export declare function canRollback(action: {
283
+ readonly outcome: string;
284
+ readonly fromVersion: string | null;
285
+ } | undefined): action is RollbackCandidate;
286
+ /**
287
+ * 一次升级/回滚结果里"盘上事实"是否真的对上了。
288
+ *
289
+ * 回滚用的是 host 的 clean(它按盘上事实核对过);升级用的是 ok。
290
+ * 两者都不是"我们没看到报错"——**没看到不等于核对过**。
291
+ *
292
+ * @param result - 升级或回滚结果。
293
+ * @returns 核对通过时 true;载荷缺字段时 false(读不出来不算核对过)。
294
+ */
295
+ export declare function diskVerified(result: {
296
+ readonly ok: boolean;
297
+ readonly clean?: boolean;
298
+ }): boolean;
299
+ /**
300
+ * 市场卡片上要不要画「升级到 x.y.z」。
301
+ *
302
+ * 零新机制:用既有的 updateAvailable 判据(marketView.ts)与 latestVersion。
303
+ * 未安装、无更新、或版本读不到时都不出现。
304
+ *
305
+ * @param item - 市场条目(只读它关心的字段)。
306
+ * @param updateAvailable - marketView 的判据(注入以便复用同一份实现,不抄第二份)。
307
+ * @returns 目标版本;不画时 undefined。
308
+ */
309
+ export declare function marketUpgradeTarget<T extends {
310
+ readonly installed?: boolean;
311
+ readonly installedVersion?: string;
312
+ readonly latestVersion?: string;
313
+ }>(item: T, updateAvailable: (item: T) => boolean): string | undefined;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * upgradeView.ts — 升级的**纯决策**:四态怎么显示、注册面收哪些包、结果怎么读。
3
+ *
4
+ * 归属:A 类·重写(新模块,无旧实现可参考:旧仓库的"升级"只是一句提示文案)。
5
+ * 旧实现参考:无(旧 dsh-web-plugin-manager 没有升级功能;其市场页只有"可更新 x.y.z"徽标)。
6
+ * 官方复用:无。本模块**不认识 React、不认识 ctx、不联网、不读盘**——它是可独立测试的
7
+ * 决策层(与 marketView.ts / tags.ts 同一方法论:把判定抽出来,组件只负责画)。
8
+ * 前提检查:四态(update-available / up-to-date / unknown / not-upgradable)由 host 的
9
+ * upgradeCheck op 直接给出(docs/REST-CONTRACT.md「UpgradeCheckResult.units[].state 四态」),
10
+ * 界面**直接渲染,不自己推断**。本模块只回答三个界面问题:
11
+ * ① 这一行该不该画、画什么;② 官方插件页的 keyed 注册面该注册哪些包名;③ 一次升级结果
12
+ * 该被读成成功/失败/回滚/没验证。
13
+ *
14
+ * 为什么必须有这一层(而不是把判断写进组件):
15
+ * · "靠缺席传达状态"是本仓库反复踩过的坑(DESIGN §5.5 / §12.3.3)。四态里 up-to-date
16
+ * 与 unknown 都不画升级入口,但它们的**事实完全不同**:前者是"查过,没有更新",
17
+ * 后者是"查不到"。把两者的可见性判定收进一个纯函数,测试才能逐个钉住
18
+ * "unknown 绝不显示成已是最新"。
19
+ * · 注册对账(§5.5:不留孤儿 key)需要一个可断言的集合运算,而不是散在 apply 里的副作用。
20
+ */
21
+ /** 状态值 → 形态(逐态显式表:新增状态时编译期就会在这里暴露,不会静默落进 default)。 */
22
+ export const ROW_KIND_OF_STATE = {
23
+ 'update-available': 'upgrade',
24
+ 'up-to-date': 'hidden',
25
+ 'unknown': 'unknown',
26
+ 'not-upgradable': 'command',
27
+ };
28
+ /** 四态的稳定顺序(用于断言与文档对照,不用于渲染顺序)。 */
29
+ export const ROW_KIND_ORDER = ['upgrade', 'unknown', 'command', 'hidden'];
30
+ /**
31
+ * 一个单元在界面上的形态。
32
+ *
33
+ * @param unit - host 给出的单元报告。
34
+ * @returns 形态;状态值未知时按 unknown 处理(读不懂的状态不能染成"已是最新",
35
+ * 也不能给出一个可能无效的升级按钮)。
36
+ */
37
+ export function rowKindOf(unit) {
38
+ return ROW_KIND_OF_STATE[unit.state] ?? 'unknown';
39
+ }
40
+ /**
41
+ * 这一行是否要出现在界面上。
42
+ *
43
+ * 判据直接来自 DESIGN §5.5 的表格:只有 up-to-date 缺席,其余三态各自有形态。
44
+ * 这是本模块**唯一**的可见性出口——组件不许再写一份。
45
+ *
46
+ * @param unit - host 给出的单元报告。
47
+ * @returns 要画时 true。
48
+ */
49
+ export function rowVisible(unit) {
50
+ return rowKindOf(unit) !== 'hidden';
51
+ }
52
+ /**
53
+ * 把一次检查结果折成"包名 → 单元"的查找表。
54
+ *
55
+ * 同名条目后到者不覆盖先到者:host 的 unitFacts 按名字去重过,出现重复说明契约被破坏,
56
+ * 此时保留第一条(并让界面的计数与列表一致)比"悄悄用最后一条"更可解释。
57
+ *
58
+ * @param units - 检查结果的单元列表。
59
+ * @returns 查找表。
60
+ */
61
+ export function unitIndex(units) {
62
+ const index = new Map();
63
+ for (const unit of units)
64
+ if (!index.has(unit.name))
65
+ index.set(unit.name, unit);
66
+ return index;
67
+ }
68
+ /**
69
+ * 注册面的**目标集合**:该为哪些包名持有 plugins.bundle.config 的 key。
70
+ *
71
+ * 判据 = **已装** ∧ (还没查过 ∨ 这一态要显示)。两种情况都在集合里:
72
+ *
73
+ * ① **还没查过**(units 为 undefined,或这一轮检查里没有这个包)→ 注册。
74
+ * 这是必须的,而且有两个理由:
75
+ * · 诚实:此时界面画的是"尚未检查 + 检查按钮",而不是一片安静——
76
+ * "查不到"与"没查"都必须说出来(§12.3.3:不许用缺席表达状态);
77
+ * · 因果:检查正是由**行自己挂载时的 effect** 触发的(§5.5 的"进入即查")。
78
+ * 若先要求"查过才注册",就永远没有东西去触发那次检查(先有鸡还是先有蛋)。
79
+ * ② **查过且要显示**(update-available / unknown / not-upgradable)→ 注册;
80
+ * **查过且 up-to-date** → 不注册,于是那个 key 的 disposer 被释放,
81
+ * 官方 config-ledger 重算,那一节**当场消失**(§5.5 的回收纪律)。
82
+ *
83
+ * 与 {@link rowVisible} 分开的理由:它们回答不同的问题——"这一行要不要画"(渲染期)与
84
+ * "这个 key 要不要注册"(装配期)。合并会让"不画"变成"不注册",而 keyed slot 的 key
85
+ * 一旦撤掉那一节会**当场消失**(§5.5 要的正是这个),所以两者必须能被分别断言。
86
+ *
87
+ * @param installed - 当前已装的包名(官方台账)。
88
+ * @param units - 最新一次检查的单元列表;还没检查时为 undefined。
89
+ * @param keep - 必须**保留**的包名,见下面那段。
90
+ * @returns 要注册的包名(去重、稳定顺序:按名字排序)。
91
+ */
92
+ export function registeredNames(installed, units,
93
+ /**
94
+ * 必须**保留**的包名(即使按上面两条判据该撤掉)。
95
+ *
96
+ * 目前唯一的来源是"这个包有一次还没被用户处置的升级/回滚结果"。
97
+ * 为什么这条必须存在(真机实测的缺陷,不是洁癖):升级成功后版本事实立刻被重查,
98
+ * 那个包通常就变成 up-to-date 了 → key 被释放 → 那一节当场消失 ——
99
+ * **连同刚写下的结果一起消失**。用户点完升级,看到的是界面恢复原样,
100
+ * 完全不知道刚才那次升级是成功了、失败了还是回滚了。结果必须活到用户处置为止。
101
+ */
102
+ keep = []) {
103
+ const byName = new Map();
104
+ for (const unit of units ?? [])
105
+ if (!byName.has(unit.name))
106
+ byName.set(unit.name, unit);
107
+ const kept = new Set(keep);
108
+ const out = [];
109
+ for (const name of new Set(installed)) {
110
+ if (kept.has(name)) {
111
+ out.push(name);
112
+ continue;
113
+ }
114
+ const unit = byName.get(name);
115
+ if (unit === undefined || rowVisible(unit))
116
+ out.push(name);
117
+ }
118
+ return out.sort();
119
+ }
120
+ // ── 版本选择(多 dist-tags)───────────────────────────────────────────────
121
+ /**
122
+ * 默认选中的 tag。
123
+ *
124
+ * 口径与 host 的 pickTarget 一致(同线最新优先),但**不重算**:host 已经在
125
+ * unit.targetTag 里给了答案,这里只是"把那个 tag 在列表里找出来"。找不到时退到
126
+ * preferred 标记,再退到第一个——绝不返回一个不在列表里的 tag。
127
+ *
128
+ * @param unit - 单元报告。
129
+ * @returns 选中的 tag 名;没有可选 tag 时 undefined(此时不给"挑版本")。
130
+ */
131
+ export function defaultTag(unit) {
132
+ const tags = unit.tags;
133
+ if (tags === null || tags.length === 0)
134
+ return undefined;
135
+ if (unit.targetTag !== null && tags.some(tag => tag.tag === unit.targetTag))
136
+ return unit.targetTag;
137
+ const preferred = tags.find(tag => tag.preferred);
138
+ return (preferred ?? tags[0])?.tag;
139
+ }
140
+ /**
141
+ * 用户挑的 tag 对应的版本。
142
+ *
143
+ * @param unit - 单元报告。
144
+ * @param tag - 选中的 tag 名;undefined 时用 {@link defaultTag}。
145
+ * @returns 版本;没有可选 tag 时退回 host 给的 targetVersion。
146
+ */
147
+ export function versionForTag(unit, tag) {
148
+ const tags = unit.tags;
149
+ if (tags === null || tags.length === 0)
150
+ return unit.targetVersion;
151
+ const wanted = tag ?? defaultTag(unit);
152
+ return tags.find(entry => entry.tag === wanted)?.version ?? unit.targetVersion;
153
+ }
154
+ /**
155
+ * 所选版本是不是**切到另一条线**。
156
+ *
157
+ * 判据用 host 给的 UpgradeTag.line,不在这里重算版本线(那是 host 的 versionLine)。
158
+ *
159
+ * @param unit - 单元报告。
160
+ * @param tag - 选中的 tag 名。
161
+ * @returns 会切到另一条线时 true。
162
+ */
163
+ export function changesLine(unit, tag) {
164
+ const tags = unit.tags;
165
+ if (tags === null)
166
+ return false;
167
+ const wanted = tag ?? defaultTag(unit);
168
+ return tags.find(entry => entry.tag === wanted)?.line === 'other-line';
169
+ }
170
+ /**
171
+ * 所选版本与当前版本是否相同(相同时不给可点按钮,DESIGN §5.5)。
172
+ *
173
+ * @param unit - 单元报告。
174
+ * @param tag - 选中的 tag 名。
175
+ * @returns 相同时 true。
176
+ */
177
+ export function sameAsCurrent(unit, tag) {
178
+ const current = unit.currentVersion;
179
+ if (current === null)
180
+ return false;
181
+ return versionForTag(unit, tag) === current;
182
+ }
183
+ /**
184
+ * 「当前 x → y | 升级」里的版本对,缺失时给 null 让界面显示"未知"而不是编一个。
185
+ *
186
+ * @param unit - 单元报告。
187
+ * @param tag - 选中的 tag 名。
188
+ * @returns 当前版本与目标版本。
189
+ */
190
+ export function versionPair(unit, tag) {
191
+ return { from: unit.currentVersion, to: versionForTag(unit, tag) };
192
+ }
193
+ /**
194
+ * 「来源与时间」这一行要显示什么。
195
+ *
196
+ * 契约要求界面**必须**把事实来源与时间标出来(否则"最新"这个断言没有依据)。
197
+ * 来源未知时返回 undefined(不编一个来源出来)。
198
+ *
199
+ * @param unit - 单元报告。
200
+ * @returns 来源标识与时间;来源未知时 undefined。
201
+ */
202
+ export function sourceFacts(unit) {
203
+ if (unit.source !== 'market-index' && unit.source !== 'registry')
204
+ return undefined;
205
+ return { source: unit.source, at: unit.at };
206
+ }
207
+ /**
208
+ * 读一次升级结果。
209
+ *
210
+ * @param result - host 的 upgrade op 结果(已归一)。
211
+ * @returns 分类。
212
+ */
213
+ export function upgradeOutcome(result) {
214
+ if (!result.ok) {
215
+ // canary-not-passed 是"没动真实环境"(试装拦下),与"升级命令失败"是两件事:
216
+ // 前者环境没被改,后者可能改了一半。用户处置完全不同。
217
+ return result.code === 'canary-not-passed' ? 'rolled-back' : 'failed';
218
+ }
219
+ return result.canary !== undefined && result.canary.ran === false ? 'unverified' : 'done';
220
+ }
221
+ /**
222
+ * 金丝雀的读法:**"没验证"与"验证失败"必须分开**。
223
+ *
224
+ * @param canary - 升级结果里的金丝雀报告;没有时为 undefined。
225
+ * @returns 四态;没有金丝雀字段时 absent(宿主没给,界面据此不宣称任何结论)。
226
+ */
227
+ export function canaryVerdict(canary) {
228
+ if (canary === undefined)
229
+ return 'absent';
230
+ if (canary.ran === false)
231
+ return 'not-run';
232
+ return canary.conclusion === 'passed' ? 'passed' : 'failed';
233
+ }
234
+ export function canRollback(action) {
235
+ if (action === undefined)
236
+ return false;
237
+ if (action.outcome !== 'done' && action.outcome !== 'unverified')
238
+ return false;
239
+ return typeof action.fromVersion === 'string' && action.fromVersion.length > 0;
240
+ }
241
+ /**
242
+ * 一次升级/回滚结果里"盘上事实"是否真的对上了。
243
+ *
244
+ * 回滚用的是 host 的 clean(它按盘上事实核对过);升级用的是 ok。
245
+ * 两者都不是"我们没看到报错"——**没看到不等于核对过**。
246
+ *
247
+ * @param result - 升级或回滚结果。
248
+ * @returns 核对通过时 true;载荷缺字段时 false(读不出来不算核对过)。
249
+ */
250
+ export function diskVerified(result) {
251
+ if (result.clean !== undefined)
252
+ return result.clean;
253
+ return result.ok;
254
+ }
255
+ // ── 市场页卡片 ────────────────────────────────────────────────────────────
256
+ /**
257
+ * 市场卡片上要不要画「升级到 x.y.z」。
258
+ *
259
+ * 零新机制:用既有的 updateAvailable 判据(marketView.ts)与 latestVersion。
260
+ * 未安装、无更新、或版本读不到时都不出现。
261
+ *
262
+ * @param item - 市场条目(只读它关心的字段)。
263
+ * @param updateAvailable - marketView 的判据(注入以便复用同一份实现,不抄第二份)。
264
+ * @returns 目标版本;不画时 undefined。
265
+ */
266
+ export function marketUpgradeTarget(item, updateAvailable) {
267
+ if (item.installed !== true)
268
+ return undefined;
269
+ if (updateAvailable(item) !== true)
270
+ return undefined;
271
+ const version = item.latestVersion;
272
+ return version === undefined || version.length === 0 ? undefined : version;
273
+ }