@1e0zj/dsh-plugin-mall 0.1.8 → 0.1.10
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 +123 -76
- package/package.json +1 -1
- package/src/client.js +42 -5
- package/src/index.js +26 -2
- package/src/installer.js +48 -2
package/README.md
CHANGED
|
@@ -1,76 +1,123 @@
|
|
|
1
|
-
# dsh-plugin-mall
|
|
2
|
-
|
|
3
|
-
**dsh
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
1
|
+
# dsh-plugin-mall
|
|
2
|
+
|
|
3
|
+
**An open plugin marketplace for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh): search every GitHub repo tagged `topic:dsh-plugin`, automatically verify which ones are real dsh plugins, install and update with one click.**
|
|
4
|
+
|
|
5
|
+
[中文说明](#中文说明) · [Install](#install) · [Why another marketplace](#why-another-marketplace)
|
|
6
|
+
|
|
7
|
+
Two surfaces: a **Settings → Plugins → Marketplace** tab in the dsh web UI, and five agent tools usable from any session.
|
|
8
|
+
|
|
9
|
+
## Why another marketplace?
|
|
10
|
+
|
|
11
|
+
Curated lists only show what has been reviewed and merged. This marketplace is open by construction: **any repo tagged `topic:dsh-plugin` is discoverable the moment it is pushed** — no submission, no approval queue. To keep that openness usable:
|
|
12
|
+
|
|
13
|
+
- **Automatic verification** — every search result's `package.json` is fetched (jsDelivr/raw dual-source CDN, no API quota) and checked for the official `dsh.bundle` / `dsh.client` manifest. Verified plugins get a green badge; the default "verified only" view filters out ~73% of topic noise (empty repos and unrelated projects riding the tag).
|
|
14
|
+
- **Anti-squatting** — an install prefers the npm tarball only when the registry entry's `repository` URL points back to the same GitHub repo; anything else falls back to the explicit `github:` spec.
|
|
15
|
+
- **npm-first installs** — registry tarballs are smaller than whole-repo GitHub downloads and come with integrity checks.
|
|
16
|
+
- **Update management** — installed plugins are compared against the registry `latest`; one-click update per plugin.
|
|
17
|
+
- **Resilience** — rate-limit circuit breaker, GitHub's 1000-result search window handled gracefully, `corepack enable pnpm` self-heal when pnpm is missing, one-click dsh restart (loopback-only, `allowRestart: false` to disable).
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
# from npm
|
|
23
|
+
dsh plugin --profile web add @1e0zj/dsh-plugin-mall
|
|
24
|
+
|
|
25
|
+
# from GitHub
|
|
26
|
+
dsh plugin --profile web add github:1e0zj/dsh-plugin-mall
|
|
27
|
+
|
|
28
|
+
# local development (symlink; restart dsh after edits)
|
|
29
|
+
dsh plugin --profile web add link:C:\path\to\dsh-plugin-mall
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Restart dsh after installing.
|
|
33
|
+
|
|
34
|
+
## Agent tools
|
|
35
|
+
|
|
36
|
+
| Tool | What it does |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `market_search` | Search GitHub repos tagged `topic:dsh-plugin` (star-ranked, keyword filter, server-side `stars:>=1` noise floor) |
|
|
39
|
+
| `market_info` | Inspect one repo: stars, license, package.json, whether it declares `dsh.bundle.patch` / `dsh.client` |
|
|
40
|
+
| `market_install` | Install a plugin into a profile as a background job (npm-first spec resolution) |
|
|
41
|
+
| `market_uninstall` | Remove a plugin: `pnpm remove` + bundle-layer reconcile + client-row cleanup |
|
|
42
|
+
| `market_installed` | List a profile's installed plugins and their bundle status |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# 中文说明
|
|
47
|
+
|
|
48
|
+
**dsh 插件市场** — 搜索 GitHub `dsh-plugin` 话题下的 DeepSeek Harness 插件仓库,自动验证哪些是真 dsh 插件,一键安装与更新。
|
|
49
|
+
|
|
50
|
+
与策展列表不同:**任何打上 `topic:dsh-plugin` 的仓库推送后立即可被发现**——无需投稿、无需审批。为保证开放性可用,做了这些事:
|
|
51
|
+
|
|
52
|
+
- **自动验证**:逐仓库拉取 `package.json`(jsDelivr/raw 双源 CDN,不占 API 配额),按官方 `dsh.bundle` / `dsh.client` 声明打徽章;默认"只看已验证"视图过滤约 73% 的话题噪音
|
|
53
|
+
- **防抢注**:仅当 npm registry 条目的 `repository` 指回同一 GitHub 仓库时才用 npm 安装,否则回退 `github:` 源
|
|
54
|
+
- **npm 优先安装**:registry tarball 比整仓库下载更小且带完整性校验
|
|
55
|
+
- **更新管理**:已装插件与 registry `latest` 比对,逐个一键更新
|
|
56
|
+
- **工程韧性**:限流熔断、GitHub 1000 条搜索上限优雅处理、pnpm 缺失时 `corepack` 自愈、一键重启 dsh(仅 loopback,可 `allowRestart: false` 关闭)
|
|
57
|
+
|
|
58
|
+
## 安装
|
|
59
|
+
|
|
60
|
+
```powershell
|
|
61
|
+
# 从 npm
|
|
62
|
+
dsh plugin --profile web add @1e0zj/dsh-plugin-mall
|
|
63
|
+
|
|
64
|
+
# 从 GitHub
|
|
65
|
+
dsh plugin --profile web add github:1e0zj/dsh-plugin-mall
|
|
66
|
+
|
|
67
|
+
# 本地开发(软链,改代码后重启 dsh 即生效)
|
|
68
|
+
dsh plugin --profile web add link:C:\path\to\dsh-plugin-mall
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
装完**重启 dsh**(`dsh web` 进程)后生效。
|
|
72
|
+
|
|
73
|
+
> Windows 下用 `link:` 开发本插件时,Node 会从项目的真实路径加载模块,
|
|
74
|
+
> 因此项目目录里必须先装一次依赖(`npm install`),裸导入才能解析;
|
|
75
|
+
> 通过 npm/GitHub 安装时无此要求(pnpm 会把真实拷贝装进 profile 的 node_modules)。
|
|
76
|
+
|
|
77
|
+
## 工作原理
|
|
78
|
+
|
|
79
|
+
- 双面包(dual-face)插件:`dsh.bundle` 半边挂在 **host 平面**(profile bundle 层),
|
|
80
|
+
注册 5 个 agent 工具(进 global 层,所有会话可见,与 MCP 工具同理);
|
|
81
|
+
`dsh.client` 半边是浏览器插件,往设置页插件区注册「插件市场」tab
|
|
82
|
+
(`settings.plugins.tab` slot;手写无构建,经 `window.__ModuleLoader__` 加载)。
|
|
83
|
+
- 浏览器 → 服务端走 Connection 服务的独立 RPC 通道 `/market`
|
|
84
|
+
(loopback-only,与 `/api` 通道互不干扰);页面发起的安装任务用进程内
|
|
85
|
+
tracker 跟踪 —— web host 层没有 job 控制器,`ctx.jobs` 无法在会话外起任务。
|
|
86
|
+
- `market_install` 复刻官方 `dsh plugin add` 的流程:在 profile 目录跑
|
|
87
|
+
`pnpm add <spec>`,成功后把声明了 `dsh.bundle.patch` 的依赖登记进
|
|
88
|
+
`dsh.profile.bundles`(layer 列表),声明了 `dsh.client` 的依赖自动在
|
|
89
|
+
profile 的 `cordis.patch.yml` 注册加载行,与官方 reconcile 逻辑一致。
|
|
90
|
+
- `market_uninstall` 复刻官方 `dsh plugin remove` 的流程:在 profile 目录跑
|
|
91
|
+
`pnpm remove <package>`,成功后从 `dsh.profile.bundles` 剔除该依赖的
|
|
92
|
+
bundle 条目,并删掉 `cordis.patch.yml` 里由安装流程注册的客户端加载行
|
|
93
|
+
(文本级精准移除,用户手写的行不受影响)。
|
|
94
|
+
- 安装源解析:`github:owner/repo` 优先改写为同名 npm 包(仅当 registry
|
|
95
|
+
条目的 repository 指回该仓库,防止抢注),否则用 GitHub 全仓库 spec。
|
|
96
|
+
- GitHub 源的插件安装时要跑 prepare 构建脚本,pnpm 默认拦截;本插件检测到
|
|
97
|
+
拦截后会把包名自动合并进 profile 的 `pnpm-workspace.yaml` 的 `allowBuilds`
|
|
98
|
+
并自动重试一次(解析只认合法 npm 包名,写入一律加引号,不会写坏 YAML)。
|
|
99
|
+
- 配置(`cordis.patch.yml` 中可改):`defaultProfile`(默认装进哪个 profile,
|
|
100
|
+
默认 `web`)、`apiBase`(GitHub API 地址)、`perPageMax`(搜索单页上限)、
|
|
101
|
+
`allowRestart`(是否允许一键重启,默认 `true`)。
|
|
102
|
+
|
|
103
|
+
## 发布
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm publish --access public
|
|
107
|
+
# 或在 GitHub 建仓库并打上 topic:dsh-plugin
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 开发说明
|
|
111
|
+
|
|
112
|
+
- 插件形态:`package.json` 里 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,
|
|
113
|
+
patch 文件把 `dsh-plugin-mall` 这一行 insert 进装配树;**同一行同时是
|
|
114
|
+
client 插件行**(`dsh.client` 声明让 client-modules 扫描并服务
|
|
115
|
+
`/plugins/<id>/client.js`)。
|
|
116
|
+
- **`@deepseek-ai/*` 框架包必须声明为 `peerDependencies`**:装成 dependencies
|
|
117
|
+
会把宿主模块副本 hoist 进 profile,cordis loader 双副本加载、Symbol 身份
|
|
118
|
+
分裂,宿主的工具调度全线崩溃。宿主经 `profiles/node_modules` fallback
|
|
119
|
+
提供框架包。
|
|
120
|
+
- node 半边导出具名成员 `{ name, inject, Config, apply }`,**不要** `export default`
|
|
121
|
+
(cordis loader 会做 `exports.default ?? exports` 解包,default 会吞掉 inject/Config);
|
|
122
|
+
client 半边是 `window.__ModuleLoader__.load({id, factory})`,导出 `{ apply, inject }`。
|
|
123
|
+
- 单测 `src/github.js`(无 harness 依赖):`node src/github.js --self-test`。
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -147,8 +147,8 @@ window.__ModuleLoader__.load({
|
|
|
147
147
|
var item = props.item;
|
|
148
148
|
var installing = props.installing === true;
|
|
149
149
|
var job = props.installJob;
|
|
150
|
-
var installLabel = "安装";
|
|
151
|
-
var installDisabled = installing;
|
|
150
|
+
var installLabel = props.alreadyInstalled === true ? "已装" : "安装";
|
|
151
|
+
var installDisabled = installing || props.alreadyInstalled === true;
|
|
152
152
|
if (!installing && job) {
|
|
153
153
|
if (job.status === "running") { installLabel = "安装中…"; installDisabled = true; }
|
|
154
154
|
else if (job.status === "completed") { installLabel = "已装 · 重启生效"; installDisabled = true; }
|
|
@@ -161,7 +161,7 @@ window.__ModuleLoader__.load({
|
|
|
161
161
|
verifyBadge(props.verified),
|
|
162
162
|
item.archived ? h("span", { className: "mkt_badge" }, "archived") : null
|
|
163
163
|
),
|
|
164
|
-
item.description ? h("div", { className: "mkt_desc" },
|
|
164
|
+
item.description ? h("div", { className: "mkt_desc" }, item.description) : null,
|
|
165
165
|
h("div", { className: "mkt_metaRow" },
|
|
166
166
|
h("span", { className: "mkt_meta" }, "★" + item.stars),
|
|
167
167
|
item.language ? h("span", { className: "mkt_meta" }, item.language) : null,
|
|
@@ -293,10 +293,17 @@ window.__ModuleLoader__.load({
|
|
|
293
293
|
var _retryAt = useState(0);
|
|
294
294
|
var retryAt = _retryAt[0];
|
|
295
295
|
var setRetryAt = _retryAt[1];
|
|
296
|
+
var _restarting = useState(false);
|
|
297
|
+
var restarting = _restarting[0];
|
|
298
|
+
var setRestarting = _restarting[1];
|
|
296
299
|
|
|
297
300
|
var call = useCallback(function (endpoint, payload) {
|
|
298
301
|
return rpc.call("/market", endpoint, payload || {}).then(function (res) {
|
|
299
|
-
if (!res || res.ok !== true)
|
|
302
|
+
if (!res || res.ok !== true) {
|
|
303
|
+
var err = res && res.error;
|
|
304
|
+
var msg = err && typeof err === "object" ? (err.message || JSON.stringify(err)) : (err || "market request failed");
|
|
305
|
+
throw new Error(msg);
|
|
306
|
+
}
|
|
300
307
|
return res.value;
|
|
301
308
|
});
|
|
302
309
|
}, [rpc]);
|
|
@@ -418,6 +425,30 @@ window.__ModuleLoader__.load({
|
|
|
418
425
|
doInstallSpec("github:" + repo);
|
|
419
426
|
}, [doInstallSpec]);
|
|
420
427
|
|
|
428
|
+
// 一键重启:loopback-only 的 /market restart 端点会 detached 拉起新的
|
|
429
|
+
// dsh 进程后退出当前进程;这里轮询 host 恢复后自动刷新页面。
|
|
430
|
+
var doRestart = useCallback(function () {
|
|
431
|
+
if (typeof window !== "undefined" && typeof window.confirm === "function") {
|
|
432
|
+
if (window.confirm("重启 dsh?正在进行的任务会中断。") !== true) return;
|
|
433
|
+
}
|
|
434
|
+
setRestarting(true);
|
|
435
|
+
setError(null);
|
|
436
|
+
call("restart", {}).then(function () {
|
|
437
|
+
var tries = 0;
|
|
438
|
+
var ping = setInterval(function () {
|
|
439
|
+
tries++;
|
|
440
|
+
if (tries > 40) { clearInterval(ping); return; }
|
|
441
|
+
rpc.call("/market", "installed", {}).then(function () {
|
|
442
|
+
clearInterval(ping);
|
|
443
|
+
window.location.reload();
|
|
444
|
+
}).catch(function () { /* host 还在重启,继续等 */ });
|
|
445
|
+
}, 3000);
|
|
446
|
+
}).catch(function (e) {
|
|
447
|
+
setRestarting(false);
|
|
448
|
+
setError(errorText(e));
|
|
449
|
+
});
|
|
450
|
+
}, [call, rpc]);
|
|
451
|
+
|
|
421
452
|
var doUninstall = useCallback(function (name) {
|
|
422
453
|
setRemoving(function (prev) {
|
|
423
454
|
var next = Object.assign({}, prev, { [name]: true });
|
|
@@ -478,6 +509,7 @@ window.__ModuleLoader__.load({
|
|
|
478
509
|
),
|
|
479
510
|
h("button", { className: "mkt_btn mkt_btnPrimary", disabled: loading, onClick: doSearch }, loading ? "搜索中…" : "搜索"),
|
|
480
511
|
h("button", { className: "mkt_btn", onClick: refreshInstalled }, "刷新已装"),
|
|
512
|
+
h("button", { className: "mkt_btn mkt_btnDanger", disabled: restarting, onClick: doRestart }, restarting ? "重启中…" : "重启 dsh"),
|
|
481
513
|
h("label", { className: "mkt_check" },
|
|
482
514
|
h("input", { type: "checkbox", checked: verifiedOnly, onChange: function (e) { setVerifiedOnly(e.target.checked); } }),
|
|
483
515
|
"只看已验证插件")
|
|
@@ -502,12 +534,17 @@ window.__ModuleLoader__.load({
|
|
|
502
534
|
for (var jobId in jobs) {
|
|
503
535
|
if (jobs[jobId] && jobs[jobId].spec === "github:" + item.fullName) { installJob = jobs[jobId]; break; }
|
|
504
536
|
}
|
|
537
|
+
var vInfo = verified[item.fullName];
|
|
538
|
+
var alreadyInstalled = vInfo !== undefined && typeof vInfo.name === "string"
|
|
539
|
+
&& installed !== null && installed.error === undefined
|
|
540
|
+
&& (installed.deps || []).some(function (dep) { return dep.name === vInfo.name; });
|
|
505
541
|
return h(RepoCard, {
|
|
506
542
|
key: item.fullName,
|
|
507
543
|
item: item,
|
|
508
544
|
installing: installing[item.fullName] === true || installing["github:" + item.fullName] === true,
|
|
509
545
|
installJob: installJob,
|
|
510
|
-
verified:
|
|
546
|
+
verified: vInfo,
|
|
547
|
+
alreadyInstalled: alreadyInstalled,
|
|
511
548
|
onInstall: doInstall,
|
|
512
549
|
});
|
|
513
550
|
}),
|
package/src/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import z from "@deepseek-ai/schemastery";
|
|
|
17
17
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
18
18
|
import { existsSync, readFileSync } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
|
+
import { spawn } from "node:child_process";
|
|
20
21
|
import { resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
|
|
21
22
|
import { repoInfo, searchPlugins, verifyPlugins, preferNpmSpec, npmPackageInfo, compareVersions } from "./github.js";
|
|
22
23
|
import { ensureProfile, listInstalled, normalizeSpec, runInstall, runRemove, createJobTracker } from "./installer.js";
|
|
@@ -28,6 +29,7 @@ export const Config = z.object({
|
|
|
28
29
|
defaultProfile: z.string().default("web"),
|
|
29
30
|
apiBase: z.string().default("https://api.github.com"),
|
|
30
31
|
perPageMax: z.number().default(30),
|
|
32
|
+
allowRestart: z.boolean().default(true),
|
|
31
33
|
});
|
|
32
34
|
|
|
33
35
|
/** Clip long strings for compact model-facing output. */
|
|
@@ -128,7 +130,10 @@ function rpcOk(value) {
|
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
function rpcFail(error) {
|
|
131
|
-
|
|
133
|
+
// dsh connection RPC 的响应信封校验(dsh-client-connection rpcResultSchema)
|
|
134
|
+
// 要求 error 为 discriminated object:{code, message, details}。code 取
|
|
135
|
+
// 通用 "internal",否则整条错误会被 zod 以 invalid_union 吞掉。
|
|
136
|
+
return { ok: false, error: { code: "internal", message: error?.message ?? String(error), details: {} } };
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
/**
|
|
@@ -145,7 +150,7 @@ function rpcFail(error) {
|
|
|
145
150
|
* @returns the {ok, value|error} envelope.
|
|
146
151
|
*/
|
|
147
152
|
async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
|
|
148
|
-
const { defaultProfile = "web", apiBase = "https://api.github.com", perPageMax = 30 } = config;
|
|
153
|
+
const { defaultProfile = "web", apiBase = "https://api.github.com", perPageMax = 30, allowRestart = true } = config;
|
|
149
154
|
switch (endpoint) {
|
|
150
155
|
case "search": {
|
|
151
156
|
const perPage = Math.min(Math.max(Math.trunc(payload?.perPage ?? 10) || 10, 1), Math.trunc(perPageMax) || 30);
|
|
@@ -246,6 +251,25 @@ async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
|
|
|
246
251
|
return rpcFail(error);
|
|
247
252
|
}
|
|
248
253
|
}
|
|
254
|
+
case "restart": {
|
|
255
|
+
// 一键重启:detached 拉起新 dsh 进程(用当前进程的 argv 重建启动命令)
|
|
256
|
+
// 后退出自己。仅 loopback 直连可调(channel 级 authority 已限制);
|
|
257
|
+
// allowRestart:false 时禁用(进程由 systemd/pm2 等托管时接管重启)。
|
|
258
|
+
if (allowRestart !== true) return rpcFail(new Error("restart disabled by config (allowRestart: false)"));
|
|
259
|
+
const script = process.argv[1];
|
|
260
|
+
const scriptArgs = process.argv.slice(2);
|
|
261
|
+
if (typeof script !== "string" || script.length === 0 || !existsSync(script)) {
|
|
262
|
+
return rpcFail(new Error("cannot determine the dsh launch command for an automatic restart — please restart manually"));
|
|
263
|
+
}
|
|
264
|
+
const relaunch = `"${process.execPath}" "${script}"${scriptArgs.length > 0 ? ` ${scriptArgs.map((arg) => `"${arg}"`).join(" ")}` : ""}`;
|
|
265
|
+
const launcher = process.platform === "win32"
|
|
266
|
+
? `timeout /t 2 /nobreak >nul & ${relaunch}`
|
|
267
|
+
: `sleep 2 && ${relaunch}`;
|
|
268
|
+
const child = spawn(launcher, { shell: true, detached: true, stdio: "ignore", cwd: process.cwd(), windowsHide: true });
|
|
269
|
+
child.unref();
|
|
270
|
+
setTimeout(() => process.exit(0), 800);
|
|
271
|
+
return rpcOk({ restarting: true });
|
|
272
|
+
}
|
|
249
273
|
case "jobCancel": {
|
|
250
274
|
try {
|
|
251
275
|
return rpcOk({ result: tracker.cancel(payload?.jobId) });
|
package/src/installer.js
CHANGED
|
@@ -361,6 +361,35 @@ export function createJobTracker() {
|
|
|
361
361
|
};
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
// ── pnpm self-heal (corepack) ───────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Try to provision pnpm once via `corepack enable pnpm` (corepack ships with
|
|
368
|
+
* Node). Output lands in the caller's job log; returns whether a retry of the
|
|
369
|
+
* pnpm spawn is worth attempting.
|
|
370
|
+
*/
|
|
371
|
+
async function enablePnpmViaCorepack(push) {
|
|
372
|
+
push("\n[dsh-plugin-mall] pnpm not found on PATH — trying `corepack enable pnpm` once\n");
|
|
373
|
+
return await new Promise((resolve) => {
|
|
374
|
+
let proc;
|
|
375
|
+
try {
|
|
376
|
+
proc = spawn("corepack", ["enable", "pnpm"], {
|
|
377
|
+
env: process.env,
|
|
378
|
+
shell: process.platform === "win32",
|
|
379
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
380
|
+
windowsHide: true,
|
|
381
|
+
});
|
|
382
|
+
} catch {
|
|
383
|
+
resolve(false);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
proc.on("error", () => resolve(false));
|
|
387
|
+
proc.stdout?.on("data", (data) => push(data.toString()));
|
|
388
|
+
proc.stderr?.on("data", (data) => push(data.toString()));
|
|
389
|
+
proc.on("close", (code) => resolve(code === 0));
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
364
393
|
// ── the background install job ──────────────────────────────────────────────
|
|
365
394
|
|
|
366
395
|
/**
|
|
@@ -381,6 +410,7 @@ export function runInstall({ profile, spec }) {
|
|
|
381
410
|
deltaQueue.push(text);
|
|
382
411
|
};
|
|
383
412
|
let current = undefined;
|
|
413
|
+
let pnpmSelfHealed = false;
|
|
384
414
|
|
|
385
415
|
const spawnAdd = () => {
|
|
386
416
|
const proc = spawn("pnpm", ["add", spec, "--reporter=append-only"], {
|
|
@@ -427,6 +457,17 @@ export function runInstall({ profile, spec }) {
|
|
|
427
457
|
|
|
428
458
|
const settle = async (outcome) => {
|
|
429
459
|
if (outcome.spawnError !== undefined) {
|
|
460
|
+
// pnpm 缺失时先尝试 corepack 自愈一次,成功则重跑安装。
|
|
461
|
+
if (outcome.spawnError.code === "ENOENT" && !pnpmSelfHealed) {
|
|
462
|
+
pnpmSelfHealed = true;
|
|
463
|
+
const healed = await enablePnpmViaCorepack(push);
|
|
464
|
+
if (healed) {
|
|
465
|
+
const retry = spawnAdd();
|
|
466
|
+
current = retry.proc;
|
|
467
|
+
return settle(await retry.done);
|
|
468
|
+
}
|
|
469
|
+
return { status: "failed", detail: "pnpm not found on PATH and `corepack enable pnpm` could not provision it — install pnpm (e.g. `npm i -g pnpm`) to manage profile plugins" };
|
|
470
|
+
}
|
|
430
471
|
const hint = outcome.spawnError.code === "ENOENT"
|
|
431
472
|
? "pnpm not found on PATH — install pnpm (e.g. `corepack enable pnpm`) to manage profile plugins"
|
|
432
473
|
: `could not start pnpm: ${outcome.spawnError.message}`;
|
|
@@ -490,7 +531,7 @@ function failedNow(detail) {
|
|
|
490
531
|
* `dsh.profile.bundles` (the removed dependency's bundle entry drops out) and
|
|
491
532
|
* deletes the client loader row `ensureClientRow` had registered for it.
|
|
492
533
|
*/
|
|
493
|
-
export function runRemove({ profile, packageName }) {
|
|
534
|
+
export function runRemove({ profile, packageName }, selfHealed = false) {
|
|
494
535
|
let profileDir;
|
|
495
536
|
try {
|
|
496
537
|
profileDir = resolveProfileDir(profile);
|
|
@@ -522,8 +563,13 @@ export function runRemove({ profile, packageName }) {
|
|
|
522
563
|
const done = new Promise((resolve) => {
|
|
523
564
|
proc.on("error", (error) => resolve({ spawnError: error }));
|
|
524
565
|
proc.on("close", (exitCode) => resolve({ exitCode, signal: proc.signalCode }));
|
|
525
|
-
}).then((outcome) => {
|
|
566
|
+
}).then(async (outcome) => {
|
|
526
567
|
if (outcome.spawnError !== undefined) {
|
|
568
|
+
// pnpm 缺失时先 corepack 自愈一次再重试(重试在新 producer 里跑)。
|
|
569
|
+
if (outcome.spawnError.code === "ENOENT" && selfHealed !== true) {
|
|
570
|
+
const healed = await enablePnpmViaCorepack(() => {});
|
|
571
|
+
if (healed) return runRemove({ profile, packageName }, true);
|
|
572
|
+
}
|
|
527
573
|
const hint = outcome.spawnError.code === "ENOENT"
|
|
528
574
|
? "pnpm not found on PATH — install pnpm (e.g. `corepack enable pnpm`) to manage profile plugins"
|
|
529
575
|
: `could not start pnpm: ${outcome.spawnError.message}`;
|