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,248 @@
1
+ /**
2
+ * marketplace.ts — 索引 × 已安装状态 → MarketItem[](服务端合并、已安装判定、分类计数、内容身份缓存)。
3
+ *
4
+ * 归属:A 类·重写(旧 src/marketplaceMerge.ts 仅作意图参考,未复制代码)。
5
+ * 官方复用:paths.ts 的 dshHome/environmentDir/readEnvironmentManifest(本模块不自造路径与 manifest 解析)。
6
+ * 旧实现参考:dsh-web-plugin-manager/src/marketplaceMerge.ts(理解意图用,未复制代码)——它解决的问题是:
7
+ * 索引只描述"世界上有哪些插件",而页面要回答"这台机器上装了什么、能不能更新",
8
+ * 于是需要一处把两侧合并、判定已安装(包名 / repository / git 源 / 目录探测四条通道),
9
+ * 并给出分类计数供筛选器渲染。
10
+ * 前提检查:旧实现的前提仍然成立,但它踩过的两个坑必须避开(docs/private/audit/correctness.md):
11
+ * 1) M-1:最终管线的缓存键**不含条目内容身份**,只用了时间戳。于是"同一时间戳 + 完全不同的条目"
12
+ * 会命中旧缓存,新抓到的结果被静默丢弃。本实现的内容身份由 **条目的内容哈希** +
13
+ * 调用方给的单调代际共同构成(见 itemsIdentity / marketplaceCacheKey),时间戳不再当身份用。
14
+ * 2) m-3:把"缓存项写入时刻"当内容代际,导致内容没变也会重跑整条管线。本实现的身份来自
15
+ * 索引内容(hash)与**已安装索引的内容身份**(InstalledIndex.identity):TTL 到期重建但内容
16
+ * 不变时,身份不变、管线不重跑。
17
+ * 与旧实现的差异(由新契约决定):不做 catalog/dsh.so 覆盖层与同包去重("不要扩数据源"),
18
+ * 不读 kinds.ts 的安装记录(那是另一个模块的资产,且本模块必须能在没有它时独立工作)。
19
+ *
20
+ * 接线方式(host 侧一行即可):
21
+ * const index = await loadRegistryIndex({ refresh })
22
+ * const result = cachedMarketplace({
23
+ * profile, items: index.repos.map(registryItem), generation: index.generation,
24
+ * installed: buildInstalledIndex(profile), generatedAt: index.generatedAt, cached: index.cached,
25
+ * })
26
+ */
27
+ import { type RegistryRepo } from './registry.ts';
28
+ import type { MarketItem, MarketplaceResult } from './types.ts';
29
+ /** 已安装索引的进程内缓存有效期(毫秒)。 */
30
+ export declare const INSTALLED_INDEX_TTL_MS = 5000;
31
+ /** 技能与预设的落地根目录(与 kinds.ts 的安装目标一致)。 */
32
+ export declare const SKILLS_DIR = "skills";
33
+ export declare const PRESETS_DIR = ".agent-presets";
34
+ /**
35
+ * 市场候选条目:wire 契约(types.ts 的 MarketItem)+ **仅服务端可见**的 npm 包名。
36
+ *
37
+ * 为什么需要这个扩展:判定"已安装"要用 npm 包名去比对 profile 的依赖,而包名常常与
38
+ * 仓库名不同(`@scope/tool` vs `alice/dsh-tool`)。wire 契约已定稿、没有这个字段,
39
+ * 所以它在服务端内部流转,`finalizeMarketplace` 投影回 wire 形状时被剥掉
40
+ * (见 toWireItem)——不往契约里塞未声明的字段。
41
+ */
42
+ export interface MarketplaceCandidate extends MarketItem {
43
+ /** 索引里 CI 采集到的 npm 包名(registry.json 的 pkg_name)。 */
44
+ readonly packageName?: string;
45
+ }
46
+ /**
47
+ * 索引条目 → 市场候选条目(纯函数)。
48
+ *
49
+ * @param repo - 归一化后的索引条目。
50
+ * @returns 候选条目;`kind` 默认 `plugin`——这个索引本身就是 topic:dsh-plugin 的全量表,
51
+ * 上游若显式给了 kind 则透传。
52
+ */
53
+ export declare function registryItem(repo: RegistryRepo): MarketplaceCandidate;
54
+ /** 批量映射(唯一的"索引 → 条目"入口,避免各处自己拼对象)。 */
55
+ export declare function registryItems(repos: readonly RegistryRepo[]): MarketplaceCandidate[];
56
+ /**
57
+ * 一个条目该用哪个安装 spec —— **host 侧的唯一决定点**。
58
+ *
59
+ * 为什么必须有这么一个函数:官方 `parseInstallSpec` 只接受四种形态(registry 名 / 绝对路径 /
60
+ * git URL / tarball),而市场条目的天然键是 `owner/repo`——它**不在**这四种里,官方直接判
61
+ * invalid-spec 拒绝(实测:`拒绝安装:invalid-spec —— not a package name the registry accepts`)。
62
+ * 修法不是让客户端去拼字符串(那等于把官方的 spec 规则复制一份,索引字段一变要改两处),
63
+ * 而是让 host 在这里定好,客户端原样送。
64
+ *
65
+ * 规则(顺序即优先级):
66
+ * 1. 索引给了合法的 npm 包名 → 用它。npm 通道更快、可钉版本、不必整仓克隆。
67
+ * 2. 否则 → `github:` + owner/repo。这是官方认的 git 形态,永远可用(代价是整仓克隆)。
68
+ *
69
+ * 已知缺口(明确记账,不在本函数里假装解决):这里**不做**反抢注校验——npm 上的同名包未必
70
+ * 就是该仓库发布的。真正的校验要查 registry 的 repository 字段是否指回本仓库(竞品
71
+ * dsh-plugin-mall 专门为此做了一条规则),需要额外网络往返,属后续排期(见
72
+ * docs/private/market-benchmark.md §3.6)。
73
+ *
74
+ * @param repo - owner/repo。
75
+ * @param packageName - 索引采集到的 npm 包名(可选)。
76
+ * @returns 可直接交给官方 inspect/installBundle 的 spec。
77
+ */
78
+ export declare function installSpecFor(repo: string, packageName?: string): string;
79
+ /**
80
+ * 把候选条目投影回**严格的 wire 形状**。
81
+ *
82
+ * 单一出口的好处:缺失字段不会变成 undefined 键,上游 JSON 的形状漂移在这一处被收敛;
83
+ * `installSpec` 也在这里定死——客户端拿到的就是"该送什么",不需要(也不允许)自己拼。
84
+ */
85
+ export declare function toWireItem(item: MarketplaceCandidate): MarketItem;
86
+ /**
87
+ * 解析 package.json 的 repository 字段为 `owner/repo`(小写)。
88
+ *
89
+ * 支持 npm 生态里的全部常见写法:`owner/repo` 简写、`github:owner/repo`、
90
+ * `git+https://github.com/owner/repo.git`、`git://…`、`git@github.com:owner/repo.git`、
91
+ * 以及裸 URL。**非 GitHub 主机一律返回 null**:市场索引里的 `repo` 都是 GitHub 全名,
92
+ * 拿一个 GitLab 仓库去匹配只会制造假的"已安装"。
93
+ *
94
+ * @param value - repository 字段(字符串或对象里的 url 都可,调用方负责取字符串)。
95
+ * @returns 小写 `owner/repo`;无法判定时 null。
96
+ */
97
+ export declare function normalizeRepoRef(value: unknown): string | null;
98
+ /**
99
+ * git 源的身份:`github.com-owner-repo`。
100
+ *
101
+ * 两侧都用**同一个函数**生成身份,所以仓库名里带连字符也不会歧义(不拆 owner/repo,
102
+ * 只做"整段小写 + 去掉路径分隔"的规范化)。除了依赖声明里的 git spec,也认旧 git-cache
103
+ * 目录名形态(`…/github.com-owner-repo`),因为官方安装通道会把 git 源链接到这个目录。
104
+ *
105
+ * @param source - 依赖声明里的 source 值(如 `github:owner/repo`、`link:…`)。
106
+ * @returns 身份串;不是 github 源时 null。
107
+ */
108
+ export declare function gitSourceIdentity(source: unknown): string | null;
109
+ /**
110
+ * 目录名 slug(技能/预设的落地目录名)。
111
+ *
112
+ * 规则刻意简单(小写 + 非字母数字折叠成 `-`):它必须与写入侧的落地目录名一致,
113
+ * 因此不含任何 locale 相关处理。**注意**:本函数只用于"探测已安装",探测不到只丢一个
114
+ * 提示(假阴性),绝不误报(假阳性会让用户以为装过了)。
115
+ */
116
+ export declare function directorySlug(name: string): string;
117
+ /** 一个 profile 的已安装事实(构建一次,复用多次判定)。 */
118
+ export interface InstalledIndex {
119
+ /** 小写 npm 包名 → 已安装版本(版本未知时为空串)。 */
120
+ readonly packages: ReadonlyMap<string, string>;
121
+ /** 小写 `owner/repo`(来自各已装包的 repository 字段)→ 版本。 */
122
+ readonly repos: ReadonlyMap<string, string>;
123
+ /** git 源身份 → 版本。 */
124
+ readonly gitSources: ReadonlyMap<string, string>;
125
+ /** `<dshHome>/skills` 下的目录名(小写)。 */
126
+ readonly skills: ReadonlySet<string>;
127
+ /** `<dshHome>/.agent-presets` 下的目录名(小写)。 */
128
+ readonly presets: ReadonlySet<string>;
129
+ /**
130
+ * 内容身份:由上面五个成员的内容决定。
131
+ * TTL 到期重建但内容没变时**保持不变**,因此不会让下游管线缓存无谓失效(旧审计 m-3)。
132
+ */
133
+ readonly identity: string;
134
+ }
135
+ /**
136
+ * 条目集合的**内容身份**(缓存键的一半)。
137
+ *
138
+ * 长度进键是刻意的:哈希只是内容的指纹,长度把"哈希恰好相撞且长度不同"这种可能彻底排除。
139
+ *
140
+ * @param items - 候选条目。
141
+ * @returns `<count>-<hash>`。
142
+ */
143
+ export declare function itemsIdentity(items: readonly MarketplaceCandidate[]): string;
144
+ /**
145
+ * 构建(或复用)一个 profile 的已安装索引。
146
+ *
147
+ * 未知/非法 profile 名返回 null:调用方据此把全部条目判为"未安装",而不是抛错。
148
+ * 非法名先经 paths.ts 的 environmentDir 校验(路径穿越防线不在这里重造)。
149
+ *
150
+ * @param profile - profile 名。
151
+ * @param options - ttlMs(默认 {@link INSTALLED_INDEX_TTL_MS})与 now(注入时钟,测试用)。
152
+ * @returns 已安装索引,或 null(profile 不可用)。
153
+ */
154
+ export declare function buildInstalledIndex(profile: string, options?: {
155
+ readonly ttlMs?: number;
156
+ readonly now?: number;
157
+ }): InstalledIndex | null;
158
+ /** 丢弃一个 profile(或全部)的已安装索引缓存。装/卸/更新后必须调用,不能等 TTL。 */
159
+ export declare function invalidateInstalledIndex(profile?: string): void;
160
+ /** 清空已安装索引缓存(测试用,等价于 invalidateInstalledIndex())。 */
161
+ export declare function clearInstalledIndexCache(): void;
162
+ /** 已安装判定的附加输入。 */
163
+ export interface InstallDetectionOptions {
164
+ /**
165
+ * 目录探测要跳过的 slug:同一个 slug 在本次 listing 里对应多个条目时无法安全归属,
166
+ * 宁可少标一个"已安装"也不给错的人贴标签。
167
+ */
168
+ readonly ambiguousSlugs?: ReadonlySet<string>;
169
+ }
170
+ /**
171
+ * 判定一个条目是否已安装,并补齐已安装版本 / 形态(纯函数:只读索引,不碰磁盘)。
172
+ *
173
+ * 四条通道(任一命中即已安装):
174
+ * 1. **repository 身份**:条目 `repo`(owner/repo)命中任一已装包的 repository 字段;
175
+ * 2. **包名**:条目的 npm 包名(索引 pkg_name)或仓库名命中已装包名——包名与仓库名不一致时
176
+ * 这两条通道互补,覆盖对方看不见的情况;
177
+ * 3. **git 源**:依赖声明是 `github:…` / `git+https://github.com/…` / link 到 git-cache 目录;
178
+ * 4. **目录探测**:`<dshHome>/skills` 或 `<dshHome>/.agent-presets` 下存在对应 slug,
179
+ * 命中时形态改写为 skill / agent-preset(这正是"装成什么"的答案)。
180
+ *
181
+ * 目录探测的**已知假阴性**(与 kinds.ts 核对过落地名规则后确认):skill 的落地目录名优先取
182
+ * SKILL.md frontmatter 里的 `name`,只有缺失时才回落到仓库名末段的 slug。frontmatter 名与仓库名
183
+ * 不同时(如仓库 who/skill-pack、SKILL.md 里 name: memory-keeper),这里探不到——市场索引不携带
184
+ * SKILL.md 内容,凭空猜一个名字只会制造假阳性。要彻底修好需要把 kinds.ts 的安装记录作为第五条
185
+ * 通道传进来(loadKindRecords() 的 repo → kind/version),那需要同时把记录的内容身份纳入管线缓存键;
186
+ * 本模块先不做,等宿主侧确实需要时再加(假阴性只影响一个徽标,假阳性会误导用户)。
187
+ *
188
+ * @param item - 候选条目。
189
+ * @param index - 已安装索引;null(未知 profile)时一律判为未安装。
190
+ * @param options - 歧义 slug 集。
191
+ * @returns 新的条目对象(不修改入参)。
192
+ */
193
+ export declare function flagInstalled(item: MarketplaceCandidate, index: InstalledIndex | null, options?: InstallDetectionOptions): MarketplaceCandidate;
194
+ /** 最终管线的入参。 */
195
+ export interface MarketplaceInput {
196
+ /** 环境名;只用于选择管线缓存的槽位(索引内容与 profile 无关)。 */
197
+ readonly profile: string;
198
+ readonly items: readonly MarketplaceCandidate[];
199
+ /**
200
+ * 调用方给的内容代际(如 registry.ts 的 `RegistryIndex.generation`)。
201
+ * 它单调递增,因此"内容变了但哈希恰好相同"这种事也不会发生。
202
+ */
203
+ readonly generation: number;
204
+ readonly installed: InstalledIndex | null;
205
+ /** 索引生成时间(ISO);未知时 null(wire 上是空串)。 */
206
+ readonly generatedAt: string | null;
207
+ /** 本次数据是否来自缓存。 */
208
+ readonly cached: boolean;
209
+ /** 数据来源标识(registry.ts 的 RegistryIndex.source);缺失时不写进结果。 */
210
+ readonly source?: string;
211
+ /** 数据是否已过期(来自过期缓存,或全部来源失败)。 */
212
+ readonly stale?: boolean;
213
+ /** 逐跳失败原因(registry.ts 的 RegistryIndex.notes);这里会截断到上限。 */
214
+ readonly notes?: readonly string[];
215
+ }
216
+ /** notes 送到 UI 的上限:多到能说清"哪几跳失败",又不至于把工具栏淹掉。 */
217
+ export declare const MARKET_NOTES_LIMIT = 4;
218
+ /**
219
+ * 管线缓存键:profile | 代际 | **条目内容身份** | 已安装索引身份 | 索引生成时间 | 缓存标记 | 来源 | 过期。
220
+ *
221
+ * 后两项也必须进键:两份内容完全相同的索引(同一份磁盘缓存)在"网络刚成功"与"六跳全失败后回退"
222
+ * 两种情形下,notes/stale 是不同的——键里不带它们,后到的失败原因会被先到的成功结果顶掉。
223
+ *
224
+ * 内容身份是这一处的核心(旧审计 M-1:键里只有时间戳,于是"同一时间戳 + 不同条目"命中旧结果,
225
+ * 新数据被静默丢弃)。这里同时带上代际与内容哈希:代际负责"同内容不重算",哈希负责
226
+ * "不同内容一定重算",两者互补。
227
+ */
228
+ export declare function marketplaceCacheKey(input: MarketplaceInput): string;
229
+ /**
230
+ * 最终管线(纯函数,不做缓存):标已安装 → 投影回 wire 形状 → 分类计数。
231
+ *
232
+ * 分类计数在**最终**条目集上算(投影之后),因此筛选器列出的分类与卡片能显示的分类完全一致。
233
+ *
234
+ * @param input - 见 {@link MarketplaceInput}。
235
+ * @returns wire 结果。
236
+ */
237
+ export declare function finalizeMarketplace(input: MarketplaceInput): MarketplaceResult;
238
+ /**
239
+ * 带缓存的最终管线。
240
+ *
241
+ * 命中时返回**同一个对象实例**:调用方(REST 层的序列化缓存)可以拿它当 key。
242
+ *
243
+ * @param input - 见 {@link MarketplaceInput}。
244
+ * @returns wire 结果。
245
+ */
246
+ export declare function cachedMarketplace(input: MarketplaceInput): MarketplaceResult;
247
+ /** 清空管线缓存(测试 / 插件卸载用)。 */
248
+ export declare function clearMarketplaceCache(): void;