dsh-plugin 1.6.11 → 2.0.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.en.md +1 -1
- package/README.md +1 -1
- package/client/client.js +705 -803
- package/client/client.js.map +1 -1
- package/lib/http/catalog.js +2 -3
- package/lib/http/diagnostics.js +6 -7
- package/lib/http/install.js +47 -213
- package/lib/http/logs.js +1 -2
- package/lib/http/npm-release-age.js +42 -0
- package/lib/http/routes.js +8 -7
- package/lib/http/settings.js +2 -2
- package/lib/http/shared.js +2 -2
- package/lib/http/task.js +1 -2
- package/lib/index.js +5 -6
- package/lib/services/install/install-types.js +4 -4
- package/lib/services/install/install.js +1 -1
- package/lib/services/install/npm-check.js +3 -2
- package/lib/services/install/npm-resolve.js +9 -96
- package/lib/services/install/package-entry.js +6 -11
- package/lib/services/install/registry-check.js +5 -16
- package/lib/services/install/release-target.js +8 -9
- package/lib/services/install/task-queue.js +74 -57
- package/lib/services/npm-release-age.js +119 -0
- package/lib/services/probe.js +4 -4
- package/lib/services/profile/installed-versions.js +15 -21
- package/lib/services/profile/profile.js +16 -44
- package/lib/services/profile/progress.js +8 -11
- package/lib/services/settings.js +2 -5
- package/lib/types/http/catalog.d.ts +1 -1
- package/lib/types/http/install.d.ts +2 -2
- package/lib/types/http/npm-release-age.d.ts +2 -0
- package/lib/types/http/routes.d.ts +10 -12
- package/lib/types/http/shared.d.ts +5 -6
- package/lib/types/index.d.ts +7 -8
- package/lib/types/services/install/install-types.d.ts +12 -13
- package/lib/types/services/install/install.d.ts +1 -1
- package/lib/types/services/install/npm-resolve.d.ts +4 -16
- package/lib/types/services/install/package-entry.d.ts +6 -11
- package/lib/types/services/install/release-target.d.ts +7 -8
- package/lib/types/services/install/task-queue.d.ts +32 -31
- package/lib/types/services/npm-release-age.d.ts +15 -0
- package/lib/types/services/probe.d.ts +4 -4
- package/lib/types/services/profile/installed-versions.d.ts +5 -7
- package/lib/types/services/profile/profile.d.ts +14 -18
- package/lib/types/services/profile/progress.d.ts +7 -10
- package/lib/types/services/settings.d.ts +0 -4
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +23 -27
- package/src/client/components/catalog/CatalogList.tsx +2 -4
- package/src/client/components/catalog/PluginCard.tsx +2 -3
- package/src/client/components/layout/CatalogControls.tsx +5 -6
- package/src/client/components/layout/CatalogHeader.tsx +1 -2
- package/src/client/components/layout/CategoryTabs.tsx +2 -3
- package/src/client/components/layout/SectionTabs.tsx +3 -5
- package/src/client/components/modals/ConfirmDialog.tsx +4 -6
- package/src/client/components/modals/InstalledDetailModal.tsx +3 -6
- package/src/client/components/modals/LogsModal.tsx +7 -12
- package/src/client/components/modals/NotificationsModal.tsx +52 -24
- package/src/client/components/modals/ProgressView.tsx +3 -4
- package/src/client/components/modals/modals.tsx +58 -30
- package/src/client/components/ui/Dropdown.tsx +4 -6
- package/src/client/components/ui/Toggle.tsx +2 -3
- package/src/client/components/ui/icons.tsx +1 -2
- package/src/client/components/views/CustomInstallView.tsx +20 -178
- package/src/client/components/views/DiagnosticsView.tsx +3 -6
- package/src/client/components/views/InstalledView.tsx +6 -11
- package/src/client/components/views/MarketView.tsx +2 -4
- package/src/client/components/views/SettingsView.tsx +9 -32
- package/src/client/css-modules.d.ts +1 -1
- package/src/client/data/catalog.ts +2 -3
- package/src/client/data/host.ts +26 -3
- package/src/client/data/hub.ts +2 -3
- package/src/client/hooks/useCatalog.ts +79 -24
- package/src/client/hooks/useIncrementalList.ts +3 -5
- package/src/client/hooks/useLanguage.ts +4 -6
- package/src/client/hooks/useSettings.ts +4 -11
- package/src/client/hooks/useTaskQueue.ts +36 -42
- package/src/client/index.tsx +2 -3
- package/src/client/locales.ts +96 -123
- package/src/client/logic/constants.ts +1 -2
- package/src/client/logic/ensure-plugin-css.ts +8 -12
- package/src/client/logic/failures.ts +41 -28
- package/src/client/logic/format.ts +1 -1
- package/src/client/logic/install-command.ts +14 -35
- package/src/client/logic/installed.ts +15 -10
- package/src/client/logic/normalize.ts +3 -3
- package/src/client/logic/release-age.ts +19 -0
- package/src/client/logic/urls.ts +55 -50
- package/src/client/styles/CustomInstallView.module.css +3 -30
- package/src/client/styles/DiagnosticsView.module.css +3 -5
- package/src/client/styles/Dropdown.module.css +2 -4
- package/src/client/styles/Header.module.css +6 -7
- package/src/client/styles/InstalledView.module.css +4 -7
- package/src/client/styles/List.module.css +4 -5
- package/src/client/styles/LogsView.module.css +2 -3
- package/src/client/styles/Modal.module.css +4 -5
- package/src/client/styles/SectionTabs.module.css +2 -4
- package/src/client/styles/SettingsView.module.css +3 -5
- package/src/client/styles/Toggle.module.css +2 -4
- package/src/client/styles/tokens.module.css +14 -18
- package/src/client/types.ts +11 -11
- package/src/server/http/catalog.ts +2 -3
- package/src/server/http/diagnostics.ts +6 -7
- package/src/server/http/install.ts +47 -208
- package/src/server/http/logs.ts +1 -2
- package/src/server/http/npm-release-age.ts +42 -0
- package/src/server/http/routes.ts +12 -12
- package/src/server/http/settings.ts +2 -2
- package/src/server/http/shared.ts +5 -6
- package/src/server/http/task.ts +1 -2
- package/src/server/index.ts +7 -8
- package/src/server/services/install/install-types.ts +12 -13
- package/src/server/services/install/install.ts +0 -1
- package/src/server/services/install/npm-check.ts +3 -2
- package/src/server/services/install/npm-resolve.ts +9 -99
- package/src/server/services/install/package-entry.ts +6 -11
- package/src/server/services/install/registry-check.ts +5 -16
- package/src/server/services/install/release-target.ts +8 -9
- package/src/server/services/install/task-queue.ts +79 -63
- package/src/server/services/npm-release-age.ts +107 -0
- package/src/server/services/probe.ts +4 -4
- package/src/server/services/profile/installed-versions.ts +16 -22
- package/src/server/services/profile/profile.ts +16 -42
- package/src/server/services/profile/progress.ts +8 -11
- package/src/server/services/settings.ts +2 -9
- package/lib/services/install/preflight.js +0 -178
- package/lib/types/services/install/preflight.d.ts +0 -23
- package/src/server/services/install/preflight.ts +0 -184
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
|
|
3
|
+
* Website: https://dsh-plugin.org
|
|
4
|
+
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
|
+
*
|
|
6
|
+
* npm 包最新版本的发布时间查询:为「已安装插件是否有更新」提供供应链门禁依据。
|
|
7
|
+
* pnpm 对发布不满 24h(dsh profile minimumReleaseAge=1440)的版本会静默回退到旧版本,
|
|
8
|
+
* 因此发布未满门槛的版本不该提示更新(否则用户点了也装不上)。
|
|
9
|
+
* 结果按包名内存缓存,同一次会话内不重复请求;失败/超时/响应过大一律返回 null,绝不抛错、绝不阻塞。
|
|
10
|
+
*/
|
|
11
|
+
import { get as httpsGet } from 'node:https';
|
|
12
|
+
import { validPackageName } from './profile/profile.js';
|
|
13
|
+
import { fetchViaCurl } from './probe.js';
|
|
14
|
+
/** 请求体包名数量上限:只为已安装插件按需查询,几十个足矣,封顶防滥用。 */
|
|
15
|
+
export const MAX_RELEASE_AGE_PACKAGES = 50;
|
|
16
|
+
/** 单包查询超时(ms):慢网络下失败即返回 null,不拖住整个响应。 */
|
|
17
|
+
export const RELEASE_AGE_TIMEOUT_MS = 8000;
|
|
18
|
+
/** 响应体上限:完整 packument 可能达数 MB,超限直接放弃,避免异常大包拖垮服务。 */
|
|
19
|
+
const MAX_PACKUMENT_BYTES = 10 * 1024 * 1024;
|
|
20
|
+
export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
21
|
+
/** 会话内缓存(包名 → 版本与发布时间):只存成功结果,一次网络抖动不会把判断永久粘住。 */
|
|
22
|
+
const releaseCache = new Map();
|
|
23
|
+
/** 从完整 packument 取 dist-tags.latest 对应版本及其发布时间;缺任一字段返回 null。 */
|
|
24
|
+
export function latestReleaseOf(packument) {
|
|
25
|
+
if (packument === null || typeof packument !== 'object')
|
|
26
|
+
return null;
|
|
27
|
+
const doc = packument;
|
|
28
|
+
const tags = doc['dist-tags'];
|
|
29
|
+
const version = tags !== null && typeof tags === 'object' ? tags.latest : undefined;
|
|
30
|
+
if (typeof version !== 'string' || version === '')
|
|
31
|
+
return null;
|
|
32
|
+
const time = doc.time;
|
|
33
|
+
const publishedAt = time !== null && typeof time === 'object' ? time[version] : undefined;
|
|
34
|
+
if (typeof publishedAt !== 'string' || publishedAt === '')
|
|
35
|
+
return null;
|
|
36
|
+
return { version, publishedAt };
|
|
37
|
+
}
|
|
38
|
+
/** 请求体 → 去重后的合法包名列表;非数组 / 超量 / 含非法包名一律 null(拒绝非法输入)。 */
|
|
39
|
+
export function parseReleaseAgeRequest(body) {
|
|
40
|
+
if (body === null || typeof body !== 'object')
|
|
41
|
+
return null;
|
|
42
|
+
const raw = body.packages;
|
|
43
|
+
if (!Array.isArray(raw) || raw.length > MAX_RELEASE_AGE_PACKAGES)
|
|
44
|
+
return null;
|
|
45
|
+
const names = new Set();
|
|
46
|
+
for (const item of raw) {
|
|
47
|
+
if (typeof item !== 'string' || item === '' || item.startsWith('-') || !validPackageName(item))
|
|
48
|
+
return null;
|
|
49
|
+
names.add(item);
|
|
50
|
+
}
|
|
51
|
+
return [...names];
|
|
52
|
+
}
|
|
53
|
+
/** node:https 直连 GET,仅接受 200;超时/超限/网络失败返回 null。 */
|
|
54
|
+
function getJson(url, timeoutMs) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
let done = false;
|
|
57
|
+
const finish = (value) => { if (!done) {
|
|
58
|
+
done = true;
|
|
59
|
+
resolve(value);
|
|
60
|
+
} };
|
|
61
|
+
const req = httpsGet(url, { timeout: timeoutMs, headers: { accept: 'application/json' } }, (res) => {
|
|
62
|
+
if (res.statusCode !== 200) {
|
|
63
|
+
res.resume();
|
|
64
|
+
finish(null);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const declared = Number(res.headers['content-length']);
|
|
68
|
+
if (Number.isFinite(declared) && declared > MAX_PACKUMENT_BYTES) {
|
|
69
|
+
res.destroy();
|
|
70
|
+
finish(null);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let body = '';
|
|
74
|
+
let bytes = 0;
|
|
75
|
+
res.on('data', (chunk) => {
|
|
76
|
+
bytes += chunk.length;
|
|
77
|
+
if (bytes > MAX_PACKUMENT_BYTES) {
|
|
78
|
+
res.destroy();
|
|
79
|
+
finish(null);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
body += chunk.toString();
|
|
83
|
+
});
|
|
84
|
+
res.on('end', () => finish(body));
|
|
85
|
+
res.on('error', () => finish(null));
|
|
86
|
+
});
|
|
87
|
+
req.on('timeout', () => req.destroy());
|
|
88
|
+
req.on('error', () => finish(null));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** 拉取一个包的完整 packument 正文;配了代理时复用与安装/目录同一套代理口径(curl 子进程注入 proxy env)。 */
|
|
92
|
+
async function fetchPackument(url, proxy, timeoutMs) {
|
|
93
|
+
if (proxy !== '') {
|
|
94
|
+
const r = await fetchViaCurl(url, proxy, timeoutMs);
|
|
95
|
+
return r.ok && r.body !== '' && Buffer.byteLength(r.body) <= MAX_PACKUMENT_BYTES ? r.body : null;
|
|
96
|
+
}
|
|
97
|
+
return getJson(url, timeoutMs);
|
|
98
|
+
}
|
|
99
|
+
/** 查询一个包的最新版本与发布时间(成功结果缓存,同会话内同包不重复请求);registry 为空回退官方源,失败返回 null。 */
|
|
100
|
+
export async function lookupReleaseAge(packageName, registry, proxy, timeoutMs = RELEASE_AGE_TIMEOUT_MS) {
|
|
101
|
+
const cached = releaseCache.get(packageName);
|
|
102
|
+
if (cached !== undefined)
|
|
103
|
+
return cached;
|
|
104
|
+
const base = registry.replace(/\/+$/, '') || DEFAULT_NPM_REGISTRY;
|
|
105
|
+
const body = await fetchPackument(`${base}/${encodeURIComponent(packageName)}`, proxy, timeoutMs);
|
|
106
|
+
if (body === null)
|
|
107
|
+
return null;
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(body);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const release = latestReleaseOf(parsed);
|
|
116
|
+
if (release !== null)
|
|
117
|
+
releaseCache.set(packageName, release);
|
|
118
|
+
return release;
|
|
119
|
+
}
|
package/lib/services/probe.js
CHANGED
|
@@ -79,7 +79,7 @@ function macSystemProxy() {
|
|
|
79
79
|
* `http=host:port;https=host:port`(按协议分别配置)。
|
|
80
80
|
* 字符类必须同时排除 `/` 与 `=`:只排除 `=` 时,带 scheme 的值会让「主机名」吞掉 `//`,
|
|
81
81
|
* 拼出 `http:////127.0.0.1:10793` 这种立刻失败的非法地址 —— 代理本来是通的,却把诊断、
|
|
82
|
-
*
|
|
82
|
+
* 目录拉取、安装前网络探测全判成「不可达」。
|
|
83
83
|
*/
|
|
84
84
|
export function parseWinProxyServer(stdout) {
|
|
85
85
|
const m = /([^:\s=/]+):(\d+)/.exec(stdout);
|
|
@@ -199,13 +199,13 @@ export function probeUrl(url, proxy, timeoutMs) {
|
|
|
199
199
|
return curlProbe(target.href, proxy, timeoutMs);
|
|
200
200
|
}
|
|
201
201
|
/**
|
|
202
|
-
* git
|
|
202
|
+
* git 真实克隆握手探测:spawn git ls-remote(https 传输,与 pnpm 克隆前的
|
|
203
203
|
* ref 握手一致),注入代理 env,GIT_TERMINAL_PROMPT=0 防凭据提示挂起。
|
|
204
204
|
*
|
|
205
205
|
* 为什么要真实 git 而非 curl 打网页:网页「能打开」和 git「能克隆」是两码事 ——
|
|
206
206
|
* 防火墙/代理常按端口与协议区分,HTTP 页可达不代表 git 传输可达。这里测的就是
|
|
207
|
-
*
|
|
208
|
-
* 供系统诊断 GitHub
|
|
207
|
+
* 克隆握手本身,回答「能不能连上 github.com」。
|
|
208
|
+
* 供系统诊断 GitHub 可达性使用;探测目标用 dshplugin/hello-dsh 小仓库(秒级完成,
|
|
209
209
|
* 不打 17MB 的 dsh-plugin-hub 主页)。
|
|
210
210
|
*/
|
|
211
211
|
export function gitLsRemote(url, proxy, timeoutMs) {
|
|
@@ -3,23 +3,19 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 已安装插件版本追踪。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* signal present at install time into a small per-profile JSON file, then
|
|
12
|
-
* compare it against the catalog's current signal later.
|
|
8
|
+
* 已安装表(routes.ts 的 `readInstalled`)只知道每个 Git 依赖的 manifest spec,不含版本号。
|
|
9
|
+
* 为让插件中心能判断「有更新」,我们把安装时的目录信号记进一个 per-profile 的小 JSON 文件,
|
|
10
|
+
* 之后再与目录当前信号比对。
|
|
13
11
|
*
|
|
14
|
-
*
|
|
15
|
-
* - version
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Repos that never publish a release fall back to this: a newer push time
|
|
19
|
-
* means new commits → an update is available.
|
|
12
|
+
* 两个信号覆盖所有仓库:
|
|
13
|
+
* - version:目录最新 release tag(如 v1.2.3),发 release 的仓库按它比对。
|
|
14
|
+
* - updatedAt:仓库最近 push 时间(GitHub pushed_at,即 `repoUpdatedAt`),
|
|
15
|
+
* 从不发 release 的仓库回退用它:push 时间变新即有新提交 → 有更新。
|
|
20
16
|
*
|
|
21
|
-
*
|
|
22
|
-
* { "<owner>/<repo>"
|
|
17
|
+
* 存储:`DSH_HOME/profiles/<profile>/gro.ngilp-hsd-versions.json`
|
|
18
|
+
* { "<owner>/<repo>"(小写): { version, updatedAt, installedAt } }
|
|
23
19
|
*/
|
|
24
20
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
25
21
|
import { homedir } from 'node:os';
|
|
@@ -36,20 +32,18 @@ export function readInstalledVersions(profile) {
|
|
|
36
32
|
return {};
|
|
37
33
|
}
|
|
38
34
|
}
|
|
39
|
-
/**
|
|
35
|
+
/** 记录某仓库安装时的目录信号(版本 + 仓库更新时间)。 */
|
|
40
36
|
export function recordInstalledVersion(profile, repo, version, updatedAt) {
|
|
41
37
|
const table = readInstalledVersions(profile);
|
|
42
38
|
const key = repo.toLowerCase();
|
|
43
|
-
// 合并写入:保留已记录的 npmPackage(
|
|
39
|
+
// 合并写入:保留已记录的 npmPackage(repo → 包名映射),
|
|
44
40
|
// 客户端同步版本信号时不能把它覆盖掉
|
|
45
41
|
table[key] = { ...table[key], version, updatedAt, installedAt: new Date().toISOString() };
|
|
46
42
|
writeVersions(profile, table);
|
|
47
43
|
}
|
|
48
44
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* harmless when the repo is not (yet) installed — the client only treats a repo
|
|
52
|
-
* as installed when its dependency key actually exists.
|
|
45
|
+
* 记录某仓库解析出的 npm 包名。在 `/install` 时调用,让映射在任务完成前就存在;
|
|
46
|
+
* 仓库尚未安装时也无害 —— 客户端只在依赖 key 真实存在时才把仓库当作已安装。
|
|
53
47
|
*/
|
|
54
48
|
export function recordResolvedNpmPackage(profile, repo, npmPackage) {
|
|
55
49
|
const table = readInstalledVersions(profile);
|
|
@@ -57,7 +51,7 @@ export function recordResolvedNpmPackage(profile, repo, npmPackage) {
|
|
|
57
51
|
table[key] = { ...table[key], npmPackage };
|
|
58
52
|
writeVersions(profile, table);
|
|
59
53
|
}
|
|
60
|
-
/**
|
|
54
|
+
/** 清除某仓库记录的版本(卸载时调用)。 */
|
|
61
55
|
export function removeInstalledVersion(profile, repo) {
|
|
62
56
|
const table = readInstalledVersions(profile);
|
|
63
57
|
delete table[repo.toLowerCase()];
|
|
@@ -11,7 +11,7 @@ import { homedir } from 'node:os';
|
|
|
11
11
|
import { basename, dirname, join } from 'node:path';
|
|
12
12
|
import { PACKAGE_RE, REPO_RE } from '../install/install-types.js';
|
|
13
13
|
import { githubReleaseTarget } from '../install/release-target.js';
|
|
14
|
-
/**
|
|
14
|
+
/** 从启动参数解析当前 profile,取不到落回 `web`。 */
|
|
15
15
|
export function readProfileArg(fallback = 'web') {
|
|
16
16
|
return profileFromArgv(process.argv, fallback);
|
|
17
17
|
}
|
|
@@ -46,44 +46,18 @@ export function profileFromArgv(argv, fallback = 'web') {
|
|
|
46
46
|
}
|
|
47
47
|
return fallback;
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
49
|
+
/** 解析 profile 目录(`DSH_HOME` 或 `~/.dsh`)。 */
|
|
50
50
|
export function profileDirectory(profile) {
|
|
51
51
|
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile);
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
54
|
-
*
|
|
55
|
-
* explicitly pinned revision reaches the installer instead of being dropped. */
|
|
53
|
+
/** 把仓库身份转成规范的 GitHub 目标(git+https),仓库不合法时返回 null。
|
|
54
|
+
* 带 ref 时按 Git ref(`#tag` / `#commit`)拼接;安装通道已不再使用 git 目标,此函数仅做身份规范化。 */
|
|
56
55
|
export function githubTarget(repo, ref) {
|
|
57
56
|
if (typeof repo !== 'string' || !REPO_RE.test(repo))
|
|
58
57
|
return null;
|
|
59
58
|
const url = `git+https://github.com/${repo}.git`;
|
|
60
59
|
return ref === undefined || ref === null || ref === '' ? url : `${url}#${ref}`;
|
|
61
60
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Extract a validated explicit Git ref (`#<ref>`) from a GitHub spec, or null.
|
|
64
|
-
* Only the fragment of a Git-style address qualifies — a pinned tag/commit is
|
|
65
|
-
* user intent and must survive target normalization, whereas `#…` on a plain
|
|
66
|
-
* web URL (`https://github.com/o/r#readme`) is a page anchor, not a ref.
|
|
67
|
-
* Git-style = `git+…` / `github:` / `git@…` / `….git` / bare `owner/repo`.
|
|
68
|
-
* Grammar mirrors a Git ref: no leading `.`/`-`, no `#`/whitespace.
|
|
69
|
-
*/
|
|
70
|
-
const GIT_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
71
|
-
export function githubRefOf(value) {
|
|
72
|
-
if (typeof value !== 'string')
|
|
73
|
-
return null;
|
|
74
|
-
const input = value.trim();
|
|
75
|
-
const hash = input.indexOf('#');
|
|
76
|
-
if (hash <= 0)
|
|
77
|
-
return null;
|
|
78
|
-
const base = input.slice(0, hash);
|
|
79
|
-
if (githubRepoOf(base) === null)
|
|
80
|
-
return null;
|
|
81
|
-
const gitStyle = /^(?:git\+|github:|git@)/i.test(base) || /\.git$/i.test(base) || !base.includes('://');
|
|
82
|
-
if (!gitStyle)
|
|
83
|
-
return null;
|
|
84
|
-
const ref = input.slice(hash + 1);
|
|
85
|
-
return GIT_REF_RE.test(ref) ? ref : null;
|
|
86
|
-
}
|
|
87
61
|
/**
|
|
88
62
|
* DSH 命令行前缀(官方唯一形式 `dsh plugin --profile <name> <add|update> <target>`)—— 提取其中的安装目标;
|
|
89
63
|
* 非命令输入原样返回。兼容「直接粘贴完整命令」(防用户输错):--profile 必填且只有长选项
|
|
@@ -114,7 +88,7 @@ export function globalNpmPackagesOf(value) {
|
|
|
114
88
|
return null;
|
|
115
89
|
return packages;
|
|
116
90
|
}
|
|
117
|
-
/**
|
|
91
|
+
/** 从目录值或已装 Git spec 中提取 owner/repo 身份。 */
|
|
118
92
|
export function githubRepoOf(value) {
|
|
119
93
|
if (typeof value !== 'string')
|
|
120
94
|
return null;
|
|
@@ -142,23 +116,22 @@ export function githubRepoOf(value) {
|
|
|
142
116
|
}
|
|
143
117
|
return null;
|
|
144
118
|
}
|
|
145
|
-
/**
|
|
119
|
+
/** 校验 npm 包名(卸载目标的语法)。 */
|
|
146
120
|
export function validPackageName(name) {
|
|
147
121
|
return typeof name === 'string' && PACKAGE_RE.test(name);
|
|
148
122
|
}
|
|
149
123
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* the only reliable key is the one pnpm prints verbatim. Null when absent.
|
|
124
|
+
* 取 pnpm 在 `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` 提示里原样打印的 `allowBuilds` key。
|
|
125
|
+
* Git 安装时 pnpm 按 `name@<实际 fetch URL>`(如
|
|
126
|
+
* `https://codeload.github.com/<owner>/<repo>/tar.gz/<commit>`)匹配,而不是 `git+ssh://`
|
|
127
|
+
* 或分支 spec —— 裸包名与 `git+` spec 都匹配不上,唯一可靠的 key 就是 pnpm 原样输出的那个。
|
|
128
|
+
* 无则返回 null。
|
|
156
129
|
*/
|
|
157
130
|
export function parseAllowBuildsKey(output) {
|
|
158
131
|
const m = /^[ \t]*(\S+@\S+):[ \t]*true[ \t]*$/m.exec(output);
|
|
159
132
|
return m === null ? null : m[1].trim();
|
|
160
133
|
}
|
|
161
|
-
/**
|
|
134
|
+
/** 当裸标量非法时,为 YAML 块映射 key 加引号。 */
|
|
162
135
|
function quoteYamlKey(key) {
|
|
163
136
|
if (/^[-?:,[\]{}#&*!|>'"%@`]/.test(key) || /:(\s|$)/.test(key)) {
|
|
164
137
|
return `'${key.replace(/'/g, "''")}'`;
|
|
@@ -166,10 +139,9 @@ function quoteYamlKey(key) {
|
|
|
166
139
|
return key;
|
|
167
140
|
}
|
|
168
141
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* `set this to true or false` value) are dropped on rewrite.
|
|
142
|
+
* 向 profile 的 pnpm-workspace.yaml 追加一条 `allowBuilds` 记录:与已有条目合并,
|
|
143
|
+
* 其余 yaml 保持不变;重写时丢弃损坏的占位条目(pnpm 安装失败的 bug 会写入
|
|
144
|
+
* 字面量 `set this to true or false`)。
|
|
173
145
|
*/
|
|
174
146
|
export function addAllowBuildsKey(profile, key) {
|
|
175
147
|
const file = join(profileDirectory(profile), 'pnpm-workspace.yaml');
|
|
@@ -177,7 +149,7 @@ export function addAllowBuildsKey(profile, key) {
|
|
|
177
149
|
try {
|
|
178
150
|
yaml = readFileSync(file, 'utf8');
|
|
179
151
|
}
|
|
180
|
-
catch { /*
|
|
152
|
+
catch { /* 文件不存在,下方创建 */ }
|
|
181
153
|
const eol = /\r\n/.test(yaml) ? '\r\n' : '\n';
|
|
182
154
|
const blockRe = /allowBuilds:[ \t]*\r?\n((?:[ \t]+[^\r\n]*\r?\n?)*)/g;
|
|
183
155
|
const map = new Map();
|
|
@@ -3,22 +3,19 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 安装执行器共用的纯进度估算工具。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* line and map it onto a 0-100 estimate; keeping them free of I/O makes the
|
|
12
|
-
* estimation logic directly unit-testable (see tests/progress.test.ts).
|
|
8
|
+
* dsh CLI 包装了 pnpm,pnpm 用回车原地刷新 `Progress: …` 行,因此原始输出是一串碎片。
|
|
9
|
+
* 这些工具把单个碎片归一化成干净的终端行,并映射到 0-100 估算值;不碰 I/O
|
|
10
|
+
* 让估算逻辑可直接单测(见 tests/progress.test.ts)。
|
|
13
11
|
*/
|
|
14
|
-
/**
|
|
12
|
+
/** 剥掉 `[out]` / `[err]` 来源前缀与 ANSI 转义序列。 */
|
|
15
13
|
export function cleanLine(raw) {
|
|
16
14
|
return raw.replace(/^\[(?:err|out)\]\s*/, '').replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').trim();
|
|
17
15
|
}
|
|
18
16
|
/**
|
|
19
|
-
*
|
|
20
|
-
* `Progress: resolved N, reused X, downloaded Y, added Z
|
|
21
|
-
* phase; resolution/install phase lines bump the estimate towards done.
|
|
17
|
+
* 从单行 CLI 输出估算 0-100 进度。pnpm 在 fetch 阶段输出
|
|
18
|
+
* `Progress: resolved N, reused X, downloaded Y, added Z`;解析/安装阶段的行把估算值推向完成。
|
|
22
19
|
*/
|
|
23
20
|
export function estimateProgress(line) {
|
|
24
21
|
const progress = line.match(/Progress:\s*(.+)/);
|
|
@@ -26,7 +23,7 @@ export function estimateProgress(line) {
|
|
|
26
23
|
let resolved = 0;
|
|
27
24
|
let handled = 0;
|
|
28
25
|
for (const part of progress[1].split(',')) {
|
|
29
|
-
// pnpm
|
|
26
|
+
// pnpm 输出 `label count` 形式的 token,如 `resolved 100, reused 40`
|
|
30
27
|
const [label, raw] = part.trim().split(/\s+/);
|
|
31
28
|
const value = Number(raw);
|
|
32
29
|
if (label === 'resolved')
|
package/lib/services/settings.js
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* back to DEFAULT_SETTINGS so old files stay readable as new options land.
|
|
6
|
+
* 插件中心设置持久化:每个活跃 profile 一个 `hub-settings.json`(与 pnpm-workspace.yaml 同目录)。
|
|
7
|
+
* 所有字段可选;缺失的 key 回退到 DEFAULT_SETTINGS,新选项上线后旧文件仍可读。
|
|
9
8
|
*/
|
|
10
9
|
import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
11
10
|
import { dirname, join } from 'node:path';
|
|
@@ -15,8 +14,6 @@ export const DEFAULT_SETTINGS = {
|
|
|
15
14
|
proxy: '',
|
|
16
15
|
npmRegistry: '',
|
|
17
16
|
enableNpmInstall: true,
|
|
18
|
-
enableGitInstall: true,
|
|
19
|
-
enableDshInstall: true,
|
|
20
17
|
logPath: '',
|
|
21
18
|
};
|
|
22
19
|
export function settingsFile(profile) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type WebRoute, type WebServerService } from './shared.ts';
|
|
2
2
|
export declare function profileDirectory(profile: string): string;
|
|
3
|
-
/**
|
|
3
|
+
/** 读取安装进某 profile 的非官方依赖。 */
|
|
4
4
|
export declare function readInstalled(profile: string): Record<string, string>;
|
|
5
5
|
/** 读已装依赖实际落在磁盘上的仓库身份(`owner/repo`):解析 node_modules/<name>/package.json
|
|
6
6
|
* 的 repository 字段。已装 spec 可能是纯版本号(丢了仓库身份),但真实包元数据仍指向来源仓库,
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* 安装/卸载路由:校验目标后 spawn 官方 dsh CLI(见 services/install/install.ts),
|
|
7
|
+
* 并回传捕获的结果。自定义安装只接受 npm 包名。
|
|
8
8
|
*/
|
|
9
9
|
import { type LoaderHandle } from '../services/install/install.ts';
|
|
10
10
|
import { type WebRoute, type WebServerService } from './shared.ts';
|
|
@@ -3,24 +3,22 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* install
|
|
9
|
-
* (see services/install/install.ts) and reports the captured result back.
|
|
6
|
+
* 本地 HTTP 路由:把真实安装能力暴露给应用内的插件中心 UI。客户端请求同源
|
|
7
|
+
* `/dsh-plugin-hub/*` 端点;安装处理校验目标后 spawn 官方 dsh CLI(见
|
|
8
|
+
* services/install/install.ts)并回传捕获的结果。
|
|
10
9
|
*/
|
|
11
10
|
import { type LoaderHandle } from '../services/install/install.ts';
|
|
12
11
|
import { type WebServerService } from './shared.ts';
|
|
13
12
|
/**
|
|
14
|
-
*
|
|
15
|
-
* @param webServer - DSH web server
|
|
16
|
-
* @param profile - profile
|
|
17
|
-
* @param loader -
|
|
18
|
-
* immediately so the page survives a refresh without a host restart.
|
|
13
|
+
* 在宿主 web server 上注册插件中心 API 并返回 dispose 函数。
|
|
14
|
+
* @param webServer - DSH web server 服务。
|
|
15
|
+
* @param profile - 拥有插件变更权限的 profile。
|
|
16
|
+
* @param loader - 运行中 loader(可选):让卸载立即移除条目,页面刷新即生效、无需重启宿主。
|
|
19
17
|
*/
|
|
20
18
|
export declare function mountPluginHubRoutes(webServer: WebServerService, profile: string, loader?: LoaderHandle): () => void;
|
|
21
|
-
/**
|
|
19
|
+
/** 与客户端路由文档共用的 profile 解析。 */
|
|
22
20
|
export { readProfileArg } from '../services/install/install.ts';
|
|
23
|
-
/**
|
|
21
|
+
/** 转出以便宿主入口(index.ts)继续从本模块引入。 */
|
|
24
22
|
export type { WebServerService } from './shared.ts';
|
|
25
|
-
/**
|
|
23
|
+
/** 转出以便 tests/routes.test.ts 继续从本模块引入。 */
|
|
26
24
|
export { isDesktopHost, isSameOrigin } from './shared.ts';
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* route modules in this directory.
|
|
6
|
+
* 共享 HTTP 工具:路由/服务类型、请求校验助手,以及守卫每个 POST 变更的同源校验。
|
|
7
|
+
* 由本目录其它路由模块引入。
|
|
9
8
|
*/
|
|
10
9
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
11
10
|
/**
|
|
@@ -26,7 +25,7 @@ export interface WebRoute {
|
|
|
26
25
|
path: string;
|
|
27
26
|
handler: (request: IncomingMessage, response: ServerResponse) => void | Promise<void>;
|
|
28
27
|
}
|
|
29
|
-
/**
|
|
28
|
+
/** 本插件用到的宿主 web-server 服务子集。 */
|
|
30
29
|
export interface WebServerService {
|
|
31
30
|
register(route: WebRoute): () => void;
|
|
32
31
|
}
|
|
@@ -36,8 +35,8 @@ export declare const COMMAND_TIMEOUT_MS: number;
|
|
|
36
35
|
* 不重复走 curl 拉远程 —— 重启宿主后首次打开同样秒开(缓存跨重启存活)。 */
|
|
37
36
|
export declare const CATALOG_CACHE_TTL_MS: number;
|
|
38
37
|
export declare function sendJson(response: ServerResponse, status: number, value: unknown): void;
|
|
39
|
-
/** POST
|
|
40
|
-
*
|
|
38
|
+
/** POST 变更只接受来自本地 web server 来源或桌面宿主页面。转出供测试(tests/routes.test.ts)使用 ——
|
|
39
|
+
* 来源校验就是全部的 CSRF 防御。 */
|
|
41
40
|
export declare function isSameOrigin(request: IncomingMessage): boolean;
|
|
42
41
|
export declare function requireMethod(request: IncomingMessage, response: ServerResponse, method: 'GET' | 'POST'): boolean;
|
|
43
42
|
export declare function requireTrustedPost(request: IncomingMessage, response: ServerResponse): boolean;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -3,18 +3,17 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
* dsh-plugin
|
|
7
|
-
*
|
|
8
|
-
* these same-origin routes to perform real installs from inside the app.
|
|
6
|
+
* dsh-plugin 宿主入口:宿主 webServer 服务就绪后挂载插件中心的 HTTP 路由。
|
|
7
|
+
* 浏览器 bundle(src/client)通过这些同源路由在应用内执行真正的安装。
|
|
9
8
|
*/
|
|
10
9
|
import { type LoaderHandle } from './services/install/install.ts';
|
|
11
10
|
import { type WebServerService } from './http/routes.ts';
|
|
12
11
|
export declare const name = "dsh-plugin";
|
|
13
12
|
export interface Config {
|
|
14
|
-
/** DSH profile
|
|
13
|
+
/** 拥有插件变更权限的 DSH profile,缺省用启动时的 profile。 */
|
|
15
14
|
profile?: string;
|
|
16
15
|
}
|
|
17
|
-
/**
|
|
16
|
+
/** 本插件用到的 cordis 宿主上下文子集。 */
|
|
18
17
|
interface HostContext {
|
|
19
18
|
webServer: WebServerService;
|
|
20
19
|
/** 运行中 loader:卸载成功后主动移除条目、立即生效(宿主未提供时卸载仍需重启) */
|
|
@@ -23,9 +22,9 @@ interface HostContext {
|
|
|
23
22
|
effect(callback: () => void, label?: string): void;
|
|
24
23
|
}
|
|
25
24
|
/**
|
|
26
|
-
*
|
|
27
|
-
* @param ctx - cordis
|
|
28
|
-
* @param config -
|
|
25
|
+
* webServer 服务就绪后挂载插件中心路由。
|
|
26
|
+
* @param ctx - cordis 宿主上下文。
|
|
27
|
+
* @param config - 可选的 profile 覆盖。
|
|
29
28
|
*/
|
|
30
29
|
export declare function apply(ctx: HostContext, config?: Config): void;
|
|
31
30
|
export {};
|
|
@@ -15,27 +15,26 @@ export interface InstallResult {
|
|
|
15
15
|
}
|
|
16
16
|
export interface InstallTask {
|
|
17
17
|
id: number;
|
|
18
|
-
/**
|
|
18
|
+
/** 操作目标:npm 包名(安装、更新、卸载一致),冲突/恢复时需要展示给用户 */
|
|
19
19
|
target: string;
|
|
20
20
|
/** 展示用目标(owner/repo):npm 安装时 target 是包名,前端恢复用此字段显示仓库名,保持用户无感知 */
|
|
21
21
|
displayTarget?: string;
|
|
22
|
-
/**
|
|
23
|
-
* 失败提 Issue
|
|
24
|
-
* 仅凭仓库名猜不到,作者看到我们查过/试过的命令就能直接指认)。 */
|
|
22
|
+
/** 尝试过的安装方式(实际执行过的 CLI 命令,按先后顺序):
|
|
23
|
+
* 失败提 Issue 时如实贴给作者,作者据此判断我们真正执行了什么。 */
|
|
25
24
|
attempts: string[];
|
|
26
25
|
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时任务执行全局 npm 安装,不进任何 profile */
|
|
27
26
|
globalNpm?: string[];
|
|
28
27
|
action: 'add' | 'remove' | 'update';
|
|
29
|
-
/**
|
|
28
|
+
/** 自定义安装入口渠道(NPM 包卡片):
|
|
30
29
|
* 任务输出行与系统日志据此溯源「从哪个入口发起」;目录插件安装无此字段。 */
|
|
31
|
-
installChannel?: 'npm'
|
|
30
|
+
installChannel?: 'npm';
|
|
32
31
|
/** 队列语义:pending 排队中 / running 执行中 / done / failed / cancelled(用户取消) */
|
|
33
32
|
status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled';
|
|
34
33
|
timedOut: boolean;
|
|
35
34
|
exitCode: number | null;
|
|
36
35
|
/** 0-100 估算进度:解析 pnpm 的 `Progress: resolved…` 输出,阶段行兜底 */
|
|
37
36
|
progress: number;
|
|
38
|
-
/**
|
|
37
|
+
/** 输出行最新在前(消费方展示尾部)。 */
|
|
39
38
|
lines: string[];
|
|
40
39
|
/** 完成后是否仍需宿主重启才生效:仅「热挂载失败 / 非 dsh 插件 / 更新(ESM 缓存取不到新代码)/
|
|
41
40
|
* 无 loader」等刷新解决不了的兜底场景为 true(弹窗给「立即重启」);其余一律 false。 */
|
|
@@ -66,7 +65,7 @@ export interface QueueItem {
|
|
|
66
65
|
timeoutMs?: number;
|
|
67
66
|
env?: NodeJS.ProcessEnv;
|
|
68
67
|
globalNpm?: string[];
|
|
69
|
-
installChannel?: 'npm'
|
|
68
|
+
installChannel?: 'npm';
|
|
70
69
|
};
|
|
71
70
|
}
|
|
72
71
|
/**
|
|
@@ -74,20 +73,20 @@ export interface QueueItem {
|
|
|
74
73
|
* 它自然清空,正好等于「重启后提醒消失」的语义,无需任何重启探测。
|
|
75
74
|
*/
|
|
76
75
|
export interface PendingRestart {
|
|
77
|
-
/**
|
|
76
|
+
/** 安装目标(与任务一致;卸载时为展示用的 owner/repo) */
|
|
78
77
|
target: string;
|
|
79
78
|
/** 待重启语义:install 装完等挂载生效 / uninstall 卸完等清理 loader 残留 */
|
|
80
79
|
kind: 'install' | 'uninstall';
|
|
81
80
|
/** 任务完成时刻(epoch ms) */
|
|
82
81
|
at: number;
|
|
83
82
|
}
|
|
84
|
-
/**
|
|
83
|
+
/** 目录 GitHub 仓库身份(`owner/repo`)的语法。 */
|
|
85
84
|
export declare const REPO_RE: RegExp;
|
|
86
|
-
/**
|
|
85
|
+
/** npm 包名的语法(用于卸载目标)。 */
|
|
87
86
|
export declare const PACKAGE_RE: RegExp;
|
|
88
87
|
/** 捕获输出上限:stdout/stderr 各保留尾部 N 字节。 */
|
|
89
88
|
export declare const CAPTURE_LIMIT_BYTES: number;
|
|
90
|
-
/**
|
|
89
|
+
/** 每个任务保留的最大输出行数(最新优先)。 */
|
|
91
90
|
export declare const MAX_TASK_LINES = 200;
|
|
92
|
-
/**
|
|
91
|
+
/** 最多跟踪的任务数;超出时先丢弃最早结束的任务。 */
|
|
93
92
|
export declare const MAX_TASKS = 50;
|
|
@@ -18,5 +18,5 @@ export type { InstallResult, InstallTask, Invocation, PendingRestart, QueueItem
|
|
|
18
18
|
export type { LoaderHandle } from '../loader.ts';
|
|
19
19
|
export { addPendingRestart, clearPendingRestart, listPendingRestarts } from '../profile/pending-restart.ts';
|
|
20
20
|
export { dumpLoaderEntries, removeLoadedEntry } from '../loader.ts';
|
|
21
|
-
export { addAllowBuildsKey,
|
|
21
|
+
export { addAllowBuildsKey, githubRepoOf, githubTarget, globalNpmPackagesOf, installTargetOf, parseAllowBuildsKey, readProfileArg, validPackageName, } from '../profile/profile.ts';
|
|
22
22
|
export { activeTask, cancelTask, getTask, hasQueuedTarget, hasRunningTask, runPluginMutation, startPluginMutation } from './task-queue.ts';
|