@eya46/dsh-plugin-info 0.0.3 → 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 +3 -1
- package/lib/client.js +73 -12
- package/lib/index.js +215 -12
- package/lib/types/client.d.ts +17 -3
- 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 +14 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
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
|
|
|
@@ -35,7 +37,7 @@ dsh plugin --profile web add "file:./packages/dsh-plugin-info"
|
|
|
35
37
|
|
|
36
38
|
| 端点 | 说明 |
|
|
37
39
|
| --- | --- |
|
|
38
|
-
| `GET /plugin-info/api/plugins` | 当前 Profile 用户插件 +
|
|
40
|
+
| `GET /plugin-info/api/plugins` | 当前 Profile 用户插件 + 是否有更新;含 `profileCli`,可更新的 registry 包还会带 `updateCommand` |
|
|
39
41
|
| `GET /plugin-info/api/versions?name=<pkg>&limit=10` | 最近版本,默认 10 条 |
|
|
40
42
|
| `GET /plugin-info/api/notes?name=<pkg>&version=<ver>` | 该版本的更新说明 |
|
|
41
43
|
|
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; }',
|
|
@@ -59,6 +69,7 @@
|
|
|
59
69
|
'.dpi-sidebar .dpi-card .dpi-body { padding: 0 12px 12px; }',
|
|
60
70
|
'.dpi-sidebar .dpi-actions { gap: 6px; }',
|
|
61
71
|
'.dpi-sidebar .dpi-rowButton { height: 26px; padding: 0 8px; }',
|
|
72
|
+
'.dpi-sidebar .dpi-updateCopy { height: 26px; margin-right: 10px; padding: 0 8px; }',
|
|
62
73
|
].join('\n');
|
|
63
74
|
const React = require('react');
|
|
64
75
|
// Reuse DSH's shared untrusted-Markdown renderer instead of rolling our
|
|
@@ -67,6 +78,30 @@
|
|
|
67
78
|
const uiPrimitives = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
68
79
|
const MarkdownText = uiPrimitives.MarkdownText;
|
|
69
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
|
+
}
|
|
70
105
|
const ensureStyles = () => {
|
|
71
106
|
if (document.querySelector('style[data-plugin="@eya46/dsh-plugin-info"]') !== null)
|
|
72
107
|
return;
|
|
@@ -148,17 +183,18 @@
|
|
|
148
183
|
const [notes, setNotes] = React.useState({});
|
|
149
184
|
const [notesBusy, setNotesBusy] = React.useState('');
|
|
150
185
|
const [copied, setCopied] = React.useState({});
|
|
151
|
-
const
|
|
186
|
+
const [profileCli, setProfileCli] = React.useState('web');
|
|
187
|
+
const copyText = React.useCallback((key, text) => {
|
|
152
188
|
const write = () => {
|
|
153
189
|
if (typeof navigator !== 'undefined' && navigator.clipboard !== undefined) {
|
|
154
|
-
return navigator.clipboard.writeText(
|
|
190
|
+
return navigator.clipboard.writeText(text);
|
|
155
191
|
}
|
|
156
192
|
return Promise.reject(new Error('clipboard unavailable'));
|
|
157
193
|
};
|
|
158
194
|
write()
|
|
159
195
|
.then(() => {
|
|
160
|
-
setCopied((prev) => ({ ...prev, [
|
|
161
|
-
window.setTimeout(() => setCopied((prev) => ({ ...prev, [
|
|
196
|
+
setCopied((prev) => ({ ...prev, [key]: true }));
|
|
197
|
+
window.setTimeout(() => setCopied((prev) => ({ ...prev, [key]: false })), 1200);
|
|
162
198
|
})
|
|
163
199
|
.catch(() => { });
|
|
164
200
|
}, []);
|
|
@@ -170,6 +206,8 @@
|
|
|
170
206
|
if (!Array.isArray(data.plugins))
|
|
171
207
|
throw new Error(data.message ?? 'unexpected response');
|
|
172
208
|
setRows(data.plugins);
|
|
209
|
+
if (typeof data.profileCli === 'string' && data.profileCli.length > 0)
|
|
210
|
+
setProfileCli(data.profileCli);
|
|
173
211
|
setError('');
|
|
174
212
|
})
|
|
175
213
|
.catch((caught) => setError(String(caught)))
|
|
@@ -225,7 +263,7 @@
|
|
|
225
263
|
const updateCount = rows.filter((row) => row.hasUpdate).length;
|
|
226
264
|
const children = [
|
|
227
265
|
h('h2', { key: 'title', className: 'dpi-title' }, '插件信息'),
|
|
228
|
-
h('p', { key: 'intro', className: 'dpi-intro' }, '当前 Profile
|
|
266
|
+
h('p', { key: 'intro', className: 'dpi-intro' }, '当前 Profile(' + profileCli + ')里用户添加的插件(不含随发行版内置的 bundle)。有更新时可一键复制 dsh plugin 命令;展开后默认看最近 10 个版本,点版本可看更新说明。'),
|
|
229
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 + ' 个可更新' : ''))),
|
|
230
268
|
];
|
|
231
269
|
if (error !== '')
|
|
@@ -245,14 +283,20 @@
|
|
|
245
283
|
if (plugin.hasUpdate && plugin.latestVersion !== undefined) {
|
|
246
284
|
head.push(h('span', { key: 'up', className: 'dpi-tag', 'data-tone': 'warn' }, '可更新 ' + plugin.latestVersion));
|
|
247
285
|
}
|
|
248
|
-
else if (plugin.
|
|
286
|
+
else if (!plugin.hasUpdate && plugin.latestVersion !== undefined) {
|
|
249
287
|
head.push(h('span', { key: 'ok', className: 'dpi-tag', 'data-tone': 'ok' }, '已是最新'));
|
|
250
288
|
}
|
|
289
|
+
const updateCommand = plugin.updateCommand
|
|
290
|
+
?? ('dsh plugin --profile ' + profileCli + ' add ' + plugin.name + '@latest');
|
|
291
|
+
const updateCopied = copied['update:' + plugin.name] === true;
|
|
251
292
|
const body = [];
|
|
252
293
|
if (expanded) {
|
|
253
294
|
if (plugin.description !== undefined)
|
|
254
295
|
body.push(h('p', { key: 'desc', className: 'dpi-meta' }, plugin.description));
|
|
255
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
|
+
}
|
|
256
300
|
if (plugin.error !== undefined)
|
|
257
301
|
body.push(h('p', { key: 'perr', className: 'dpi-banner' }, plugin.error));
|
|
258
302
|
const links = [
|
|
@@ -260,7 +304,7 @@
|
|
|
260
304
|
key: 'copy',
|
|
261
305
|
className: 'dpi-copyTag',
|
|
262
306
|
title: '复制包名',
|
|
263
|
-
onClick: () => {
|
|
307
|
+
onClick: () => { copyText(plugin.name, plugin.name); },
|
|
264
308
|
}, copied[plugin.name] === true ? '已复制' : plugin.name),
|
|
265
309
|
h('button', {
|
|
266
310
|
key: 'vers',
|
|
@@ -268,6 +312,14 @@
|
|
|
268
312
|
onClick: () => { loadVersions(plugin.name); },
|
|
269
313
|
}, versions[plugin.name] === undefined ? '加载版本' : '重新加载版本'),
|
|
270
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
|
+
}
|
|
271
323
|
if (plugin.homepage !== undefined) {
|
|
272
324
|
links.push(h('a', { key: 'home', className: 'dpi-rowButton dpi-link', href: plugin.homepage, target: '_blank', rel: 'noreferrer' }, '主页'));
|
|
273
325
|
}
|
|
@@ -284,7 +336,7 @@
|
|
|
284
336
|
body.push(h('p', { key: 'vload', className: 'dpi-meta' }, '正在加载最近 10 个版本…'));
|
|
285
337
|
}
|
|
286
338
|
else if (pluginVersions.length === 0) {
|
|
287
|
-
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' ? '注册表没有版本记录。' : '没有可展示的历史版本。'));
|
|
288
340
|
}
|
|
289
341
|
else {
|
|
290
342
|
const versionNodes = pluginVersions.map((row) => {
|
|
@@ -310,7 +362,7 @@
|
|
|
310
362
|
if (note.title !== undefined)
|
|
311
363
|
noteChildren.push(h('p', { key: 'nt', className: 'dpi-meta' }, note.title));
|
|
312
364
|
if (note.body !== undefined && note.body !== '') {
|
|
313
|
-
noteChildren.push(h(
|
|
365
|
+
noteChildren.push(h(SafeMarkdown, { key: 'nb', text: note.body }));
|
|
314
366
|
}
|
|
315
367
|
if (note.url !== undefined) {
|
|
316
368
|
noteChildren.push(h('a', { key: 'nu', className: 'dpi-link', href: note.url, target: '_blank', rel: 'noreferrer' }, '在 GitHub 查看 Release'));
|
|
@@ -338,11 +390,20 @@
|
|
|
338
390
|
body.push(h('div', { key: 'versions', className: 'dpi-versions' }, ...versionNodes));
|
|
339
391
|
}
|
|
340
392
|
}
|
|
341
|
-
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', {
|
|
342
394
|
className: 'dpi-head',
|
|
343
395
|
'aria-expanded': expanded,
|
|
344
396
|
onClick: () => { togglePlugin(plugin.name); },
|
|
345
|
-
}, ...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));
|
|
346
407
|
}
|
|
347
408
|
children.push(h('ul', { key: 'cards', className: 'dpi-cards' }, ...cards));
|
|
348
409
|
return h('div', { className: 'dpi-section' }, ...children);
|
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;
|
|
@@ -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
|
@@ -22,13 +22,26 @@ interface MinimalReact {
|
|
|
22
22
|
useCallback<T>(fn: T, deps: unknown[]): T;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* Subset of DSH's `MarkdownText` shared primitive
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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.
|
|
28
31
|
*/
|
|
32
|
+
interface MarkdownLabels {
|
|
33
|
+
code: {
|
|
34
|
+
copyLabel: string;
|
|
35
|
+
copiedLabel: string;
|
|
36
|
+
};
|
|
37
|
+
footnotes: string;
|
|
38
|
+
}
|
|
29
39
|
type MarkdownTextComponent = (props: {
|
|
30
40
|
text: string;
|
|
41
|
+
labels?: MarkdownLabels;
|
|
31
42
|
}) => unknown;
|
|
43
|
+
/** Labels for the MarkdownText copy button / footnotes heading (settings UI is zh). */
|
|
44
|
+
declare const MARKDOWN_LABELS: MarkdownLabels;
|
|
32
45
|
interface SlotsService {
|
|
33
46
|
inject(key: string, register: () => void): void;
|
|
34
47
|
register(options: Record<string, unknown>, component: unknown): void;
|
|
@@ -62,6 +75,7 @@ interface PluginRow {
|
|
|
62
75
|
installedVersion?: string;
|
|
63
76
|
latestVersion?: string;
|
|
64
77
|
hasUpdate: boolean;
|
|
78
|
+
updateCommand?: string;
|
|
65
79
|
homepage?: string;
|
|
66
80
|
repository?: string;
|
|
67
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. */
|
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"
|