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
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 本机 npm 环境检测:判断一次安装失败是否源于「本机 npm 版本过低」。
|
|
7
7
|
*
|
|
8
|
-
* 背景:pnpm
|
|
9
|
-
*
|
|
8
|
+
* 背景:pnpm 在带 `prepare` 构建脚本的依赖上会调 npm 装依赖(npm 包同样可能带原生
|
|
9
|
+
* 依赖或 git 依赖),npm arborist 在解 peer 依赖时有已知内部崩溃 ——
|
|
10
|
+
* `Cannot read properties of null (reading 'edgesOut')`。这是 npm 自身缺陷,
|
|
10
11
|
* 该报错会被 pnpm 包进 ERR_PNPM_PREPARE_PACKAGE 输出,不能按插件打包问题引导用户提 Issue。
|
|
11
12
|
*
|
|
12
13
|
* 这里的 `npmTooLowMarker` 是纯函数(输出文本 + 环境 → 标记行或 null),不触碰任务/队列,
|
|
@@ -3,57 +3,18 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
* npm
|
|
7
|
-
*
|
|
6
|
+
* npm 包校验:确认某个 npm 包确实属于该 GitHub 仓库、且声明为 DSH 插件
|
|
7
|
+
* (包元数据里的 repository 指向该仓库,并带 `dsh-plugin` 关键词或 `dsh` manifest)。
|
|
8
8
|
*
|
|
9
9
|
* 这是通用机制,不针对任何具体插件:只要作者发布了 repository 指向该
|
|
10
|
-
* GitHub 仓库的 npm 包(scope 名与 GitHub
|
|
11
|
-
* 从而把 git 分发不完整的插件切换到 npm 通道安装。命中结果带内存缓存,
|
|
12
|
-
* 避免同一会话内重复查询 registry。
|
|
10
|
+
* GitHub 仓库的 npm 包(scope 名与 GitHub 用户名是否一致无所谓),就能命中。
|
|
13
11
|
*/
|
|
14
12
|
import { get } from 'node:https';
|
|
15
13
|
import { githubRepoOf } from '../profile/profile.js';
|
|
16
14
|
import { finalUrl } from '../probe.js';
|
|
17
|
-
/**
|
|
18
|
-
const cache = new Map();
|
|
19
|
-
/** 单次 registry 查询超时(ms):慢网络下失败返回 null,不阻塞安装流程。 */
|
|
15
|
+
/** 单次 registry 查询超时(ms):慢网络下失败返回 false,不阻塞安装流程。 */
|
|
20
16
|
const REQUEST_TIMEOUT_MS = 8000;
|
|
21
|
-
/**
|
|
22
|
-
const SEARCH_RESULT_LIMIT = 250;
|
|
23
|
-
/**
|
|
24
|
-
* 反查 repo(`owner/repo`)对应的官方 npm 包名;未命中、网络异常或超时返回 null。
|
|
25
|
-
* 返回 null 不代表仓库一定没有 npm 包,只代表本次未能确认 —— 调用方应保留
|
|
26
|
-
* 原有错误路径,反查只是额外的一次尝试。只有「已确认」的结果(包名,或 200
|
|
27
|
-
* 响应下确认无匹配)会进缓存;网络异常/超时/解析失败不缓存,同一会话内下次
|
|
28
|
-
* 安装仍会重试反查,避免一次瞬时故障把整个会话锁死在 git 通道。
|
|
29
|
-
* registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
|
|
30
|
-
* 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
|
|
31
|
-
* proxy 参数:GitHub 改名跳转探测走的代理(与安装/诊断同一口径)。
|
|
32
|
-
* 慢网络下失败不阻塞安装。
|
|
33
|
-
*/
|
|
34
|
-
export async function resolveNpmPackage(repo, registry = '', proxy = '') {
|
|
35
|
-
const key = repo.toLowerCase();
|
|
36
|
-
if (cache.has(key))
|
|
37
|
-
return cache.get(key) ?? null;
|
|
38
|
-
let found = await searchRepo(repo, registry);
|
|
39
|
-
// 仓库改名后 npm 包的 repository 已指向新名,按旧名反查不到:跟随一次 GitHub 的
|
|
40
|
-
// 改名跳转拿到规范名再查一遍(旧地址本身仍可用,只是仓库名变了)。
|
|
41
|
-
if (found === null) {
|
|
42
|
-
const canonical = await canonicalRepo(repo, proxy);
|
|
43
|
-
if (canonical !== '' && canonical.toLowerCase() !== key)
|
|
44
|
-
found = await searchRepo(canonical, registry);
|
|
45
|
-
}
|
|
46
|
-
if (found !== undefined)
|
|
47
|
-
cache.set(key, found);
|
|
48
|
-
return found ?? null;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Pick the installable npm package for one GitHub repository. npm search is a
|
|
52
|
-
* relevance-ranked text search, not an exact repository index, so first filter
|
|
53
|
-
* by repository URL. If a monorepo publishes both a CLI/helper package and a
|
|
54
|
-
* DSH plugin, prefer the package that explicitly advertises the `dsh-plugin`
|
|
55
|
-
* keyword instead of whichever result npm happened to rank first.
|
|
56
|
-
*/
|
|
17
|
+
/** 从包元数据取 repository 地址:字符串或 `{ url }` 对象,缺省回退 search 结果的 links.repository。 */
|
|
57
18
|
function repositoryUrlOf(pkg) {
|
|
58
19
|
if (typeof pkg.repository === 'string')
|
|
59
20
|
return pkg.repository;
|
|
@@ -72,6 +33,10 @@ export function isDshPackageMetadataForRepo(pkg, repo) {
|
|
|
72
33
|
const manifestMarked = pkg.dsh !== null && typeof pkg.dsh === 'object';
|
|
73
34
|
return keywordMarked || manifestMarked;
|
|
74
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* 校验包名对应的 npm 包确实属于该仓库且声明为 DSH 插件:取 registry 上的 `latest`
|
|
38
|
+
* 元数据比对 repository 与插件标记。网络异常、超时、非 200 一律返回 false,不阻塞调用方。
|
|
39
|
+
*/
|
|
75
40
|
export function isDshNpmPackageForRepo(packageName, repo, registry = '', proxy = '') {
|
|
76
41
|
if (packageName === '' || repo === '')
|
|
77
42
|
return Promise.resolve(false);
|
|
@@ -121,55 +86,3 @@ async function canonicalRepo(repo, proxy) {
|
|
|
121
86
|
const canonical = final === null ? null : githubRepoOf(final);
|
|
122
87
|
return canonical !== null && canonical.toLowerCase() !== key ? canonical : '';
|
|
123
88
|
}
|
|
124
|
-
function matchingPackagesForRepo(objects, repo) {
|
|
125
|
-
const matches = [];
|
|
126
|
-
for (const obj of objects) {
|
|
127
|
-
const pkg = obj.package;
|
|
128
|
-
if (typeof pkg?.name !== 'string' || pkg.name === '')
|
|
129
|
-
continue;
|
|
130
|
-
const repoUrl = repositoryUrlOf(pkg);
|
|
131
|
-
if (githubRepoOf(repoUrl)?.toLowerCase() !== repo.toLowerCase())
|
|
132
|
-
continue;
|
|
133
|
-
const keywords = Array.isArray(pkg.keywords) ? pkg.keywords : [];
|
|
134
|
-
matches.push({
|
|
135
|
-
name: pkg.name,
|
|
136
|
-
isDshPlugin: keywords.some((keyword) => typeof keyword === 'string' && keyword.toLowerCase() === 'dsh-plugin'),
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
return matches;
|
|
140
|
-
}
|
|
141
|
-
export function selectNpmPackageForRepo(objects, repo) {
|
|
142
|
-
const matches = matchingPackagesForRepo(objects, repo);
|
|
143
|
-
return matches.find((candidate) => candidate.isDshPlugin)?.name ?? matches[0]?.name ?? null;
|
|
144
|
-
}
|
|
145
|
-
/** 用 npm search 接口按 repository 地址反查,并做铁证校验(包元数据必须指向该仓库)。
|
|
146
|
-
* 返回:包名 = 命中;null = 200 响应下确认无匹配;undefined = 本次未能确认(网络/超时/解析失败)。 */
|
|
147
|
-
function searchRepo(repo, registry) {
|
|
148
|
-
const [owner, name] = repo.split('/');
|
|
149
|
-
if (owner === undefined || name === undefined || name === '')
|
|
150
|
-
return Promise.resolve(null);
|
|
151
|
-
const base = registry === '' ? 'https://registry.npmjs.org' : registry.replace(/\/+$/, '');
|
|
152
|
-
const url = `${base}/-/v1/search?text=${encodeURIComponent(`repository:${owner}/${name}`)}&size=${SEARCH_RESULT_LIMIT}`;
|
|
153
|
-
return new Promise((resolve) => {
|
|
154
|
-
const req = get(url, { timeout: REQUEST_TIMEOUT_MS }, (res) => {
|
|
155
|
-
if (res.statusCode !== 200) {
|
|
156
|
-
res.resume();
|
|
157
|
-
resolve(undefined);
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
let body = '';
|
|
161
|
-
res.on('data', (chunk) => { body += chunk.toString(); });
|
|
162
|
-
res.on('end', () => {
|
|
163
|
-
try {
|
|
164
|
-
const data = JSON.parse(body);
|
|
165
|
-
resolve(selectNpmPackageForRepo(data.objects ?? [], `${owner}/${name}`));
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
resolve(undefined);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
req.on('timeout', () => req.destroy());
|
|
173
|
-
req.on('error', () => resolve(undefined));
|
|
174
|
-
});
|
|
175
|
-
}
|
|
@@ -4,17 +4,12 @@
|
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
6
|
* 分发文件解析:从 package.json 推出装完后必须真实存在的文件 —— 作者声明的入口
|
|
7
|
-
* (exports["."] / main)与 bundle 声明的补丁(dsh.bundle.patch
|
|
8
|
-
* 与 npm 通道(装后 statSync 校验)共用,保证两条通道对同一个包判定一致。
|
|
7
|
+
* (exports["."] / main)与 bundle 声明的补丁(dsh.bundle.patch),供装后校验使用。
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 例外是补丁自己 mount 了本包:dsh 的《打包与安装插件》里组合包的常规写法就是
|
|
16
|
-
* `- insert: - name: <本包名>`,宿主随后按包名 import 本包,入口那时必须存在。
|
|
17
|
-
* 所以只有补丁真的写了自己的包名,才把这个入口算进分发要求。
|
|
9
|
+
* 只在包自己声明了入口时才要求它存在:宿主加载 bundle 读的是它的 package.json 与声明的补丁,
|
|
10
|
+
* 从不 import 包本身。硬要一个作者没声明过的 index.js,会把完整的包判成分发残缺、把一条假缺陷
|
|
11
|
+
* 推到作者仓库。例外是补丁自己 mount 了本包(组合包的常规写法)—— 那时宿主按包名 import 本包,
|
|
12
|
+
* 入口必须存在,所以只有补丁真的写了自己的包名才把这个入口算进分发要求。
|
|
18
13
|
*/
|
|
19
14
|
/** exports 条件对象里按优先级尝试的键;Node 语义下 default 最宽,import/require 次之。 */
|
|
20
15
|
const EXPORT_CONDITION_KEYS = ['default', 'import', 'require'];
|
|
@@ -42,7 +37,7 @@ export function resolvePackageEntry(meta) {
|
|
|
42
37
|
/**
|
|
43
38
|
* 解析 `dsh.bundle.patch` 声明的补丁文件(相对包根,不带 `./` 前缀)。
|
|
44
39
|
* 形状不合规范(既不是字符串也不是字符串数组)在这里不判定:那是 dsh 启动时
|
|
45
|
-
* bundlePatchFiles
|
|
40
|
+
* bundlePatchFiles 的报错职责,装后校验只负责「声明了就要在包里存在」。
|
|
46
41
|
*/
|
|
47
42
|
export function resolveBundlePatchFiles(meta) {
|
|
48
43
|
const patch = meta.dsh?.bundle?.patch;
|
|
@@ -3,20 +3,9 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
* 官方 registry
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* ① 缺的包是 **profile 自己声明的依赖**(历史残留条目,指向一个从未发布的包)—— 用户环境问题:
|
|
10
|
-
* pnpm 每次解析依赖都会先卡在它上面,任何插件都装不进来(issue #73),提示删掉该条目即可;
|
|
11
|
-
* ② 缺的包来自**插件的依赖树**,且该包在官方 registry 上也不存在 —— 作者侧问题(漏声明依赖、
|
|
12
|
-
* 依赖被删除或从未发布),应引导用户投插件作者仓库;
|
|
13
|
-
* ③ 缺的包来自插件依赖树,但官方 registry 上有 —— 用户本机 registry 指向的镜像/私有源未同步
|
|
14
|
-
* 该包,属用户环境问题,提示换源即可。
|
|
15
|
-
* 故本模块先排除 ①(读 profile 的 package.json),再用**官方** registry(registry.npmjs.org,
|
|
16
|
-
* 绕开本机可能配置的镜像)复查存在性,产出 ② / ③ 的标记行。
|
|
17
|
-
*
|
|
18
|
-
* 与 npm-check.ts 同构:本模块只产出「标记行」,由 task-queue 追加到任务输出,前端按标记分类。
|
|
19
|
-
* fetch 与 profile 目录都可注入,便于单测覆盖全部结论而不依赖网络。
|
|
6
|
+
* 官方 registry 存在性复核:用官方源(绕开本机镜像)复查 404 的包,产出标记行区分
|
|
7
|
+
* 「profile 残留依赖」「插件依赖在官方源也不存在(作者侧)」「本机镜像未同步」三种情形;
|
|
8
|
+
* 标记由 task-queue 追加到任务输出,前端按标记归类。fetch 与 profile 目录可注入,便于单测。
|
|
20
9
|
*/
|
|
21
10
|
import { readFileSync } from 'node:fs';
|
|
22
11
|
import { join } from 'node:path';
|
|
@@ -74,8 +63,8 @@ export async function registryMissingMarker(output, opts) {
|
|
|
74
63
|
const name = missingPackageOf(output);
|
|
75
64
|
if (name === null)
|
|
76
65
|
return null;
|
|
77
|
-
//
|
|
78
|
-
//
|
|
66
|
+
// 该包是 profile 自己声明的依赖(用户环境里留下的条目)→ 用户环境问题,交给既有的 pnpmMissingDep
|
|
67
|
+
// 提示处理(那条提示就是教用户删掉它),不产出标记、不改归属
|
|
79
68
|
if (profileDependencyNames(opts.profileDir).some((dep) => dep.toLowerCase() === name.toLowerCase()))
|
|
80
69
|
return null;
|
|
81
70
|
const exists = await existsOnOfficialRegistry(name, opts.fetchImpl ?? fetch, opts.timeoutMs ?? 8000);
|
|
@@ -3,23 +3,22 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
* 预构建 GitHub release
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 校验共用(单一事实来源,避免两端口径漂移)。
|
|
6
|
+
* 预构建 release 直链的仓库身份解析。部分插件(如 loopx)当初以 GitHub release 的 .tgz
|
|
7
|
+
* 安装过,仓库 HEAD 反而是 monorepo 根、走 git 直装会因缺入口文件失败;老安装记录里留的
|
|
8
|
+
* 就是这种 release 直链。这里把「历史安装目标」还原成所属仓库身份,供已装态读回、队列
|
|
9
|
+
* 去重与展示复用(单一事实来源,避免两端口径漂移)。
|
|
11
10
|
*
|
|
12
11
|
* 只接受固定形态的 HTTPS `github.com/<owner>/<repo>/releases/download/<tag>/<asset>.tgz`:
|
|
13
|
-
*
|
|
12
|
+
* 输入是记录/展示数据、绝非 shell 代码,绝不执行其文本;跨仓跳转、外部主机、凭据、
|
|
14
13
|
* 移动的 latest 链接、query/fragment、多余 CLI 参数与 shell 片段一律拒绝。
|
|
15
14
|
*/
|
|
16
|
-
/**
|
|
15
|
+
/** 完整命令前缀:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf 同口径)。 */
|
|
17
16
|
const DSH_PLUGIN_CMD_RE = /^dsh[ \t]+plugin[ \t]+--profile(?:[ \t]+|=)[A-Za-z0-9_-]+[ \t]+(?:add|update)[ \t]+(.+)$/i;
|
|
18
17
|
/** 固定形态的 release .tgz 直链:主机、路径段与扩展名全部锁死。 */
|
|
19
18
|
const RELEASE_URL_RE = /^https:\/\/github\.com\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)\/releases\/download\/([A-Za-z0-9._+-]+)\/([A-Za-z0-9._+-]+\.tgz)$/;
|
|
20
19
|
/**
|
|
21
|
-
*
|
|
22
|
-
* @param value -
|
|
20
|
+
* 解析一个历史安装目标里的 release 直链。
|
|
21
|
+
* @param value - 完整命令(`dsh plugin --profile web add <url>`)或裸直链。
|
|
23
22
|
* @returns 命中返回 `{ target, repo }`,否则 null。
|
|
24
23
|
*/
|
|
25
24
|
export function githubReleaseTarget(value) {
|
|
@@ -14,31 +14,31 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
14
14
|
import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.js';
|
|
15
15
|
import { npmTooLowMarker } from './npm-check.js';
|
|
16
16
|
import { registryMissingMarker } from './registry-check.js';
|
|
17
|
-
import { backfillOperationLog } from './operation-log.js';
|
|
17
|
+
import { backfillOperationLog, operationLogPathOf } from './operation-log.js';
|
|
18
18
|
import { cleanLine, estimateProgress } from '../profile/progress.js';
|
|
19
19
|
import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '../loader.js';
|
|
20
20
|
import { addPendingRestart, clearPendingRestart } from '../profile/pending-restart.js';
|
|
21
21
|
import { addAllowBuildsKey, githubRepoOf, parseAllowBuildsKey, profileDirectory } from '../profile/profile.js';
|
|
22
22
|
import { appendLog } from '../log.js';
|
|
23
|
-
/**
|
|
23
|
+
/** 内存任务注册表,按任务 id 索引。 */
|
|
24
24
|
const tasks = new Map();
|
|
25
25
|
let nextTaskId = 1;
|
|
26
|
-
/**
|
|
26
|
+
/** 等待轮到的排队任务(FIFO)。队列 worker 同一时刻最多跑一个变更。 */
|
|
27
27
|
const queue = [];
|
|
28
|
-
/**
|
|
28
|
+
/** 当前运行任务的子进程(用于取消)。 */
|
|
29
29
|
const runningChildren = new Map();
|
|
30
|
-
/**
|
|
30
|
+
/** 任务快照(避免消费方改动实时对象)。 */
|
|
31
31
|
export function getTask(id) {
|
|
32
32
|
const task = tasks.get(id);
|
|
33
33
|
if (!task)
|
|
34
34
|
return undefined;
|
|
35
35
|
return { ...task, lines: task.lines.slice(0, MAX_TASK_LINES) };
|
|
36
36
|
}
|
|
37
|
-
/**
|
|
37
|
+
/** 全部未终态任务,按队列顺序(running 在前、pending 在后)。供客户端刷新页面后恢复队列。 */
|
|
38
38
|
export function activeTask() {
|
|
39
39
|
const found = [];
|
|
40
|
-
// 展示目标用 displayTarget(owner/repo):安装 target
|
|
41
|
-
//
|
|
40
|
+
// 展示目标用 displayTarget(owner/repo):安装 target 是 npm 包名,
|
|
41
|
+
// 队列恢复/待重启展示对用户保持仓库名无感知
|
|
42
42
|
const show = (task) => task.displayTarget ?? task.target;
|
|
43
43
|
for (const item of queue) {
|
|
44
44
|
const task = item.task;
|
|
@@ -72,24 +72,22 @@ export function activeTask() {
|
|
|
72
72
|
return found;
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* (the close event lands later) — the slot must stay held until then, otherwise
|
|
79
|
-
* a new task would start while the old pnpm still runs in the same profile dir.
|
|
75
|
+
* 变更子进程仍存活时为 true。并发占位以存活子进程集合为准,而非 task.status:
|
|
76
|
+
* 取消运行中任务会立即翻转其状态,但子进程可能仍在退出中(close 事件稍后才到)——
|
|
77
|
+
* 占位必须维持到那时,否则旧 pnpm 还在同一 profile 目录跑,新任务就开始了。
|
|
80
78
|
*/
|
|
81
79
|
export function hasRunningTask() {
|
|
82
80
|
return runningChildren.size > 0;
|
|
83
81
|
}
|
|
84
|
-
/**
|
|
82
|
+
/** 目标在队列里是否已有未终态任务(安装/卸载去重)。 */
|
|
85
83
|
export function hasQueuedTarget(target) {
|
|
86
84
|
return queue.some((it) => it.task.target === target && (it.task.status === 'pending' || it.task.status === 'running'));
|
|
87
85
|
}
|
|
88
86
|
/**
|
|
89
|
-
*
|
|
90
|
-
* - pending
|
|
91
|
-
* - running
|
|
92
|
-
*
|
|
87
|
+
* 取消排队中或运行中的任务。
|
|
88
|
+
* - pending:立即移出队列(从未 spawn)。
|
|
89
|
+
* - running:杀掉子进程;队列 worker 随后取下一个。
|
|
90
|
+
* 任务不存在或已结束时返回 false。
|
|
93
91
|
*/
|
|
94
92
|
export function cancelTask(id) {
|
|
95
93
|
const task = tasks.get(id);
|
|
@@ -132,6 +130,12 @@ function pushLine(task, line) {
|
|
|
132
130
|
task.progress = Math.max(task.progress, 6 + Math.min(79, task.lines.length * 2));
|
|
133
131
|
}
|
|
134
132
|
}
|
|
133
|
+
/** spawn 的 cwd:profile 目录(asar 路径不能作 cwd);目录不存在时回退进程工作目录,
|
|
134
|
+
* 否则 Node 报的是 `spawn <file> ENOENT`,与「可执行文件不存在」无法区分。 */
|
|
135
|
+
function spawnCwdFor(profile) {
|
|
136
|
+
const dir = profileDirectory(profile);
|
|
137
|
+
return existsSync(dir) ? dir : process.cwd();
|
|
138
|
+
}
|
|
135
139
|
function cliInvocation(profile) {
|
|
136
140
|
const entry = process.argv[1];
|
|
137
141
|
if (entry !== undefined) {
|
|
@@ -145,8 +149,8 @@ function cliInvocation(profile) {
|
|
|
145
149
|
return {
|
|
146
150
|
file: process.execPath,
|
|
147
151
|
prefixArgs: ['--expose-internals', bootstrap],
|
|
148
|
-
//
|
|
149
|
-
cwd:
|
|
152
|
+
// cwd 用 profile 目录(asar 路径不能作 cwd),不存在时由 spawnCwdFor 回退
|
|
153
|
+
cwd: spawnCwdFor(profile),
|
|
150
154
|
useShell: false,
|
|
151
155
|
// Electron 可执行文件只有带这个变量才按 Node 运行引导脚本
|
|
152
156
|
env: { ELECTRON_RUN_AS_NODE: '1' },
|
|
@@ -235,18 +239,13 @@ function mutationVerb(action) {
|
|
|
235
239
|
/** 系统日志入口渠道后缀:自定义安装(带渠道)在日志里体现「从哪张卡片发起」,
|
|
236
240
|
* 目录插件安装(无渠道)返回空串。日志中文语境,渠道名用中文口语。 */
|
|
237
241
|
function channelLabel(channel) {
|
|
238
|
-
if (channel === 'git')
|
|
239
|
-
return '(GitHub 源码入口)';
|
|
240
|
-
if (channel === 'dsh')
|
|
241
|
-
return '(DSH 命令入口)';
|
|
242
242
|
if (channel === 'npm')
|
|
243
243
|
return '(NPM 包入口)';
|
|
244
244
|
return '';
|
|
245
245
|
}
|
|
246
246
|
/**
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
* Progress is visible through `getTask(id)` / `activeTask()` until done.
|
|
247
|
+
* 插件变更入队并返回其任务。任务严格串行:队列 worker 只在上一个结束后才启动下一个。
|
|
248
|
+
* 完成前可通过 `getTask(id)` / `activeTask()` 查看进度。
|
|
250
249
|
*/
|
|
251
250
|
export function startPluginMutation(options) {
|
|
252
251
|
// 卸载入队时先快照「该包是否带客户端 UI」:卸载完成后包已从磁盘删除,读不到 package.json,
|
|
@@ -296,7 +295,7 @@ export function startPluginMutation(options) {
|
|
|
296
295
|
pumpQueue();
|
|
297
296
|
return task;
|
|
298
297
|
}
|
|
299
|
-
/**
|
|
298
|
+
/** 队列 worker:同一时刻最多跑一个变更;占位空出时取下一个 pending 任务。 */
|
|
300
299
|
function pumpQueue() {
|
|
301
300
|
if (hasRunningTask())
|
|
302
301
|
return;
|
|
@@ -358,9 +357,8 @@ function pumpQueue() {
|
|
|
358
357
|
});
|
|
359
358
|
}
|
|
360
359
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* Never rejects; failures surface through the result / task state.
|
|
360
|
+
* 执行一次 `dsh plugin --profile <profile> <action> <target>` spawn,把输出流式写到
|
|
361
|
+
* 跟踪的任务上,并以捕获结果 resolve。绝不 reject;失败通过结果/任务状态体现。
|
|
364
362
|
*/
|
|
365
363
|
function spawnMutation(options) {
|
|
366
364
|
const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, loader, globalNpm, updateNpm } = options;
|
|
@@ -382,8 +380,7 @@ function spawnMutation(options) {
|
|
|
382
380
|
const args = isGlobalNpm
|
|
383
381
|
? ['install', '-g', ...(globalNpm ?? [])]
|
|
384
382
|
: [...invocation.prefixArgs, 'plugin', '--profile', profile, cliAction, npmUpdateTarget];
|
|
385
|
-
//
|
|
386
|
-
// 失败提 Issue 时作为「已尝试的安装方式」随附
|
|
383
|
+
// 记录实际执行的命令(显示包名),失败提 Issue 时作为「已尝试的安装方式」随附
|
|
387
384
|
const executed = isGlobalNpm
|
|
388
385
|
? `npm install -g ${(globalNpm ?? []).join(' ')}`
|
|
389
386
|
: `dsh plugin --profile ${profile} ${cliAction} ${npmUpdateTarget}`;
|
|
@@ -427,15 +424,20 @@ function spawnMutation(options) {
|
|
|
427
424
|
});
|
|
428
425
|
}
|
|
429
426
|
catch (error) {
|
|
427
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
428
|
+
// 宿主自身入口起不来 → 补标记,前端据此归因
|
|
429
|
+
const marker = hostSpawnMarker(invocation, error);
|
|
430
430
|
if (task) {
|
|
431
431
|
task.status = 'failed';
|
|
432
432
|
task.exitCode = null;
|
|
433
|
-
pushLine(task, `[error] ${
|
|
433
|
+
pushLine(task, `[error] ${detail}`);
|
|
434
|
+
if (marker !== null)
|
|
435
|
+
pushLine(task, marker);
|
|
434
436
|
}
|
|
435
437
|
resolveOnce({
|
|
436
438
|
exitCode: null,
|
|
437
439
|
timedOut: false,
|
|
438
|
-
error:
|
|
440
|
+
error: marker === null ? detail : `${detail}\n${marker}`,
|
|
439
441
|
stdout,
|
|
440
442
|
stderr,
|
|
441
443
|
});
|
|
@@ -515,12 +517,16 @@ function spawnMutation(options) {
|
|
|
515
517
|
}, 3_000);
|
|
516
518
|
});
|
|
517
519
|
child.once('error', (error) => {
|
|
520
|
+
// 同同步抛出那条路径:宿主自身入口起不来 → 补标记
|
|
521
|
+
const marker = hostSpawnMarker(invocation, error);
|
|
518
522
|
if (task && task.status !== 'cancelled') {
|
|
519
523
|
task.status = 'failed';
|
|
520
524
|
task.exitCode = null;
|
|
521
525
|
pushLine(task, `[error] ${error.message}`);
|
|
526
|
+
if (marker !== null)
|
|
527
|
+
pushLine(task, marker);
|
|
522
528
|
}
|
|
523
|
-
settleOnce({ exitCode: null, timedOut: false, error: error.message
|
|
529
|
+
settleOnce({ exitCode: null, timedOut: false, error: marker === null ? error.message : `${error.message}\n${marker}`, stdout, stderr });
|
|
524
530
|
});
|
|
525
531
|
child.once('close', (code) => {
|
|
526
532
|
settleOnce({ exitCode: code, timedOut, error: null, stdout, stderr });
|
|
@@ -608,7 +614,7 @@ function spawnMutation(options) {
|
|
|
608
614
|
}
|
|
609
615
|
/**
|
|
610
616
|
* 安装后校验已装包里 package.json 声明的文件(main / exports["."].default 与
|
|
611
|
-
* dsh.bundle.patch)是否真实存在。
|
|
617
|
+
* dsh.bundle.patch)是否真实存在。npm 包也可能漏发构建产物(lib/ 等),pnpm 装完没有
|
|
612
618
|
* 报错,但宿主重启加载插件树时会 ERR_MODULE_NOT_FOUND 直接崩溃、网页打不开 —— 这里在
|
|
613
619
|
* 登记「待重启」前就把这类残缺包拦下。没有声明过的文件不在这里追究:没声明入口的
|
|
614
620
|
* bundle 包(只挂技能)本来就不需要入口,宿主读的是它的 package.json 与声明的补丁。
|
|
@@ -682,10 +688,8 @@ function annotateNpmTooLow(result, options) {
|
|
|
682
688
|
pushLine(options.task, line);
|
|
683
689
|
return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
|
|
684
690
|
}
|
|
685
|
-
/**
|
|
686
|
-
*
|
|
687
|
-
* registry-check.ts;这里之所以是异步,是因为要联网查官方 registry —— 只在安装失败且日志含 404
|
|
688
|
-
* 时发生,代价可忽略。 */
|
|
691
|
+
/** 安装失败且日志含 404 时,用官方源复查一次并注入标记(逻辑见 registry-check.ts)。
|
|
692
|
+
* 异步是因为要联网;只在失败路径上发生。 */
|
|
689
693
|
async function annotateRegistryMissing(result, options) {
|
|
690
694
|
if (options.action === 'remove' || result.exitCode === 0 || result.timedOut)
|
|
691
695
|
return result;
|
|
@@ -699,13 +703,23 @@ async function annotateRegistryMissing(result, options) {
|
|
|
699
703
|
return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
|
|
700
704
|
}
|
|
701
705
|
/**
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
706
|
+
* 宿主自身 CLI 调不起来时的 ASCII 标记行:spawn 我们解析出的宿主入口报 ENOENT 时返回
|
|
707
|
+
* `[host-spawn]`,供前端单独归因、不引导提 Issue。PATH 上的 dsh/npm 各有自己的标记,返回 null。
|
|
708
|
+
* 导出供单测覆盖判据。
|
|
709
|
+
*/
|
|
710
|
+
export function hostSpawnMarker(invocation, error) {
|
|
711
|
+
if (error?.code !== 'ENOENT')
|
|
712
|
+
return null;
|
|
713
|
+
if (invocation.file === 'dsh' || invocation.file === 'npm')
|
|
714
|
+
return null;
|
|
715
|
+
return `[host-spawn] the DSH host could not be launched (${invocation.file}: ENOENT)`;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* 执行插件变更,带一条恢复路径:当 `add` 因 pnpm 的 allowBuilds 门禁拦下构建脚本而失败 ——
|
|
719
|
+
* 或是 git 源包自己的 `prepare`(`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`,key 取自打印的
|
|
720
|
+
* key@url 提示),或是某依赖的原生构建(`ERR_PNPM_IGNORED_BUILDS`,key = 列出的 name@version)——
|
|
721
|
+
* 就把确切的 key 写进 profile 的 pnpm-workspace.yaml 并重试一次。
|
|
722
|
+
* 其它失败一律落到单次 spawn 的结果。
|
|
709
723
|
*/
|
|
710
724
|
export async function runPluginMutation(options) {
|
|
711
725
|
let result = await spawnMutation(options);
|
|
@@ -719,8 +733,7 @@ export async function runPluginMutation(options) {
|
|
|
719
733
|
const key = parseAllowBuildsKey(output);
|
|
720
734
|
if (key !== null) {
|
|
721
735
|
if (options.task) {
|
|
722
|
-
//
|
|
723
|
-
// retry streams progress as a running task again.
|
|
736
|
+
// 首次尝试已把任务标为失败;重置它,让重试重新以 running 任务形式流式输出进度。
|
|
724
737
|
options.task.status = 'running';
|
|
725
738
|
options.task.exitCode = null;
|
|
726
739
|
pushLine(options.task, `[allow build] ${key}`);
|
|
@@ -749,14 +762,14 @@ export async function runPluginMutation(options) {
|
|
|
749
762
|
}
|
|
750
763
|
}
|
|
751
764
|
}
|
|
752
|
-
// pnpm
|
|
765
|
+
// pnpm 安装本身成功,但目标包声明分发的文件缺失(分发物缺构建产物 / 缺声明的补丁)→ 判为失败:
|
|
753
766
|
// 撤销刚登记的待重启,避免用户重启时宿主加载残缺包直接崩溃。
|
|
754
767
|
// 全局 npm 安装不进 profile(profile deps 里没有这些包),跳过入口校验
|
|
755
768
|
if (options.action !== 'remove' && result.exitCode === 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
|
|
756
769
|
const verified = verifyInstalledEntry(options.profile, options.target);
|
|
757
770
|
if (verified.name !== null && verified.missing !== null) {
|
|
758
771
|
clearPendingRestart(options.displayTarget ?? options.target);
|
|
759
|
-
const detail = `[packaging] ${verified.name}: declared file missing: ${verified.missing} (
|
|
772
|
+
const detail = `[packaging] ${verified.name}: declared file missing: ${verified.missing} (the published package is missing this file — report it to the author)`;
|
|
760
773
|
if (options.task && options.task.status !== 'cancelled') {
|
|
761
774
|
options.task.status = 'failed';
|
|
762
775
|
options.task.exitCode = null;
|
|
@@ -766,12 +779,12 @@ export async function runPluginMutation(options) {
|
|
|
766
779
|
return { exitCode: 1, timedOut: false, error: `declared file missing: ${verified.missing}`, stdout: result.stdout, stderr: `${result.stderr}\n${detail}` };
|
|
767
780
|
}
|
|
768
781
|
}
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
// 只在本就不含任何错误码时回捞 —— pnpm 已把报错回显出来时无需多读一次文件。
|
|
782
|
+
// 宿主只把一行摘要(`dsh: plugin command failed; diagnostics: <path>`)回显到控制台、真正的报错
|
|
783
|
+
// 写进该日志文件时,前端拿不到任何错误特征、只能归为「未知」。按该路径回捞日志尾部补进失败信息。
|
|
784
|
+
// 只在输出本就不含错误码时回捞 —— pnpm 已回显报错时无需多读一次文件。
|
|
773
785
|
if (result.exitCode !== 0 && !result.timedOut && !/\bERR_[A-Z0-9_]+\b/.test(`${result.stderr}\n${result.stdout}`)) {
|
|
774
|
-
const
|
|
786
|
+
const output = `${result.stderr}\n${result.stdout}`;
|
|
787
|
+
const backfilled = backfillOperationLog(output);
|
|
775
788
|
if (backfilled !== null) {
|
|
776
789
|
if (options.task && options.task.status !== 'cancelled') {
|
|
777
790
|
pushLine(options.task, '[pnpm log] DSH did not echo pnpm output — recovered the operation log from disk');
|
|
@@ -781,6 +794,10 @@ export async function runPluginMutation(options) {
|
|
|
781
794
|
}
|
|
782
795
|
result = { ...result, stderr: `${result.stderr}\n${backfilled}` };
|
|
783
796
|
}
|
|
797
|
+
else if (options.task && options.task.status !== 'cancelled' && operationLogPathOf(output) !== null) {
|
|
798
|
+
// 诊断路径在、日志却取不到:留一行证据,否则下一次同类上报又只剩宿主那句摘要、看不出根因
|
|
799
|
+
pushLine(options.task, '[pnpm log] the host-reported diagnostics log could not be read from disk (missing, empty, or unreadable)');
|
|
800
|
+
}
|
|
784
801
|
}
|
|
785
802
|
// 包在 registry 不存在(404)→ 官方源复查,区分「作者的依赖缺失/包被删」与「本机镜像源滞后」
|
|
786
803
|
result = await annotateRegistryMissing(result, options);
|