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,295 @@
1
+ /**
2
+ * registry.ts — 社区索引(topic:dsh-plugin 全量表)的多源兜底链与磁盘缓存。
3
+ *
4
+ * 归属:A 类·重写(旧 src/registry.ts 仅作意图参考,未复制代码)。
5
+ * 旧实现参考:dsh-web-plugin-manager/src/registry.ts(理解意图用,未复制代码)——它解决的问题是:
6
+ * 社区索引(registry.json,CI 每两小时重建)能让市场拿到几千条条目而**完全不碰 GitHub API 配额**;
7
+ * CDN 会滞后所以需要新鲜度判断;单一来源不可靠所以需要多源兜底;全部失败时要有磁盘缓存兜底。
8
+ * 官方复用:无。官方没有市场概念,也没有索引/代理/缓存能力(见 DESIGN.md §1 的分工表)。
9
+ * 前提检查:旧实现的前提仍然成立(社区索引依旧是唯一零配额的全量来源),但它踩过的四个坑必须避开,
10
+ * 旧审计 docs/private/audit/correctness.md 有记录:
11
+ * 1) M14:GitHub api/raw 的响应**没有 generated_at**,旧实现因此只允许 CDN 结果落盘——一旦 CDN 不可达,
12
+ * 缓存永远不会被写入。本实现改成"**只允许更新,不允许回退**":候选索引的 generated_at 必须不旧于
13
+ * 已缓存的那个,才允许落盘(对任何来源都成立,比"按来源白名单"更精确)。
14
+ * 2) M15:五跳串行 × 单请求 15s 可以挂住 75s。本实现给整条链一个总预算(默认 60s)并**真正中止**
15
+ * 在途请求(AbortController),而不是只把 Promise.race 掉、让请求继续在后台跑。
16
+ * 3) 缓存写入用 pretty-print 的 JSON,3115 条时 1.93MB / 5.4ms(旧 perf 审计 §1.8)。这里一律紧凑输出。
17
+ * 4) 索引内容没有做"条目级"校验——非对象、缺 repo 名的条目会被当成合法条目流入下游。这里逐条严格归一化,
18
+ * 并如实报告丢掉了多少条(skipped),不静默。
19
+ *
20
+ * 数据源与旧仓库一致(**不扩数据源**:不做 catalog 目录抓取、不引入 dsh.so 覆盖层)。
21
+ */
22
+ import type { Fetcher } from './net.ts';
23
+ import type { MarketInstallable, MarketItemKind, MarketRiskFlag, MarketRiskTier } from './types.ts';
24
+ /** 社区索引仓库。只读消费,永远是硬编码兜底,不是依赖。 */
25
+ export declare const REGISTRY_OWNER = "bradeGithub";
26
+ export declare const REGISTRY_REPO = "DSH-Plugins-Marketplace";
27
+ /** 索引文件名(仓库根目录下由 CI 生成)。 */
28
+ export declare const REGISTRY_FILE = "registry.json";
29
+ /** 索引缓存的默认有效期(分钟级配置见 settings.marketplace.cacheTtlMinutes)。 */
30
+ export declare const REGISTRY_CACHE_TTL_MS: number;
31
+ /** CDN 会滞后:标记为 requireFresh 的来源,其索引超过这个年龄就跳到下一跳。 */
32
+ export declare const CDN_MAX_INDEX_AGE_MS: number;
33
+ /** 整条兜底链的总预算:单请求超时由调用方给,总预算在这里封顶。 */
34
+ export declare const REGISTRY_TOTAL_BUDGET_MS = 60000;
35
+ /**
36
+ * 生态泛化主题词:它们是"生态标签"而不是功能信号,会挤掉卡片上有信息量的 topic。
37
+ *
38
+ * 这是一份**手写的小表**(不是从旧仓库或上游复制的 100+ 词表):只保留最容易泛滥的
39
+ * 十几个,宁可少过滤也不误杀有意义的主题。要扩充时按同一标准加("这个词能否区分两个插件")。
40
+ */
41
+ export declare const ECO_GENERIC_TOPICS: Set<string>;
42
+ /** 一个主题列表 → 去掉生态泛化词后的功能主题(最多 8 个,卡片上只显示 2 个)。 */
43
+ export declare function functionalTopics(topics: readonly string[] | undefined): string[];
44
+ /** 索引里的一条仓库条目(已归一化,JSON-safe)。 */
45
+ export interface RegistryRepo {
46
+ /** owner/repo,原样大小写。 */
47
+ readonly repo: string;
48
+ /** 仓库名(repo 的 basename)。 */
49
+ readonly name: string;
50
+ readonly description: string;
51
+ /** 星数;索引没给时为 null(**不伪造 0**——0 与"未知"在排序/展示上是两回事)。 */
52
+ readonly stars: number | null;
53
+ /** 最后更新时间的 ISO 串;未知时 null。 */
54
+ readonly updatedAt: string | null;
55
+ readonly topics: readonly string[];
56
+ readonly category?: string;
57
+ /** 仓库发布的 npm 包名(CI 采集);装到 profile 里的键就是它。 */
58
+ readonly packageName?: string;
59
+ /** 仓库 package.json 里的版本(CI 采集)。 */
60
+ readonly latestVersion?: string;
61
+ /** 上游若显式给出条目形态就透传;否则由 marketplace.ts 按来源判定。 */
62
+ readonly kind?: MarketItemKind;
63
+ /**
64
+ * ── 上游元数据(task-45 政策决定"展示哪些",本层只负责**原样透传事实**)──
65
+ *
66
+ * 分层:**服务端只给事实,展示决策在客户端**。典型例子:installable=non-plugin 的条目
67
+ * 仍然会出现在 REST 结果里(1,018 条),是否隐藏由 UI 的 filterInstallable 决定——
68
+ * host 替用户做过滤会让"上游到底标了什么"无从查证。
69
+ */
70
+ readonly installable?: MarketInstallable;
71
+ /** 上游风险等级;缺省表示上游没给结论(不是 safe)。 */
72
+ readonly riskTier?: MarketRiskTier;
73
+ /** 上游风险明细(超长会截断:卡片不用,详情够用)。 */
74
+ readonly riskFlags?: readonly MarketRiskFlag[];
75
+ /** 独立验证报告外链(仅在 verdict=pass 时透传,见 normalize)。 */
76
+ readonly reportUrl?: string;
77
+ /** 上游收录标记(community-pick / verified-install / …)。 */
78
+ readonly marketTags?: readonly string[];
79
+ /** 仓库已归档。 */
80
+ readonly archived?: boolean;
81
+ /** 近 7 天 star 增量(只用于排序)。 */
82
+ readonly starsDelta7d?: number;
83
+ /** 仓库许可证(SPDX id);按政策只进详情。 */
84
+ readonly license?: string;
85
+ /** 独立校验方(verdict=pass 时)。 */
86
+ readonly verifiedBy?: string;
87
+ /** 独立校验时间(verdict=pass 时,ISO 日期)。 */
88
+ readonly verifiedAt?: string;
89
+ }
90
+ /**
91
+ * 归一化一条原始索引条目;不可用时返回 null。
92
+ *
93
+ * 严格性是有意的:索引是外部 JSON,字段缺失/类型漂移必须在这里被挡住,
94
+ * 否则下游的类型假设会变成运行时 TypeError(旧仓库在卡片渲染里就被 topic 非字符串炸过)。
95
+ *
96
+ * @param raw - 索引里的一条记录(registry.json 或 search API 的形状都兼容)。
97
+ * @returns 归一化条目;非对象、无 repo 名、或被排除的仓库返回 null。
98
+ */
99
+ export declare function normalizeRegistryRepo(raw: unknown): RegistryRepo | null;
100
+ /** 索引载荷的解析结果。 */
101
+ export interface ParsedRegistryPayload {
102
+ readonly repos: readonly RegistryRepo[];
103
+ /** 索引自身的生成时间(ISO);载荷没给时为 null。 */
104
+ readonly generatedAt: string | null;
105
+ /** 被丢弃的条目数(形状不合法或被排除)——如实报告,不静默。 */
106
+ readonly skipped: number;
107
+ }
108
+ /**
109
+ * 解析一份索引载荷(registry.json 的 `{generated_at, repos}`、search API 的 `{items}`、或裸数组)。
110
+ *
111
+ * 按 repo 名去重(保留先出现的那条:索引自己已排好序,先出现的是上游的取舍)。
112
+ *
113
+ * @param payload - 已 JSON.parse 的载荷。
114
+ * @returns 解析结果;没有任何可用条目时返回 null(调用方据此跳到下一跳)。
115
+ */
116
+ export declare function parseRegistryPayload(payload: unknown): ParsedRegistryPayload | null;
117
+ /** 索引的一跳来源。 */
118
+ export interface RegistryIndexSource {
119
+ readonly id: string;
120
+ readonly url: string;
121
+ /** 响应是 gzip(.gz 文件)时需要解压。 */
122
+ readonly gzip: boolean;
123
+ /** 附带 GitHub token(只有 api.github.com 认这个头)。 */
124
+ readonly token: boolean;
125
+ /** CDN 一跳:索引必须比 CDN_MAX_INDEX_AGE_MS 新,否则跳到下一跳。 */
126
+ readonly requireFresh: boolean;
127
+ }
128
+ /**
129
+ * 兜底链的跳数顺序:api.github → jsDelivr(.gz) → raw(.gz) → jsDelivr → raw。
130
+ *
131
+ * 顺序的由来:api.github.com 带 token 时最可靠且能读到刚 push 的文件(CDN 会滞后),
132
+ * 但无 token 时配额只有 60/h,所以 CDN 的 .gz(体积最小)紧跟其后;raw 是最不依赖 CDN 的一跳。
133
+ *
134
+ * settings.marketplace.indexUrl 非空时作为**首选**跳插入队首,但内置链仍然保留:
135
+ * 用户填错地址不该让整个市场变成空的(配置是"优选"而不是"唯一",这一点在配置项注释里也写了)。
136
+ *
137
+ * @param options - indexUrl(用户自定义源)与 branch(fork/镜像用)。
138
+ * @returns 按尝试顺序排列的来源列表。
139
+ */
140
+ export declare function registryIndexSources(options?: {
141
+ readonly indexUrl?: string;
142
+ readonly branch?: string;
143
+ }): RegistryIndexSource[];
144
+ /**
145
+ * 磁盘缓存的落盘格式版本。
146
+ *
147
+ * 为什么必须显式带版本:缓存里存的是**我们归一化后的记录形**(stars / updatedAt / riskTier …),
148
+ * 与上游 registry.json 的 snake_case 完全不同。旧版本(无该字段)的读法把缓存记录又喂给上游解析器,
149
+ * 于是 11 个字段静默丢失(stars 变 null、riskTier/marketTags/starsDelta7d 等整块消失)——
150
+ * 表现为"缓存命中的那次加载功能少一半,冷抓取却正常"。
151
+ *
152
+ * 版本不符按**无缓存**处理:宁可重新抓一次,也不读出一份被削过的数据。
153
+ */
154
+ export declare const REGISTRY_CACHE_FORMAT = 2;
155
+ /** 磁盘缓存的落盘形状。 */
156
+ export interface RegistryCacheFile {
157
+ /** 落盘时刻(epoch ms)——文件年龄。 */
158
+ readonly savedAt: number;
159
+ /** 索引自身生成时间(ISO);未知时 null。 */
160
+ readonly generatedAt: string | null;
161
+ readonly repos: readonly RegistryRepo[];
162
+ /** 形状对不上而被丢弃的记录数(如实记账用;0 表示整份缓存都可用)。 */
163
+ readonly skipped: number;
164
+ }
165
+ /** 索引磁盘缓存路径(本插件自己的缓存目录,不与旧包共用)。 */
166
+ export declare function registryCachePath(): string;
167
+ /**
168
+ * 解析一条**已归一化**的缓存记录(我们自己的记录形)。
169
+ *
170
+ * 与 {@link normalizeRegistryRepo} 的唯一区别是字段名:那个读上游的 snake_case
171
+ * (stargazers_count / updated_at / risk_tier …),这个读我们自己写出去的名字
172
+ * (stars / updatedAt / riskTier …)。**两者不能互相复用**——这正是本轮缺陷的成因。
173
+ *
174
+ * 校验仍然严格:缓存是外部文件(用户可能手改、磁盘可能截断),形状对不上的条目
175
+ * 一律丢弃并计数,绝不产出一条"看着像记录、其实缺了半数字段"的对象。
176
+ *
177
+ * @param raw - 缓存文件里的一条记录。
178
+ * @returns 归一化条目;形状不可用时 null。
179
+ */
180
+ export declare function normalizeCachedRepo(raw: unknown): RegistryRepo | null;
181
+ /**
182
+ * 解析缓存文件里的记录列表(我们自己的记录形):严格校验 + 按 repo 去重。
183
+ *
184
+ * @param list - 缓存文件里的 repos 数组。
185
+ * @returns 记录与丢弃计数;没有任何可用记录时 null。
186
+ */
187
+ export declare function parseCachedRepos(list: unknown): {
188
+ repos: RegistryRepo[];
189
+ skipped: number;
190
+ } | null;
191
+ /**
192
+ * 读取磁盘缓存;不存在/损坏/格式版本不符/形状不对时返回 null。
193
+ *
194
+ * 三条边界:
195
+ * 1. **不抛错**:缓存是可重建的派生物,抛错会把一次网络抖动升级成市场页整页失败;
196
+ * 2. **格式版本不符即视为无缓存**(旧文件按新读法读会得到一份被削过的数据,宁可重抓);
197
+ * 3. **按我们自己的记录形解析**({@link parseCachedRepos}),不再复用上游解析器——
198
+ * 复用会让 11 个字段在往返里静默消失(本轮 task-69 的根因)。
199
+ */
200
+ export declare function readRegistryCacheFile(): RegistryCacheFile | null;
201
+ /**
202
+ * 写入磁盘缓存(紧凑 JSON)。
203
+ *
204
+ * 写失败**不抛**:缓存是派生物,磁盘满/无权限不该让一次成功的抓取变成失败。
205
+ *
206
+ * @returns 是否真的写成功了(调用方据此在 notes 里如实记账)。
207
+ */
208
+ export declare function writeRegistryCacheFile(entry: {
209
+ readonly savedAt: number;
210
+ readonly generatedAt: string | null;
211
+ readonly repos: readonly RegistryRepo[];
212
+ }): boolean;
213
+ /**
214
+ * 缓存是否新鲜(纯函数,TTL 语义的唯一实现处)。
215
+ *
216
+ * 两条都要满足:**文件年龄**在 TTL 内(savedAt),且**内容年龄**(generatedAt,若可知)也在 TTL 内。
217
+ * 只看 savedAt 会让"今天才存下来的三天前索引"被当成新鲜;只看 generatedAt 则无法处理
218
+ * 载荷不带生成时间的来源(那时只能靠文件年龄)。
219
+ *
220
+ * @param entry - 缓存的 savedAt 与 generatedAt。
221
+ * @param now - 当前时刻(epoch ms),由调用方注入以便测试。
222
+ * @param ttlMs - 有效期。
223
+ */
224
+ export declare function isRegistryCacheFresh(entry: {
225
+ readonly savedAt: number;
226
+ readonly generatedAt: string | null;
227
+ }, now: number, ttlMs: number): boolean;
228
+ /**
229
+ * 候选索引是否允许覆盖已缓存的那个(M14,纯函数)。
230
+ *
231
+ * 规则只有一条:**只许更新,不许回退**。
232
+ * - 候选没有 generatedAt:无法证明它不旧,拒绝落盘(宁可下次再抓,也不拿一个无法判断年龄的
233
+ * 快照覆盖掉已知新鲜的缓存);
234
+ * - 没有缓存:允许;
235
+ * - 有缓存:候选的 generatedAt 必须 >= 缓存的那个。
236
+ */
237
+ export declare function shouldPersistRegistryIndex(candidateGeneratedAt: string | null, cachedGeneratedAt: string | null): boolean;
238
+ /** 一次索引加载的结果。 */
239
+ export interface RegistryIndex {
240
+ readonly repos: readonly RegistryRepo[];
241
+ /** 索引自身生成时间(ISO);未知时 null。 */
242
+ readonly generatedAt: string | null;
243
+ /** 本进程读到这份数据的时刻(epoch ms)。 */
244
+ readonly savedAt: number;
245
+ /** true = 来自磁盘缓存(本次没有走网络)。 */
246
+ readonly cached: boolean;
247
+ /** true = 缓存已过期(TTL 之外)但仍被采用——UI 应如实显示"数据可能过时"。 */
248
+ readonly stale: boolean;
249
+ /** 数据来源标识:`network:<id>` / `cache` / `empty`。 */
250
+ readonly source: string;
251
+ /** 逐跳的失败原因与如实记账(不静默吞掉)。 */
252
+ readonly notes: readonly string[];
253
+ /** 进程内镜像的内容代际(每次写入 +1);缓存键的内容身份之一。 */
254
+ readonly generation: number;
255
+ /** 被丢弃的索引条目数。 */
256
+ readonly skipped: number;
257
+ }
258
+ /** 加载入参。 */
259
+ export interface LoadRegistryIndexOptions {
260
+ /** 忽略新鲜缓存,强制走网络(用户点"刷新")。 */
261
+ readonly refresh?: boolean;
262
+ readonly ttlMs?: number;
263
+ readonly timeoutMs?: number;
264
+ readonly totalBudgetMs?: number;
265
+ readonly indexUrl?: string;
266
+ readonly branch?: string;
267
+ /** 注入当前时刻(测试用)。 */
268
+ readonly now?: number;
269
+ /** 注入抓取器(测试用);默认走 net.ts 的 fetchWithProxy。 */
270
+ readonly fetcher?: Fetcher;
271
+ /** 注入环境变量(token 读取用)。 */
272
+ readonly env?: Readonly<Record<string, string | undefined>>;
273
+ }
274
+ /**
275
+ * 内容哈希(FNV-1a,32 位十六进制)。
276
+ *
277
+ * 逐段就地计算,不拼接中间大字符串。段间混入一个分隔符,避免 (`ab`,`c`) 与 (`a`,`bc`) 撞。
278
+ * 放在 registry.ts 而不是 marketplace.ts:内容身份首先是"索引内容"的概念,
279
+ * 下游(管线缓存键)只是复用同一个哈希。
280
+ */
281
+ export declare function hashIdentity(parts: readonly string[]): string;
282
+ /** 当前镜像的内容代际;无镜像时为 0。 */
283
+ export declare function registryGeneration(): number;
284
+ /** 清空进程内镜像与在途状态(测试与插件卸载用)。 */
285
+ export declare function resetRegistryMemory(): void;
286
+ /**
287
+ * 加载索引:新鲜镜像 → 新鲜磁盘缓存 → 网络兜底链 → 过期磁盘缓存 → 空。
288
+ *
289
+ * 每一跳的结果都记进 `notes`:调用方(诊断页/市场页)能把"这次数据是怎么来的、
290
+ * 哪几跳失败了"如实展示给用户,而不是给一个空列表让人以为是"没有插件"。
291
+ *
292
+ * @param options - 见 {@link LoadRegistryIndexOptions}。
293
+ * @returns 索引结果;全部来源都不可用时返回空 items 的 `source: 'empty'`(**不抛错**)。
294
+ */
295
+ export declare function loadRegistryIndex(options?: LoadRegistryIndexOptions): Promise<RegistryIndex>;