@eya46/dsh-plugin-info 0.0.2 → 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.
- package/README.md +11 -3
- package/lib/client.js +138 -15
- package/lib/index.js +216 -13
- package/lib/types/client.d.ts +38 -0
- package/lib/types/index.d.ts +3 -3
- package/lib/types/profile.d.ts +24 -1
- package/lib/types/semver.d.ts +2 -0
- package/lib/types/service.d.ts +19 -0
- package/lib/types/types.d.ts +19 -4
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -2,15 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的插件信息页。包名 `@eya46/dsh-plugin-info`,挂到当前 Profile 后,设置里会出现「插件信息」分区。
|
|
4
4
|
|
|
5
|
-
它只看 **Profile 用户自己加的插件**(`profiles/<name>/package.json` 的 `dependencies`),不把 `@deepseek-ai/dsh-base` 这类随发行版带的 bundle
|
|
5
|
+
它只看 **Profile 用户自己加的插件**(`profiles/<name>/package.json` 的 `dependencies`),不把 `@deepseek-ai/dsh-base` 这类随发行版带的 bundle 算进去。默认展示全部用户插件;如需只看特定命名空间,可配置 `allowScopes`。
|
|
6
6
|
|
|
7
7
|
- 列出已安装插件、当前版本、来源(npm / 本地 / git)
|
|
8
8
|
- 对 registry 安装的包对照 `dist-tags.latest`,标出可更新
|
|
9
|
+
- 对 **GitHub Release 直装**的包(依赖是 `github.com/…/releases/download/<tag>/<file>.tgz` 链接),从依赖链接里解析出仓库和当前版本,对照该仓库的最新 Release 标出可更新——仓库与版本全部取自 profile 里的依赖信息和已安装的包信息,无需任何按插件配置;更新命令直接指向按新版本重写过的 tarball 链接,版本列表也会展示该仓库最近的 Release,点开可读对应 Release 说明
|
|
10
|
+
- 可更新时一键复制 `dsh plugin --profile <当前 profile> add <包>@latest`(profile 从当前运行的 Profile 清单解析,web 端通常是 `web`)
|
|
9
11
|
- 展开后默认看最近 **10** 条版本
|
|
10
12
|
- 点某个版本读更新说明:优先 GitHub Release,没有就回落到相邻版本的 compare / commit
|
|
11
13
|
|
|
12
14
|
实现方式对齐 [dsh-model-meta-autofill](https://github.com/QJAG1024/dsh-model-meta-autofill):Host 注册本机 HTTP API,Client half 通过 `dsh.client` 挂到 `settings.section`。
|
|
13
15
|
|
|
16
|
+
### 可选:dsh-better-sidebar 标签页
|
|
17
|
+
|
|
18
|
+
若 Profile 同时安装了 [`dsh-better-sidebar`](https://github.com/omdsh-dev/DSH-better-sidebar) (≥0.4.0),本插件会自动在侧边栏 `+` 菜单里多注册一个**插件信息**标签页(id `@eya46/dsh-plugin-info`,order 90,单实例),内容与设置页完全一致——侧边栏较窄,内部已做滚动/宽度适配。
|
|
19
|
+
|
|
20
|
+
实现是**软依赖**:不写 `peerDependencies`,不 `import 'dsh-better-sidebar'`,只用 Cordis 的 `ctx.inject(['betterSidebar'], ...)` 开一个子 fiber——未安装时回调永不触发,其他功能不受影响;禁用 / HMR 任一插件时 disposer 随各自 fiber 清理,不会残留 `"already registered"`。
|
|
21
|
+
|
|
14
22
|
## 安装
|
|
15
23
|
|
|
16
24
|
在仓库根目录构建后,把它加进当前 Profile:
|
|
@@ -29,7 +37,7 @@ dsh plugin --profile web add "file:./packages/dsh-plugin-info"
|
|
|
29
37
|
|
|
30
38
|
| 端点 | 说明 |
|
|
31
39
|
| --- | --- |
|
|
32
|
-
| `GET /plugin-info/api/plugins` | 当前 Profile 用户插件 +
|
|
40
|
+
| `GET /plugin-info/api/plugins` | 当前 Profile 用户插件 + 是否有更新;含 `profileCli`,可更新的 registry 包还会带 `updateCommand` |
|
|
33
41
|
| `GET /plugin-info/api/versions?name=<pkg>&limit=10` | 最近版本,默认 10 条 |
|
|
34
42
|
| `GET /plugin-info/api/notes?name=<pkg>&version=<ver>` | 该版本的更新说明 |
|
|
35
43
|
|
|
@@ -41,7 +49,7 @@ dsh plugin --profile web add "file:./packages/dsh-plugin-info"
|
|
|
41
49
|
| `defaultVersionLimit` | `10` | 默认版本条数 |
|
|
42
50
|
| `timeoutMs` | `15000` | 注册表 / GitHub 超时 |
|
|
43
51
|
| `cacheTtlMs` | `300000` | packument 与说明缓存 |
|
|
44
|
-
| `allowScopes` |
|
|
52
|
+
| `allowScopes` | 无(展示全部) | 名称前缀白名单;省略或为空数组时展示全部用户插件 |
|
|
45
53
|
|
|
46
54
|
覆盖写在 Profile 的 `cordis.patch.yml`:
|
|
47
55
|
|
package/lib/client.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module @eya46/dsh-plugin-info/client
|
|
9
9
|
*/
|
|
10
|
+
/** Labels for the MarkdownText copy button / footnotes heading (settings UI is zh). */
|
|
11
|
+
const MARKDOWN_LABELS = {
|
|
12
|
+
code: { copyLabel: '复制代码', copiedLabel: '已复制' },
|
|
13
|
+
footnotes: '脚注',
|
|
14
|
+
};
|
|
10
15
|
;
|
|
11
16
|
(() => {
|
|
12
17
|
const loader = window.__ModuleLoader__;
|
|
@@ -25,12 +30,17 @@
|
|
|
25
30
|
'.dpi-rowButton { height: 28px; padding: 0 10px; font-size: 12px; line-height: 18px; }',
|
|
26
31
|
'.dpi-secondaryButton:hover:not(:disabled), .dpi-rowButton:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-solid); }',
|
|
27
32
|
'.dpi-secondaryButton:disabled, .dpi-rowButton:disabled { opacity: 0.4; cursor: default; }',
|
|
28
|
-
'.dpi-secondaryButton:focus-visible, .dpi-rowButton:focus-visible, .dpi-head:focus-visible, .dpi-version:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--dsw-alias-border-l3); }',
|
|
33
|
+
'.dpi-secondaryButton:focus-visible, .dpi-rowButton:focus-visible, .dpi-head:focus-visible, .dpi-version:focus-visible, .dpi-updateCopy:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--dsw-alias-border-l3); }',
|
|
29
34
|
'.dpi-status { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }',
|
|
30
35
|
'.dpi-banner { margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); }',
|
|
31
36
|
'.dpi-empty { color: var(--dsw-alias-label-tertiary); font-size: 14px; line-height: 22px; }',
|
|
32
37
|
'.dpi-cards { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 10px; }',
|
|
33
38
|
'.dpi-card { border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; overflow: hidden; }',
|
|
39
|
+
'.dpi-cardTop { display: flex; align-items: stretch; gap: 0; }',
|
|
40
|
+
'.dpi-cardTop .dpi-head { flex: 1; min-width: 0; }',
|
|
41
|
+
'.dpi-updateCopy { box-sizing: border-box; flex-shrink: 0; align-self: center; height: 28px; margin: 0 12px 0 0; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l3); border-radius: 14px; background: transparent; color: var(--dsw-alias-state-warn-label); font: inherit; font-size: 12px; line-height: 18px; cursor: pointer; }',
|
|
42
|
+
'.dpi-updateCopy:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }',
|
|
43
|
+
'.dpi-command { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); word-break: break-all; }',
|
|
34
44
|
'.dpi-head { box-sizing: border-box; width: 100%; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 12px 14px; border: none; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }',
|
|
35
45
|
'.dpi-head:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }',
|
|
36
46
|
'.dpi-name { font-size: 14px; line-height: 22px; font-weight: 500; }',
|
|
@@ -46,14 +56,52 @@
|
|
|
46
56
|
'.dpi-version:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }',
|
|
47
57
|
'.dpi-version[data-open="1"] { background: var(--dsw-alias-interactive-bg-hover-solid); }',
|
|
48
58
|
'.dpi-notes { display: flex; flex-direction: column; gap: 6px; padding: 8px 2px 2px; }',
|
|
49
|
-
'.dpi-notesBody { margin: 0; white-space: pre-wrap; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }',
|
|
50
59
|
'.dpi-commits { margin: 0; padding-left: 18px; font-size: 13px; line-height: 20px; }',
|
|
51
60
|
'.dpi-link { color: inherit; text-underline-offset: 3px; }',
|
|
52
61
|
'.dpi-copyTag { box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l3); color: var(--dsw-alias-label-secondary); background: transparent; border-radius: 4px; padding: 1px 6px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; line-height: 16px; cursor: pointer; }',
|
|
53
62
|
'.dpi-copyTag:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }',
|
|
63
|
+
// Sidebar tab wrapper: the panel sits in a narrow right-side column
|
|
64
|
+
// (default 420px), where PluginInfoPanel's 720px max-width and
|
|
65
|
+
// non-scrolling body would overflow. Force full width + own scroll.
|
|
66
|
+
'.dpi-sidebar { box-sizing: border-box; width: 100%; height: 100%; overflow-y: auto; overflow-x: hidden; padding: 12px 14px 16px; background: var(--dsw-alias-bg-base); }',
|
|
67
|
+
'.dpi-sidebar .dpi-section { max-width: none; gap: 10px; }',
|
|
68
|
+
'.dpi-sidebar .dpi-card .dpi-head { padding: 10px 12px; }',
|
|
69
|
+
'.dpi-sidebar .dpi-card .dpi-body { padding: 0 12px 12px; }',
|
|
70
|
+
'.dpi-sidebar .dpi-actions { gap: 6px; }',
|
|
71
|
+
'.dpi-sidebar .dpi-rowButton { height: 26px; padding: 0 8px; }',
|
|
72
|
+
'.dpi-sidebar .dpi-updateCopy { height: 26px; margin-right: 10px; padding: 0 8px; }',
|
|
54
73
|
].join('\n');
|
|
55
74
|
const React = require('react');
|
|
75
|
+
// Reuse DSH's shared untrusted-Markdown renderer instead of rolling our
|
|
76
|
+
// own — it ships in the web shell's static module table and handles GFM,
|
|
77
|
+
// math, code fences, and sanitization (raw HTML / unsafe URLs stripped).
|
|
78
|
+
const uiPrimitives = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
79
|
+
const MarkdownText = uiPrimitives.MarkdownText;
|
|
56
80
|
const h = React.createElement.bind(React);
|
|
81
|
+
const ReactComponent = React.Component;
|
|
82
|
+
/**
|
|
83
|
+
* MarkdownText is an undocumented shell primitive whose render can throw
|
|
84
|
+
* (labels shape drift, markdown edge cases). Contain any failure to this
|
|
85
|
+
* one note body instead of crashing the whole settings section.
|
|
86
|
+
*/
|
|
87
|
+
class SafeMarkdown extends ReactComponent {
|
|
88
|
+
constructor(props) {
|
|
89
|
+
super(props);
|
|
90
|
+
this.state = { failed: false };
|
|
91
|
+
}
|
|
92
|
+
static getDerivedStateFromError() {
|
|
93
|
+
return { failed: true };
|
|
94
|
+
}
|
|
95
|
+
componentDidCatch(error) {
|
|
96
|
+
console.warn('[dsh-plugin-info] MarkdownText failed; falling back to plain text', error);
|
|
97
|
+
}
|
|
98
|
+
render() {
|
|
99
|
+
if (this.state.failed === true) {
|
|
100
|
+
return h('pre', { className: 'dpi-id', style: { whiteSpace: 'pre-wrap', margin: 0 } }, this.props.text);
|
|
101
|
+
}
|
|
102
|
+
return h(MarkdownText, { text: this.props.text, labels: MARKDOWN_LABELS });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
57
105
|
const ensureStyles = () => {
|
|
58
106
|
if (document.querySelector('style[data-plugin="@eya46/dsh-plugin-info"]') !== null)
|
|
59
107
|
return;
|
|
@@ -135,17 +183,18 @@
|
|
|
135
183
|
const [notes, setNotes] = React.useState({});
|
|
136
184
|
const [notesBusy, setNotesBusy] = React.useState('');
|
|
137
185
|
const [copied, setCopied] = React.useState({});
|
|
138
|
-
const
|
|
186
|
+
const [profileCli, setProfileCli] = React.useState('web');
|
|
187
|
+
const copyText = React.useCallback((key, text) => {
|
|
139
188
|
const write = () => {
|
|
140
189
|
if (typeof navigator !== 'undefined' && navigator.clipboard !== undefined) {
|
|
141
|
-
return navigator.clipboard.writeText(
|
|
190
|
+
return navigator.clipboard.writeText(text);
|
|
142
191
|
}
|
|
143
192
|
return Promise.reject(new Error('clipboard unavailable'));
|
|
144
193
|
};
|
|
145
194
|
write()
|
|
146
195
|
.then(() => {
|
|
147
|
-
setCopied((prev) => ({ ...prev, [
|
|
148
|
-
window.setTimeout(() => setCopied((prev) => ({ ...prev, [
|
|
196
|
+
setCopied((prev) => ({ ...prev, [key]: true }));
|
|
197
|
+
window.setTimeout(() => setCopied((prev) => ({ ...prev, [key]: false })), 1200);
|
|
149
198
|
})
|
|
150
199
|
.catch(() => { });
|
|
151
200
|
}, []);
|
|
@@ -157,6 +206,8 @@
|
|
|
157
206
|
if (!Array.isArray(data.plugins))
|
|
158
207
|
throw new Error(data.message ?? 'unexpected response');
|
|
159
208
|
setRows(data.plugins);
|
|
209
|
+
if (typeof data.profileCli === 'string' && data.profileCli.length > 0)
|
|
210
|
+
setProfileCli(data.profileCli);
|
|
160
211
|
setError('');
|
|
161
212
|
})
|
|
162
213
|
.catch((caught) => setError(String(caught)))
|
|
@@ -212,7 +263,7 @@
|
|
|
212
263
|
const updateCount = rows.filter((row) => row.hasUpdate).length;
|
|
213
264
|
const children = [
|
|
214
265
|
h('h2', { key: 'title', className: 'dpi-title' }, '插件信息'),
|
|
215
|
-
h('p', { key: 'intro', className: 'dpi-intro' }, '当前 Profile
|
|
266
|
+
h('p', { key: 'intro', className: 'dpi-intro' }, '当前 Profile(' + profileCli + ')里用户添加的插件(不含随发行版内置的 bundle)。有更新时可一键复制 dsh plugin 命令;展开后默认看最近 10 个版本,点版本可看更新说明。'),
|
|
216
267
|
h('div', { key: 'toolbar', className: 'dpi-toolbar' }, h('button', { className: 'dpi-secondaryButton', disabled: busy, onClick: () => { load(); } }, busy ? '刷新中…' : '刷新'), h('span', { className: 'dpi-status' }, '共 ' + rows.length + ' 个插件' + (updateCount > 0 ? ' · ' + updateCount + ' 个可更新' : ''))),
|
|
217
268
|
];
|
|
218
269
|
if (error !== '')
|
|
@@ -232,14 +283,20 @@
|
|
|
232
283
|
if (plugin.hasUpdate && plugin.latestVersion !== undefined) {
|
|
233
284
|
head.push(h('span', { key: 'up', className: 'dpi-tag', 'data-tone': 'warn' }, '可更新 ' + plugin.latestVersion));
|
|
234
285
|
}
|
|
235
|
-
else if (plugin.
|
|
286
|
+
else if (!plugin.hasUpdate && plugin.latestVersion !== undefined) {
|
|
236
287
|
head.push(h('span', { key: 'ok', className: 'dpi-tag', 'data-tone': 'ok' }, '已是最新'));
|
|
237
288
|
}
|
|
289
|
+
const updateCommand = plugin.updateCommand
|
|
290
|
+
?? ('dsh plugin --profile ' + profileCli + ' add ' + plugin.name + '@latest');
|
|
291
|
+
const updateCopied = copied['update:' + plugin.name] === true;
|
|
238
292
|
const body = [];
|
|
239
293
|
if (expanded) {
|
|
240
294
|
if (plugin.description !== undefined)
|
|
241
295
|
body.push(h('p', { key: 'desc', className: 'dpi-meta' }, plugin.description));
|
|
242
296
|
body.push(h('p', { key: 'spec', className: 'dpi-meta' }, '依赖:', h('span', { className: 'dpi-id' }, plugin.spec), plugin.latestVersion === undefined ? null : ' · 最新 ' + plugin.latestVersion));
|
|
297
|
+
if (plugin.hasUpdate) {
|
|
298
|
+
body.push(h('p', { key: 'upd', className: 'dpi-command' }, updateCommand));
|
|
299
|
+
}
|
|
243
300
|
if (plugin.error !== undefined)
|
|
244
301
|
body.push(h('p', { key: 'perr', className: 'dpi-banner' }, plugin.error));
|
|
245
302
|
const links = [
|
|
@@ -247,7 +304,7 @@
|
|
|
247
304
|
key: 'copy',
|
|
248
305
|
className: 'dpi-copyTag',
|
|
249
306
|
title: '复制包名',
|
|
250
|
-
onClick: () => {
|
|
307
|
+
onClick: () => { copyText(plugin.name, plugin.name); },
|
|
251
308
|
}, copied[plugin.name] === true ? '已复制' : plugin.name),
|
|
252
309
|
h('button', {
|
|
253
310
|
key: 'vers',
|
|
@@ -255,6 +312,14 @@
|
|
|
255
312
|
onClick: () => { loadVersions(plugin.name); },
|
|
256
313
|
}, versions[plugin.name] === undefined ? '加载版本' : '重新加载版本'),
|
|
257
314
|
];
|
|
315
|
+
if (plugin.hasUpdate) {
|
|
316
|
+
links.push(h('button', {
|
|
317
|
+
key: 'upd',
|
|
318
|
+
className: 'dpi-rowButton',
|
|
319
|
+
title: updateCommand,
|
|
320
|
+
onClick: () => { copyText('update:' + plugin.name, updateCommand); },
|
|
321
|
+
}, updateCopied ? '已复制更新命令' : '复制更新命令'));
|
|
322
|
+
}
|
|
258
323
|
if (plugin.homepage !== undefined) {
|
|
259
324
|
links.push(h('a', { key: 'home', className: 'dpi-rowButton dpi-link', href: plugin.homepage, target: '_blank', rel: 'noreferrer' }, '主页'));
|
|
260
325
|
}
|
|
@@ -271,7 +336,7 @@
|
|
|
271
336
|
body.push(h('p', { key: 'vload', className: 'dpi-meta' }, '正在加载最近 10 个版本…'));
|
|
272
337
|
}
|
|
273
338
|
else if (pluginVersions.length === 0) {
|
|
274
|
-
body.push(h('p', { key: 'vempty', className: 'dpi-meta' }, plugin.source === 'registry' ? '注册表没有版本记录。' : '
|
|
339
|
+
body.push(h('p', { key: 'vempty', className: 'dpi-meta' }, plugin.source === 'registry' ? '注册表没有版本记录。' : '没有可展示的历史版本。'));
|
|
275
340
|
}
|
|
276
341
|
else {
|
|
277
342
|
const versionNodes = pluginVersions.map((row) => {
|
|
@@ -296,8 +361,9 @@
|
|
|
296
361
|
else if (note.source === 'github-release') {
|
|
297
362
|
if (note.title !== undefined)
|
|
298
363
|
noteChildren.push(h('p', { key: 'nt', className: 'dpi-meta' }, note.title));
|
|
299
|
-
if (note.body !== undefined)
|
|
300
|
-
noteChildren.push(h(
|
|
364
|
+
if (note.body !== undefined && note.body !== '') {
|
|
365
|
+
noteChildren.push(h(SafeMarkdown, { key: 'nb', text: note.body }));
|
|
366
|
+
}
|
|
301
367
|
if (note.url !== undefined) {
|
|
302
368
|
noteChildren.push(h('a', { key: 'nu', className: 'dpi-link', href: note.url, target: '_blank', rel: 'noreferrer' }, '在 GitHub 查看 Release'));
|
|
303
369
|
}
|
|
@@ -309,7 +375,10 @@
|
|
|
309
375
|
}
|
|
310
376
|
}
|
|
311
377
|
else {
|
|
312
|
-
|
|
378
|
+
const fallback = note.body ?? '这个版本没有找到 GitHub Release 或提交说明。';
|
|
379
|
+
// Error payloads and the "none" source are plain text; keep
|
|
380
|
+
// them literal rather than feeding them through Markdown.
|
|
381
|
+
noteChildren.push(h('p', { key: 'nnone', className: 'dpi-meta' }, fallback));
|
|
313
382
|
}
|
|
314
383
|
}
|
|
315
384
|
return h('div', { key: row.version }, h('button', {
|
|
@@ -321,15 +390,66 @@
|
|
|
321
390
|
body.push(h('div', { key: 'versions', className: 'dpi-versions' }, ...versionNodes));
|
|
322
391
|
}
|
|
323
392
|
}
|
|
324
|
-
cards.push(h('li', { key: plugin.name, className: 'dpi-card' }, h('button', {
|
|
393
|
+
cards.push(h('li', { key: plugin.name, className: 'dpi-card' }, h('div', { className: 'dpi-cardTop' }, h('button', {
|
|
325
394
|
className: 'dpi-head',
|
|
326
395
|
'aria-expanded': expanded,
|
|
327
396
|
onClick: () => { togglePlugin(plugin.name); },
|
|
328
|
-
}, ...head),
|
|
397
|
+
}, ...head), plugin.hasUpdate
|
|
398
|
+
? h('button', {
|
|
399
|
+
className: 'dpi-updateCopy',
|
|
400
|
+
title: updateCommand,
|
|
401
|
+
onClick: (event) => {
|
|
402
|
+
event.stopPropagation?.();
|
|
403
|
+
copyText('update:' + plugin.name, updateCommand);
|
|
404
|
+
},
|
|
405
|
+
}, updateCopied ? '已复制' : '复制更新命令')
|
|
406
|
+
: null), expanded ? h('div', { className: 'dpi-body' }, ...body) : null));
|
|
329
407
|
}
|
|
330
408
|
children.push(h('ul', { key: 'cards', className: 'dpi-cards' }, ...cards));
|
|
331
409
|
return h('div', { className: 'dpi-section' }, ...children);
|
|
332
410
|
};
|
|
411
|
+
/**
|
|
412
|
+
* Sidebar-tab wrapper: same content as the settings panel, but hosted in
|
|
413
|
+
* the narrow right-side better-sidebar pane where it must manage its own
|
|
414
|
+
* vertical scroll and drop the 720px max-width.
|
|
415
|
+
*/
|
|
416
|
+
const PluginInfoSidebarPanel = () => {
|
|
417
|
+
return h('div', { className: 'dpi-sidebar' }, h(PluginInfoPanel, null));
|
|
418
|
+
};
|
|
419
|
+
/** Small Cordis-plugin outline glyph (same geometry as the settings
|
|
420
|
+
* nav uses, suitable for the sidebar's 16px tab icon slot). */
|
|
421
|
+
const PluginIcon = (size) => {
|
|
422
|
+
return h('svg', {
|
|
423
|
+
width: size,
|
|
424
|
+
height: size,
|
|
425
|
+
viewBox: '0 0 14 14',
|
|
426
|
+
fill: 'none',
|
|
427
|
+
'aria-hidden': true,
|
|
428
|
+
}, h('path', {
|
|
429
|
+
fill: 'currentColor',
|
|
430
|
+
d: 'M3.03426 5.66661L1.70084 7.00003L3.0315 8.33069L2.14762 9.21457L-0.0669245 7.00003L2.15038 4.78273L3.03426 5.66661ZM7 14.067L4.77924 11.8462L5.66313 10.9623L7 12.2992L8.33342 10.9658L9.2173 11.8496L7 14.067ZM11.8489 9.21803L10.965 8.33414L12.2992 7.00003L10.9623 5.66316L11.8462 4.77927L14.0669 7.00003L11.8489 9.21803ZM8.33066 3.03153L7 1.70087L5.66589 3.03498L4.782 2.1511L7 -0.0668945L9.21454 2.14765L8.33066 3.03153Z',
|
|
431
|
+
}), h('rect', { x: 5.98535, y: 5.98535, width: 2.02942, height: 2.02942, fill: 'currentColor' }));
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* Register the sidebar tab ONLY when dsh-better-sidebar is present. The
|
|
435
|
+
* child fiber waits for the `betterSidebar` service; if that plugin is
|
|
436
|
+
* not installed the callback never runs and the rest of this plugin is
|
|
437
|
+
* unaffected. The disposer returned by registerTab is attached to the
|
|
438
|
+
* child fiber, so disabling/HMR of either side cleans up cleanly.
|
|
439
|
+
*/
|
|
440
|
+
const registerSidebarTab = (ctx) => {
|
|
441
|
+
ctx.inject(['betterSidebar'], ((sctx) => {
|
|
442
|
+
const service = sctx.betterSidebar;
|
|
443
|
+
sctx.effect(() => service.registerTab({
|
|
444
|
+
id: '@eya46/dsh-plugin-info',
|
|
445
|
+
title: '插件信息',
|
|
446
|
+
icon: (size) => PluginIcon(size),
|
|
447
|
+
order: 90,
|
|
448
|
+
single: true,
|
|
449
|
+
component: PluginInfoSidebarPanel,
|
|
450
|
+
}), '@eya46/dsh-plugin-info: better-sidebar tab');
|
|
451
|
+
}));
|
|
452
|
+
};
|
|
333
453
|
return {
|
|
334
454
|
name: 'dsh-plugin-info',
|
|
335
455
|
inject: ['slots'],
|
|
@@ -338,6 +458,9 @@
|
|
|
338
458
|
ctx.slots.inject('settings.section', () => {
|
|
339
459
|
ctx.slots.register({ name: 'settings.section', id: 'plugin-info', order: 16, label: '插件信息' }, PluginInfoPanel);
|
|
340
460
|
});
|
|
461
|
+
// Optional injection into dsh-better-sidebar: no-op when that
|
|
462
|
+
// plugin is absent from the profile.
|
|
463
|
+
registerSidebarTab(ctx);
|
|
341
464
|
},
|
|
342
465
|
};
|
|
343
466
|
},
|
package/lib/index.js
CHANGED
|
@@ -64,6 +64,11 @@ function compareParsed(left, right) {
|
|
|
64
64
|
}
|
|
65
65
|
return 0;
|
|
66
66
|
}
|
|
67
|
+
/** Version embedded in a release tag: `v2.0.3`, `2.0.3`, or `panel-1.20.3-rc.1`. */
|
|
68
|
+
function versionFromTag(tag) {
|
|
69
|
+
if (parseVersion(tag) !== void 0) return normalizeVersion(tag);
|
|
70
|
+
return /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/.exec(tag)?.[1];
|
|
71
|
+
}
|
|
67
72
|
/** True when `latest` is a newer published version than `installed`. */
|
|
68
73
|
function hasNewerVersion(installed, latest) {
|
|
69
74
|
if (installed === void 0 || latest === void 0) return false;
|
|
@@ -75,6 +80,28 @@ function hasNewerVersion(installed, latest) {
|
|
|
75
80
|
}
|
|
76
81
|
//#endregion
|
|
77
82
|
//#region src/profile.ts
|
|
83
|
+
/** Prefix DSH writes onto a profile package.json `name`. */
|
|
84
|
+
const PROFILE_PACKAGE_PREFIX = "dsh-profile-";
|
|
85
|
+
/**
|
|
86
|
+
* CLI `--profile` argument for this profile directory.
|
|
87
|
+
*
|
|
88
|
+
* DSH names the manifest `dsh-profile-<dir>` and the CLI flag is the
|
|
89
|
+
* directory basename (`web`, `tui`, …). Prefer the manifest prefix so temp
|
|
90
|
+
* test dirs still resolve; fall back to the folder name, then `web`.
|
|
91
|
+
*/
|
|
92
|
+
function profileCliName(manifestName, profileDir) {
|
|
93
|
+
if (typeof manifestName === "string" && manifestName.startsWith(PROFILE_PACKAGE_PREFIX)) {
|
|
94
|
+
const id = manifestName.slice(12).trim();
|
|
95
|
+
if (id.length > 0) return id;
|
|
96
|
+
}
|
|
97
|
+
const dirName = profileDir.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "";
|
|
98
|
+
if (dirName.length > 0 && dirName !== "." && dirName !== ".." && !/^[A-Za-z]:$/.test(dirName)) return dirName;
|
|
99
|
+
return "web";
|
|
100
|
+
}
|
|
101
|
+
/** `dsh plugin add …@latest` command for one registry package in this profile. */
|
|
102
|
+
function pluginUpdateCommand(profileCli, packageName) {
|
|
103
|
+
return `dsh plugin --profile ${profileCli} add ${packageName}@latest`;
|
|
104
|
+
}
|
|
78
105
|
/** Classify a profile dependency spec. */
|
|
79
106
|
function classifySpec(spec) {
|
|
80
107
|
const trimmed = spec.trim();
|
|
@@ -154,6 +181,47 @@ function parseGithubRepo(input) {
|
|
|
154
181
|
};
|
|
155
182
|
}
|
|
156
183
|
}
|
|
184
|
+
const GITHUB_RELEASE_SPEC = /^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/releases\/download\/([^/\s]+)\/([^/\s]+)$/i;
|
|
185
|
+
/**
|
|
186
|
+
* Parse a `github.com/<owner>/<repo>/releases/download/<tag>/<asset>`
|
|
187
|
+
* dependency spec into its repository, tag, and asset. Everything is derived
|
|
188
|
+
* from the spec itself — no per-plugin configuration.
|
|
189
|
+
*/
|
|
190
|
+
function parseGithubReleaseSpec(spec) {
|
|
191
|
+
const match = GITHUB_RELEASE_SPEC.exec(spec.trim());
|
|
192
|
+
if (match === null) return void 0;
|
|
193
|
+
const [rawOwner, rawRepo, rawTag, rawAsset] = match.slice(1);
|
|
194
|
+
if (rawOwner === void 0 || rawRepo === void 0 || rawTag === void 0 || rawAsset === void 0) return void 0;
|
|
195
|
+
try {
|
|
196
|
+
return {
|
|
197
|
+
owner: decodeURIComponent(rawOwner),
|
|
198
|
+
repo: decodeURIComponent(rawRepo),
|
|
199
|
+
tag: decodeURIComponent(rawTag),
|
|
200
|
+
asset: decodeURIComponent(rawAsset)
|
|
201
|
+
};
|
|
202
|
+
} catch {
|
|
203
|
+
return {
|
|
204
|
+
owner: rawOwner,
|
|
205
|
+
repo: rawRepo,
|
|
206
|
+
tag: rawTag,
|
|
207
|
+
asset: rawAsset
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Rewrite a release spec onto a newer tag. The asset file name keeps its
|
|
213
|
+
* shape: an embedded version token equal to `oldVersion` is replaced by
|
|
214
|
+
* `newVersion` (`dsh-skill-mcp-panel-2.0.3.tgz` → `…-2.0.4.tgz`) without
|
|
215
|
+
* touching longer version numbers (`12.0.3`) that merely contain it.
|
|
216
|
+
*/
|
|
217
|
+
function releaseDownloadUrl(spec, newTag, newVersion, oldVersion) {
|
|
218
|
+
return `https://github.com/${spec.owner}/${spec.repo}/releases/download/${encodeURIComponent(newTag)}/${encodeURIComponent(replaceVersionToken(spec.asset, oldVersion, newVersion))}`;
|
|
219
|
+
}
|
|
220
|
+
function replaceVersionToken(asset, from, to) {
|
|
221
|
+
if (from.length === 0 || from === to) return asset;
|
|
222
|
+
const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
223
|
+
return asset.replace(new RegExp(`(?<![\\d.])${escaped}(?!\\d)`), to);
|
|
224
|
+
}
|
|
157
225
|
//#endregion
|
|
158
226
|
//#region src/registry.ts
|
|
159
227
|
/** Join a registry origin with a package name, encoding the scoped slash. */
|
|
@@ -208,6 +276,8 @@ var PluginInfoService = class {
|
|
|
208
276
|
now;
|
|
209
277
|
packuments = /* @__PURE__ */ new Map();
|
|
210
278
|
notes = /* @__PURE__ */ new Map();
|
|
279
|
+
githubLatest = /* @__PURE__ */ new Map();
|
|
280
|
+
githubReleases = /* @__PURE__ */ new Map();
|
|
211
281
|
constructor(config, fetchJson, now = () => Date.now()) {
|
|
212
282
|
this.config = config;
|
|
213
283
|
this.fetchJson = fetchJson;
|
|
@@ -216,10 +286,12 @@ var PluginInfoService = class {
|
|
|
216
286
|
/** List user-added plugins for one profile directory. */
|
|
217
287
|
async listPlugins(profileDir) {
|
|
218
288
|
const manifest = readProfileManifest(profileDir);
|
|
289
|
+
const profileCli = profileCliName(manifest.name, profileDir);
|
|
219
290
|
const listed = listUserPlugins(manifest).filter((item) => isAllowedScope(item.name, this.config.allowScopes));
|
|
220
|
-
const plugins = await Promise.all(listed.map(async (item) => this.describePlugin(profileDir, item.name, item.spec)));
|
|
291
|
+
const plugins = await Promise.all(listed.map(async (item) => this.describePlugin(profileDir, profileCli, item.name, item.spec)));
|
|
221
292
|
return {
|
|
222
293
|
profile: manifest.name ?? profileDir,
|
|
294
|
+
profileCli,
|
|
223
295
|
plugins
|
|
224
296
|
};
|
|
225
297
|
}
|
|
@@ -229,6 +301,10 @@ var PluginInfoService = class {
|
|
|
229
301
|
const installed = readInstalledPackage(profileDir, name);
|
|
230
302
|
if (plugin.source !== "registry") {
|
|
231
303
|
const installedVersion = installed?.version;
|
|
304
|
+
const releaseSpec = parseGithubReleaseSpec(plugin.spec);
|
|
305
|
+
if (releaseSpec !== void 0) try {
|
|
306
|
+
return await this.githubReleaseVersions(name, installedVersion, releaseSpec, limit);
|
|
307
|
+
} catch {}
|
|
232
308
|
return {
|
|
233
309
|
name,
|
|
234
310
|
...installedVersion === void 0 ? {} : { installedVersion },
|
|
@@ -251,18 +327,18 @@ var PluginInfoService = class {
|
|
|
251
327
|
}
|
|
252
328
|
/** Update notes for one published version. */
|
|
253
329
|
async versionNotes(profileDir, name, version) {
|
|
254
|
-
findUserPlugin(profileDir, name, this.config.allowScopes);
|
|
330
|
+
const plugin = findUserPlugin(profileDir, name, this.config.allowScopes);
|
|
255
331
|
const cacheKey = `${name}@${version}`;
|
|
256
332
|
const cached = this.notes.get(cacheKey);
|
|
257
333
|
if (cached !== void 0 && cached.expiresAt > this.now()) return cached.value;
|
|
258
|
-
const notes = await this.loadNotes(profileDir, name, version);
|
|
334
|
+
const notes = await this.loadNotes(profileDir, name, version, plugin.spec);
|
|
259
335
|
this.notes.set(cacheKey, {
|
|
260
336
|
value: notes,
|
|
261
337
|
expiresAt: this.now() + this.config.cacheTtlMs
|
|
262
338
|
});
|
|
263
339
|
return notes;
|
|
264
340
|
}
|
|
265
|
-
async describePlugin(profileDir, name, spec) {
|
|
341
|
+
async describePlugin(profileDir, profileCli, name, spec) {
|
|
266
342
|
const source = classifySpec(spec);
|
|
267
343
|
const installed = readInstalledPackage(profileDir, name);
|
|
268
344
|
const installedRepository = repositoryUrlOf(installed?.repository);
|
|
@@ -276,17 +352,19 @@ var PluginInfoService = class {
|
|
|
276
352
|
...installed?.homepage === void 0 ? {} : { homepage: installed.homepage },
|
|
277
353
|
...installedRepository === void 0 ? {} : { repository: installedRepository }
|
|
278
354
|
};
|
|
279
|
-
if (source !== "registry") return summary;
|
|
355
|
+
if (source !== "registry") return this.describeReleaseSpecPlugin(profileCli, spec, summary);
|
|
280
356
|
try {
|
|
281
357
|
const packument = await this.loadPackument(name);
|
|
282
358
|
const latestVersion = latestVersionOf(packument);
|
|
283
359
|
const homepage = summary.homepage ?? packument.homepage;
|
|
284
360
|
const repository = summary.repository ?? repositoryUrlOf(packument.repository);
|
|
361
|
+
const hasUpdate = hasNewerVersion(summary.installedVersion, latestVersion);
|
|
285
362
|
return {
|
|
286
363
|
...summary,
|
|
287
364
|
...packument.description === void 0 || summary.description !== void 0 ? {} : { description: packument.description },
|
|
288
365
|
...latestVersion === void 0 ? {} : { latestVersion },
|
|
289
|
-
hasUpdate
|
|
366
|
+
hasUpdate,
|
|
367
|
+
...hasUpdate ? { updateCommand: pluginUpdateCommand(profileCli, name) } : {},
|
|
290
368
|
...homepage === void 0 ? {} : { homepage },
|
|
291
369
|
...repository === void 0 ? {} : { repository }
|
|
292
370
|
};
|
|
@@ -309,7 +387,106 @@ var PluginInfoService = class {
|
|
|
309
387
|
});
|
|
310
388
|
return packument;
|
|
311
389
|
}
|
|
312
|
-
|
|
390
|
+
/**
|
|
391
|
+
* The repo's latest published release (GitHub skips prereleases/drafts),
|
|
392
|
+
* or undefined when the repo has no releases. Negative answers are cached
|
|
393
|
+
* too so a repo without releases does not burn the API rate limit.
|
|
394
|
+
*/
|
|
395
|
+
async loadGithubLatest(owner, repo) {
|
|
396
|
+
const key = `${owner}/${repo}`;
|
|
397
|
+
const cached = this.githubLatest.get(key);
|
|
398
|
+
if (cached !== void 0 && cached.expiresAt > this.now()) return cached.value;
|
|
399
|
+
let payload;
|
|
400
|
+
try {
|
|
401
|
+
payload = await this.fetchJson(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, this.config.timeoutMs, githubHeaders());
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error instanceof Error && error.message.startsWith("HTTP 404")) {
|
|
404
|
+
this.githubLatest.set(key, {
|
|
405
|
+
value: void 0,
|
|
406
|
+
expiresAt: this.now() + this.config.cacheTtlMs
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
const release = asGithubRelease(payload);
|
|
413
|
+
this.githubLatest.set(key, {
|
|
414
|
+
value: release,
|
|
415
|
+
expiresAt: this.now() + this.config.cacheTtlMs
|
|
416
|
+
});
|
|
417
|
+
return release;
|
|
418
|
+
}
|
|
419
|
+
/** Newest-first GitHub releases for one repo, cached per repo. */
|
|
420
|
+
async loadGithubReleases(owner, repo, limit) {
|
|
421
|
+
const key = `${owner}/${repo}`;
|
|
422
|
+
const cached = this.githubReleases.get(key);
|
|
423
|
+
if (cached !== void 0 && cached.expiresAt > this.now()) return cached.value;
|
|
424
|
+
const perPage = Math.min(100, Math.max(1, Math.trunc(limit)));
|
|
425
|
+
const payload = await this.fetchJson(`https://api.github.com/repos/${owner}/${repo}/releases?per_page=${perPage}`, this.config.timeoutMs, githubHeaders());
|
|
426
|
+
if (!Array.isArray(payload)) throw new Error(`GitHub returned a non-array releases payload for ${owner}/${repo}`);
|
|
427
|
+
const releases = payload.map((item) => asGithubRelease(item)).filter((item) => item !== void 0);
|
|
428
|
+
this.githubReleases.set(key, {
|
|
429
|
+
value: releases,
|
|
430
|
+
expiresAt: this.now() + this.config.cacheTtlMs
|
|
431
|
+
});
|
|
432
|
+
return releases;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Update detection for GitHub-release tarball installs: the dependency spec
|
|
436
|
+
* itself names the repository and the currently installed tag, so the
|
|
437
|
+
* latest release is looked up from that repo — nothing is hardcoded.
|
|
438
|
+
*/
|
|
439
|
+
async describeReleaseSpecPlugin(profileCli, spec, summary) {
|
|
440
|
+
const releaseSpec = parseGithubReleaseSpec(spec);
|
|
441
|
+
if (releaseSpec === void 0) return summary;
|
|
442
|
+
const repository = `https://github.com/${releaseSpec.owner}/${releaseSpec.repo}`;
|
|
443
|
+
try {
|
|
444
|
+
const latest = await this.loadGithubLatest(releaseSpec.owner, releaseSpec.repo);
|
|
445
|
+
const latestVersion = latest === void 0 ? void 0 : versionFromTag(latest.tag);
|
|
446
|
+
const hasUpdate = hasNewerVersion(summary.installedVersion ?? versionFromTag(releaseSpec.tag), latestVersion);
|
|
447
|
+
const oldVersion = versionFromTag(releaseSpec.tag) ?? summary.installedVersion;
|
|
448
|
+
const updateUrl = latest !== void 0 && latestVersion !== void 0 && oldVersion !== void 0 && hasUpdate ? releaseDownloadUrl(releaseSpec, latest.tag, latestVersion, oldVersion) : void 0;
|
|
449
|
+
return {
|
|
450
|
+
...summary,
|
|
451
|
+
...summary.repository === void 0 ? { repository } : {},
|
|
452
|
+
...latestVersion === void 0 ? {} : { latestVersion },
|
|
453
|
+
hasUpdate,
|
|
454
|
+
...hasUpdate && updateUrl !== void 0 ? { updateCommand: `dsh plugin --profile ${profileCli} add "${updateUrl}"` } : {}
|
|
455
|
+
};
|
|
456
|
+
} catch (error) {
|
|
457
|
+
return {
|
|
458
|
+
...summary,
|
|
459
|
+
repository,
|
|
460
|
+
error: error instanceof Error ? error.message : String(error)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/** GitHub release history reshaped into version rows for one URL install. */
|
|
465
|
+
async githubReleaseVersions(name, installedVersion, releaseSpec, limit) {
|
|
466
|
+
const releases = await this.loadGithubReleases(releaseSpec.owner, releaseSpec.repo, limit);
|
|
467
|
+
const normalizedInstalled = installedVersion === void 0 ? void 0 : normalizeVersion(installedVersion);
|
|
468
|
+
const published = releases.filter((release) => !release.draft && versionFromTag(release.tag) !== void 0).slice(0, Math.max(0, limit));
|
|
469
|
+
const rows = published.map((release) => {
|
|
470
|
+
const version = versionFromTag(release.tag);
|
|
471
|
+
return {
|
|
472
|
+
version,
|
|
473
|
+
...release.publishedAt === void 0 ? {} : { publishedAt: release.publishedAt },
|
|
474
|
+
latest: false,
|
|
475
|
+
installed: normalizedInstalled !== void 0 && version === normalizedInstalled
|
|
476
|
+
};
|
|
477
|
+
});
|
|
478
|
+
const latestIndex = published.findIndex((release) => !release.prerelease);
|
|
479
|
+
const latestRow = (latestIndex === -1 ? rows[0] : rows[latestIndex]) ?? void 0;
|
|
480
|
+
if (latestRow !== void 0) latestRow.latest = true;
|
|
481
|
+
const latestVersion = latestRow?.version;
|
|
482
|
+
return {
|
|
483
|
+
name,
|
|
484
|
+
...installedVersion === void 0 ? {} : { installedVersion },
|
|
485
|
+
...latestVersion === void 0 ? {} : { latestVersion },
|
|
486
|
+
versions: rows
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
async loadNotes(profileDir, name, version, spec) {
|
|
313
490
|
const installed = readInstalledPackage(profileDir, name);
|
|
314
491
|
let packument;
|
|
315
492
|
try {
|
|
@@ -317,8 +494,13 @@ var PluginInfoService = class {
|
|
|
317
494
|
} catch {
|
|
318
495
|
packument = void 0;
|
|
319
496
|
}
|
|
497
|
+
const releaseSpec = parseGithubReleaseSpec(spec);
|
|
320
498
|
const repository = repositoryUrlOf(installed?.repository) ?? repositoryUrlOf(packument?.repository);
|
|
321
|
-
|
|
499
|
+
let github = repository === void 0 ? void 0 : parseGithubRepo(repository);
|
|
500
|
+
if (github === void 0 && releaseSpec !== void 0) github = {
|
|
501
|
+
owner: releaseSpec.owner,
|
|
502
|
+
repo: releaseSpec.repo
|
|
503
|
+
};
|
|
322
504
|
const publishedAt = packument?.time?.[version];
|
|
323
505
|
const base = {
|
|
324
506
|
name,
|
|
@@ -327,7 +509,8 @@ var PluginInfoService = class {
|
|
|
327
509
|
...publishedAt === void 0 ? {} : { publishedAt }
|
|
328
510
|
};
|
|
329
511
|
if (github === void 0) return base;
|
|
330
|
-
const
|
|
512
|
+
const specTag = releaseSpec !== void 0 && versionFromTag(releaseSpec.tag) === normalizeVersion(version) ? releaseSpec.tag : void 0;
|
|
513
|
+
const release = await this.fetchGithubRelease(github.owner, github.repo, version, specTag === void 0 ? [] : [specTag]);
|
|
331
514
|
if (release !== void 0) return {
|
|
332
515
|
...base,
|
|
333
516
|
source: "github-release",
|
|
@@ -347,8 +530,12 @@ var PluginInfoService = class {
|
|
|
347
530
|
commits: compare.commits
|
|
348
531
|
};
|
|
349
532
|
}
|
|
350
|
-
async fetchGithubRelease(owner, repo, version) {
|
|
351
|
-
const tags = unique([
|
|
533
|
+
async fetchGithubRelease(owner, repo, version, preferredTags = []) {
|
|
534
|
+
const tags = unique([
|
|
535
|
+
...preferredTags,
|
|
536
|
+
version,
|
|
537
|
+
`v${version}`
|
|
538
|
+
]);
|
|
352
539
|
for (const tag of tags) try {
|
|
353
540
|
const payload = await this.fetchJson(`https://api.github.com/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(tag)}`, this.config.timeoutMs, githubHeaders());
|
|
354
541
|
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) continue;
|
|
@@ -410,6 +597,22 @@ function findUserPlugin(profileDir, name, allowScopes) {
|
|
|
410
597
|
function unique(values) {
|
|
411
598
|
return [...new Set(values.filter((value) => value.length > 0))];
|
|
412
599
|
}
|
|
600
|
+
/** Extract the release fields the service needs from a GitHub API payload. */
|
|
601
|
+
function asGithubRelease(payload) {
|
|
602
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return void 0;
|
|
603
|
+
const record = payload;
|
|
604
|
+
const tag = typeof record.tag_name === "string" && record.tag_name.length > 0 ? record.tag_name : void 0;
|
|
605
|
+
if (tag === void 0) return void 0;
|
|
606
|
+
const publishedAt = typeof record.published_at === "string" ? record.published_at : void 0;
|
|
607
|
+
const url = typeof record.html_url === "string" ? record.html_url : void 0;
|
|
608
|
+
return {
|
|
609
|
+
tag,
|
|
610
|
+
...publishedAt === void 0 ? {} : { publishedAt },
|
|
611
|
+
...url === void 0 ? {} : { url },
|
|
612
|
+
prerelease: record.prerelease === true,
|
|
613
|
+
draft: record.draft === true
|
|
614
|
+
};
|
|
615
|
+
}
|
|
413
616
|
/** True when `name` matches one of the configured scope prefixes (empty list = allow all). */
|
|
414
617
|
function isAllowedScope(name, allowScopes) {
|
|
415
618
|
if (allowScopes === void 0 || allowScopes.length === 0) return true;
|
|
@@ -460,7 +663,7 @@ const Config = z.object({
|
|
|
460
663
|
defaultVersionLimit: z.number().step(1).min(1).max(50).default(10),
|
|
461
664
|
timeoutMs: z.number().step(1).min(1e3).default(15e3),
|
|
462
665
|
cacheTtlMs: z.number().step(1).min(0).default(3e5),
|
|
463
|
-
allowScopes: z.array(z.string())
|
|
666
|
+
allowScopes: z.array(z.string())
|
|
464
667
|
});
|
|
465
668
|
/**
|
|
466
669
|
* Register the plugin-info HTTP API when a web server is present.
|
|
@@ -540,4 +743,4 @@ function sendError(res, error) {
|
|
|
540
743
|
});
|
|
541
744
|
}
|
|
542
745
|
//#endregion
|
|
543
|
-
export { Config, DEFAULT_VERSION_LIMIT, PluginInfoService, apply, classifySpec, fetchJson, hasNewerVersion, latestVersionOf, listUserPlugins, name, packumentUrl, parseGithubRepo, parseVersion, profileDirFromBaseUrl, recentVersions, repositoryUrlOf, resolveVersionLimit };
|
|
746
|
+
export { Config, DEFAULT_VERSION_LIMIT, PluginInfoService, apply, classifySpec, fetchJson, hasNewerVersion, latestVersionOf, listUserPlugins, name, packumentUrl, parseGithubReleaseSpec, parseGithubRepo, parseVersion, pluginUpdateCommand, profileCliName, profileDirFromBaseUrl, recentVersions, releaseDownloadUrl, repositoryUrlOf, resolveVersionLimit, versionFromTag };
|
package/lib/types/client.d.ts
CHANGED
|
@@ -21,14 +21,51 @@ interface MinimalReact {
|
|
|
21
21
|
useEffect(effect: () => void | (() => void), deps?: unknown[]): void;
|
|
22
22
|
useCallback<T>(fn: T, deps: unknown[]): T;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Subset of DSH's `MarkdownText` shared primitive (GFM + KaTeX, unsafe
|
|
26
|
+
* HTML/protocols stripped). `labels` is REQUIRED in practice: the fenced
|
|
27
|
+
* code-block renderer reads `labels.code.copyLabel` / `labels.code.copiedLabel`
|
|
28
|
+
* with no defaults, and the footnotes section reads `labels.footnotes` —
|
|
29
|
+
* passing only `text` crashes the whole settings section the moment a
|
|
30
|
+
* release note contains a fenced code block.
|
|
31
|
+
*/
|
|
32
|
+
interface MarkdownLabels {
|
|
33
|
+
code: {
|
|
34
|
+
copyLabel: string;
|
|
35
|
+
copiedLabel: string;
|
|
36
|
+
};
|
|
37
|
+
footnotes: string;
|
|
38
|
+
}
|
|
39
|
+
type MarkdownTextComponent = (props: {
|
|
40
|
+
text: string;
|
|
41
|
+
labels?: MarkdownLabels;
|
|
42
|
+
}) => unknown;
|
|
43
|
+
/** Labels for the MarkdownText copy button / footnotes heading (settings UI is zh). */
|
|
44
|
+
declare const MARKDOWN_LABELS: MarkdownLabels;
|
|
24
45
|
interface SlotsService {
|
|
25
46
|
inject(key: string, register: () => void): void;
|
|
26
47
|
register(options: Record<string, unknown>, component: unknown): void;
|
|
27
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Minimal structural mirror of dsh-better-sidebar's client service — the
|
|
51
|
+
* only method we call is `registerTab`. The plugin is an OPTIONAL soft
|
|
52
|
+
* dependency: this plugin does not value-import it, does not list it in
|
|
53
|
+
* peerDependencies, and reaches the service through `ctx.inject(...)` so
|
|
54
|
+
* the whole registration block is inert when better-sidebar is absent.
|
|
55
|
+
*/
|
|
56
|
+
interface BetterSidebarServiceLike {
|
|
57
|
+
registerTab(descriptor: Record<string, unknown>): () => void;
|
|
58
|
+
}
|
|
28
59
|
interface ClientContext {
|
|
29
60
|
slots: SlotsService;
|
|
30
61
|
/** Cordis fiber-lifetime effect: runs now, disposes on fiber unload. */
|
|
31
62
|
effect(callback: () => (() => void) | void, label?: string): () => void;
|
|
63
|
+
/**
|
|
64
|
+
* Cordis child-fiber spawn: waits until all named services are available
|
|
65
|
+
* before running the callback. Used to register with better-sidebar only
|
|
66
|
+
* when that plugin is loaded (optional soft dependency).
|
|
67
|
+
*/
|
|
68
|
+
inject(deps: string[], apply: (ctx: ClientContext) => void): unknown;
|
|
32
69
|
}
|
|
33
70
|
interface PluginRow {
|
|
34
71
|
name: string;
|
|
@@ -38,6 +75,7 @@ interface PluginRow {
|
|
|
38
75
|
installedVersion?: string;
|
|
39
76
|
latestVersion?: string;
|
|
40
77
|
hasUpdate: boolean;
|
|
78
|
+
updateCommand?: string;
|
|
41
79
|
homepage?: string;
|
|
42
80
|
repository?: string;
|
|
43
81
|
error?: string;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
import type { Context } from '@deepseek-ai/cordis';
|
|
7
7
|
import z from '@deepseek-ai/schemastery';
|
|
8
8
|
import type { PluginConfig } from './types.ts';
|
|
9
|
-
export { classifySpec, listUserPlugins, parseGithubRepo, repositoryUrlOf } from './profile.ts';
|
|
10
|
-
export { hasNewerVersion, parseVersion } from './semver.ts';
|
|
9
|
+
export { classifySpec, listUserPlugins, parseGithubReleaseSpec, parseGithubRepo, pluginUpdateCommand, profileCliName, releaseDownloadUrl, repositoryUrlOf } from './profile.ts';
|
|
10
|
+
export { hasNewerVersion, parseVersion, versionFromTag } from './semver.ts';
|
|
11
11
|
export { latestVersionOf, packumentUrl, recentVersions, resolveVersionLimit } from './registry.ts';
|
|
12
12
|
export { fetchJson, PluginInfoService, profileDirFromBaseUrl } from './service.ts';
|
|
13
|
-
export type { InstalledPackage, Packument, PluginConfig, PluginSource, PluginSummary, PluginVersionRow, ProfileManifest, VersionNotes, } from './types.ts';
|
|
13
|
+
export type { GithubReleaseSpec, InstalledPackage, Packument, PluginConfig, PluginSource, PluginSummary, PluginVersionRow, ProfileManifest, VersionNotes, } from './types.ts';
|
|
14
14
|
/** Cordis plugin name used by Loader diagnostics. */
|
|
15
15
|
export declare const name = "dsh-plugin-info";
|
|
16
16
|
/** Default version window shown by the settings page. */
|
package/lib/types/profile.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import type { InstalledPackage, PluginSource, ProfileManifest } from './types.ts';
|
|
1
|
+
import type { GithubReleaseSpec, InstalledPackage, PluginSource, ProfileManifest } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* CLI `--profile` argument for this profile directory.
|
|
4
|
+
*
|
|
5
|
+
* DSH names the manifest `dsh-profile-<dir>` and the CLI flag is the
|
|
6
|
+
* directory basename (`web`, `tui`, …). Prefer the manifest prefix so temp
|
|
7
|
+
* test dirs still resolve; fall back to the folder name, then `web`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function profileCliName(manifestName: string | undefined, profileDir: string): string;
|
|
10
|
+
/** `dsh plugin add …@latest` command for one registry package in this profile. */
|
|
11
|
+
export declare function pluginUpdateCommand(profileCli: string, packageName: string): string;
|
|
2
12
|
/** Classify a profile dependency spec. */
|
|
3
13
|
export declare function classifySpec(spec: string): PluginSource;
|
|
4
14
|
/** User-added plugins are the profile's own `dependencies`, not in-box bundles. */
|
|
@@ -18,4 +28,17 @@ export declare function parseGithubRepo(input: string): {
|
|
|
18
28
|
owner: string;
|
|
19
29
|
repo: string;
|
|
20
30
|
} | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Parse a `github.com/<owner>/<repo>/releases/download/<tag>/<asset>`
|
|
33
|
+
* dependency spec into its repository, tag, and asset. Everything is derived
|
|
34
|
+
* from the spec itself — no per-plugin configuration.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseGithubReleaseSpec(spec: string): GithubReleaseSpec | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Rewrite a release spec onto a newer tag. The asset file name keeps its
|
|
39
|
+
* shape: an embedded version token equal to `oldVersion` is replaced by
|
|
40
|
+
* `newVersion` (`dsh-skill-mcp-panel-2.0.3.tgz` → `…-2.0.4.tgz`) without
|
|
41
|
+
* touching longer version numbers (`12.0.3`) that merely contain it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function releaseDownloadUrl(spec: GithubReleaseSpec, newTag: string, newVersion: string, oldVersion: string): string;
|
|
21
44
|
//# sourceMappingURL=profile.d.ts.map
|
package/lib/types/semver.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare function parseVersion(input: string): ParsedVersion | undefined;
|
|
|
13
13
|
export declare function compareParsed(left: ParsedVersion, right: ParsedVersion): number;
|
|
14
14
|
/** Compare two version strings. Unparseable values compare as equal (0). */
|
|
15
15
|
export declare function compareVersions(left: string, right: string): number;
|
|
16
|
+
/** Version embedded in a release tag: `v2.0.3`, `2.0.3`, or `panel-1.20.3-rc.1`. */
|
|
17
|
+
export declare function versionFromTag(tag: string): string | undefined;
|
|
16
18
|
/** True when `latest` is a newer published version than `installed`. */
|
|
17
19
|
export declare function hasNewerVersion(installed: string | undefined, latest: string | undefined): boolean;
|
|
18
20
|
//# sourceMappingURL=semver.d.ts.map
|
package/lib/types/service.d.ts
CHANGED
|
@@ -8,10 +8,13 @@ export declare class PluginInfoService {
|
|
|
8
8
|
private readonly now;
|
|
9
9
|
private readonly packuments;
|
|
10
10
|
private readonly notes;
|
|
11
|
+
private readonly githubLatest;
|
|
12
|
+
private readonly githubReleases;
|
|
11
13
|
constructor(config: PluginConfig, fetchJson: JsonFetcher, now?: () => number);
|
|
12
14
|
/** List user-added plugins for one profile directory. */
|
|
13
15
|
listPlugins(profileDir: string): Promise<{
|
|
14
16
|
profile: string;
|
|
17
|
+
profileCli: string;
|
|
15
18
|
plugins: PluginSummary[];
|
|
16
19
|
}>;
|
|
17
20
|
/** Recent published versions for one user-added plugin. */
|
|
@@ -25,6 +28,22 @@ export declare class PluginInfoService {
|
|
|
25
28
|
versionNotes(profileDir: string, name: string, version: string): Promise<VersionNotes>;
|
|
26
29
|
private describePlugin;
|
|
27
30
|
private loadPackument;
|
|
31
|
+
/**
|
|
32
|
+
* The repo's latest published release (GitHub skips prereleases/drafts),
|
|
33
|
+
* or undefined when the repo has no releases. Negative answers are cached
|
|
34
|
+
* too so a repo without releases does not burn the API rate limit.
|
|
35
|
+
*/
|
|
36
|
+
private loadGithubLatest;
|
|
37
|
+
/** Newest-first GitHub releases for one repo, cached per repo. */
|
|
38
|
+
private loadGithubReleases;
|
|
39
|
+
/**
|
|
40
|
+
* Update detection for GitHub-release tarball installs: the dependency spec
|
|
41
|
+
* itself names the repository and the currently installed tag, so the
|
|
42
|
+
* latest release is looked up from that repo — nothing is hardcoded.
|
|
43
|
+
*/
|
|
44
|
+
private describeReleaseSpecPlugin;
|
|
45
|
+
/** GitHub release history reshaped into version rows for one URL install. */
|
|
46
|
+
private githubReleaseVersions;
|
|
28
47
|
private loadNotes;
|
|
29
48
|
private fetchGithubRelease;
|
|
30
49
|
private fetchGithubCompare;
|
package/lib/types/types.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
/** Where a profile dependency spec came from. */
|
|
2
2
|
export type PluginSource = 'registry' | 'file' | 'link' | 'git' | 'other';
|
|
3
|
+
/** A profile dependency spec that installs a GitHub release tarball directly. */
|
|
4
|
+
export interface GithubReleaseSpec {
|
|
5
|
+
owner: string;
|
|
6
|
+
repo: string;
|
|
7
|
+
/** Release tag embedded in the download URL, e.g. `v2.0.3`. */
|
|
8
|
+
tag: string;
|
|
9
|
+
/** Tarball file name, e.g. `dsh-skill-mcp-panel-2.0.3.tgz`. */
|
|
10
|
+
asset: string;
|
|
11
|
+
}
|
|
3
12
|
/** One user-added profile plugin as the UI lists it. */
|
|
4
13
|
export interface PluginSummary {
|
|
5
14
|
/** npm package name. */
|
|
@@ -16,6 +25,11 @@ export interface PluginSummary {
|
|
|
16
25
|
latestVersion?: string;
|
|
17
26
|
/** True when the registry latest is newer than the installed version. */
|
|
18
27
|
hasUpdate: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* `dsh plugin --profile <name> add <pkg>@latest` when this row has a
|
|
30
|
+
* registry update. Omitted for local / git installs and up-to-date packages.
|
|
31
|
+
*/
|
|
32
|
+
updateCommand?: string;
|
|
19
33
|
/** Package homepage. */
|
|
20
34
|
homepage?: string;
|
|
21
35
|
/** Normalized repository browse URL. */
|
|
@@ -102,10 +116,11 @@ export interface PluginConfig {
|
|
|
102
116
|
/** How long packument and notes stay cached. */
|
|
103
117
|
cacheTtlMs: number;
|
|
104
118
|
/**
|
|
105
|
-
* Optional allow-list: when
|
|
106
|
-
*
|
|
107
|
-
*
|
|
119
|
+
* Optional allow-list: when omitted or empty, all profile-added plugins
|
|
120
|
+
* appear in the settings page and the version/notes APIs. When non-empty,
|
|
121
|
+
* only plugins whose npm name starts with one of these prefixes appear
|
|
122
|
+
* (case-insensitive, e.g. `@eya46/`).
|
|
108
123
|
*/
|
|
109
|
-
allowScopes
|
|
124
|
+
allowScopes?: string[];
|
|
110
125
|
}
|
|
111
126
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eya46/dsh-plugin-info",
|
|
3
3
|
"description": "List profile-added DSH plugins, mark available updates, and show recent version notes",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
53
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
52
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
53
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"@deepseek-ai/dsh-host-webserver": {
|
|
@@ -58,12 +58,12 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
61
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
65
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
66
|
-
"@types/node": "^22.20.
|
|
64
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
65
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
|
|
66
|
+
"@types/node": "^22.20.3",
|
|
67
67
|
"tsdown": "^0.22.2",
|
|
68
68
|
"typescript": "^5.9.3",
|
|
69
69
|
"vitest": "^4.1.8"
|