@1e0zj/dsh-plugin-mall 0.1.2 → 0.1.4
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 +8 -3
- package/package.json +1 -1
- package/src/client.js +45 -13
- package/src/index.js +76 -6
- package/src/installer.js +123 -6
package/README.md
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
两个入口:
|
|
6
6
|
|
|
7
|
-
1. **设置 → 插件 → 插件市场 tab**(浏览器 UI,dsh web
|
|
8
|
-
2. **会话内
|
|
7
|
+
1. **设置 → 插件 → 插件市场 tab**(浏览器 UI,dsh web 模式):搜索、看详情、点安装/卸载、看进度与已装列表
|
|
8
|
+
2. **会话内 5 个 agent 工具**(所有模式可用)
|
|
9
9
|
|
|
10
10
|
| 工具 | 作用 |
|
|
11
11
|
|---|---|
|
|
12
12
|
| `market_search` | 搜索 GitHub 上带 `topic:dsh-plugin` 标签的仓库(按 star 排序,可按关键词过滤) |
|
|
13
13
|
| `market_info` | 查看单个仓库详情:star、license、package.json、是否声明 `dsh.bundle.patch` / `dsh.client` |
|
|
14
14
|
| `market_install` | 把插件装进某个 profile(后台任务,可用 `job_output` 查看进度) |
|
|
15
|
+
| `market_uninstall` | 把插件从某个 profile 卸载(后台任务:`pnpm remove` + 剔除 bundle 层 + 删除客户端加载行) |
|
|
15
16
|
| `market_installed` | 列出某 profile 已装的插件及其 bundle 状态 |
|
|
16
17
|
|
|
17
18
|
## 安装
|
|
@@ -36,7 +37,7 @@ dsh plugin --profile web add link:C:\path\to\dsh-plugin-mall
|
|
|
36
37
|
## 工作原理
|
|
37
38
|
|
|
38
39
|
- 双面包(dual-face)插件:`dsh.bundle` 半边挂在 **host 平面**(profile bundle 层),
|
|
39
|
-
注册
|
|
40
|
+
注册 5 个 agent 工具(进 global 层,所有会话可见,与 MCP 工具同理);
|
|
40
41
|
`dsh.client` 半边是浏览器插件,往设置页插件区注册「插件市场」tab
|
|
41
42
|
(`settings.plugins.tab` slot;手写无构建,经 `window.__ModuleLoader__` 加载)。
|
|
42
43
|
- 浏览器 → 服务端走 Connection 服务的独立 RPC 通道 `/market`
|
|
@@ -46,6 +47,10 @@ dsh plugin --profile web add link:C:\path\to\dsh-plugin-mall
|
|
|
46
47
|
`pnpm add <spec>`,成功后把声明了 `dsh.bundle.patch` 的依赖登记进
|
|
47
48
|
`dsh.profile.bundles`(layer 列表),声明了 `dsh.client` 的依赖自动在
|
|
48
49
|
profile 的 `cordis.patch.yml` 注册加载行,与官方 reconcile 逻辑一致。
|
|
50
|
+
- `market_uninstall` 复刻官方 `dsh plugin remove` 的流程:在 profile 目录跑
|
|
51
|
+
`pnpm remove <package>`,成功后从 `dsh.profile.bundles` 剔除该依赖的
|
|
52
|
+
bundle 条目,并删掉 `cordis.patch.yml` 里由安装流程注册的客户端加载行
|
|
53
|
+
(文本级精准移除,用户手写的行不受影响)。
|
|
49
54
|
- GitHub 源的插件安装时要跑 prepare 构建脚本,pnpm 默认拦截;本插件检测到
|
|
50
55
|
拦截后会把包名自动合并进 profile 的 `pnpm-workspace.yaml` 的 `allowBuilds`
|
|
51
56
|
并自动重试一次。
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -15,7 +15,7 @@ window.__ModuleLoader__.load({
|
|
|
15
15
|
|
|
16
16
|
// ── styles ──────────────────────────────────────────────────────────────
|
|
17
17
|
var css = [
|
|
18
|
-
".mkt_root{display:flex;flex-direction:column;gap:12px;max-width:
|
|
18
|
+
".mkt_root{display:flex;flex-direction:column;gap:12px;max-width:1040px}",
|
|
19
19
|
".mkt_head{display:flex;flex-direction:column;gap:2px}",
|
|
20
20
|
".mkt_title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
21
21
|
".mkt_sub{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
|
|
@@ -28,16 +28,19 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
".mkt_btnPrimary{background:var(--dsw-alias-state-business-primary,#2b6cb0);border-color:transparent;color:#fff}",
|
|
29
29
|
".mkt_btnPrimary:hover:not(:disabled){color:#fff;opacity:.92}",
|
|
30
30
|
".mkt_btnDanger:hover:not(:disabled){border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}",
|
|
31
|
+
".mkt_btnSm{padding:3px 10px;font-size:12px}",
|
|
31
32
|
".mkt_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}",
|
|
32
33
|
".mkt_columns{display:grid;grid-template-columns:1fr 320px;gap:14px;align-items:start}",
|
|
33
34
|
"@media (max-width:860px){.mkt_columns{grid-template-columns:1fr}}",
|
|
34
|
-
".mkt_list{display:
|
|
35
|
+
".mkt_list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;align-items:stretch;min-width:0}",
|
|
36
|
+
"@media (max-width:820px){.mkt_list{grid-template-columns:minmax(0,1fr)}}",
|
|
37
|
+
".mkt_listHead{grid-column:1/-1;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
|
|
35
38
|
".mkt_card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px;background:var(--dsw-alias-bg-primary,#fff);display:flex;flex-direction:column;gap:6px;min-width:0}",
|
|
36
39
|
".mkt_cardHead{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}",
|
|
37
40
|
".mkt_name{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}",
|
|
38
41
|
".mkt_meta{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
|
|
39
42
|
".mkt_desc{font-size:12.5px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
|
|
40
|
-
".mkt_cardActions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}",
|
|
43
|
+
".mkt_cardActions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:auto;padding-top:4px}",
|
|
41
44
|
".mkt_rail{display:flex;flex-direction:column;gap:10px;min-width:0}",
|
|
42
45
|
".mkt_panelTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}",
|
|
43
46
|
".mkt_pre{font-family:Consolas,Monaco,monospace;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-secondary,#f6f7f8);border-radius:6px;padding:8px;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-all}",
|
|
@@ -46,7 +49,8 @@ window.__ModuleLoader__.load({
|
|
|
46
49
|
".mkt_link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;cursor:pointer}",
|
|
47
50
|
".mkt_installedHead{display:flex;align-items:center;gap:8px;width:100%;background:none;border:0;padding:0;margin:0;cursor:pointer;font:inherit;text-align:left}",
|
|
48
51
|
".mkt_installedHead:hover .mkt_panelTitle{color:var(--dsw-alias-state-business-primary)}",
|
|
49
|
-
".mkt_depList{display:
|
|
52
|
+
".mkt_depList{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 14px;margin-top:8px}",
|
|
53
|
+
"@media (max-width:820px){.mkt_depList{grid-template-columns:minmax(0,1fr)}}",
|
|
50
54
|
".mkt_depRow{display:flex;justify-content:space-between;align-items:center;gap:8px}",
|
|
51
55
|
].join("\n");
|
|
52
56
|
var tagId = "@1e0zj/dsh-plugin-mall/market-tab.css";
|
|
@@ -180,7 +184,7 @@ window.__ModuleLoader__.load({
|
|
|
180
184
|
var ids = Object.keys(props.jobs);
|
|
181
185
|
if (ids.length === 0) return null;
|
|
182
186
|
return h("div", { className: "mkt_card" },
|
|
183
|
-
h("p", { className: "mkt_panelTitle" }, "
|
|
187
|
+
h("p", { className: "mkt_panelTitle" }, "任务"),
|
|
184
188
|
ids.map(function (id) {
|
|
185
189
|
var job = props.jobs[id];
|
|
186
190
|
var done = job.status === "completed" || job.status === "failed" || job.status === "killed";
|
|
@@ -189,9 +193,9 @@ window.__ModuleLoader__.load({
|
|
|
189
193
|
id + " · " + clip(job.spec || "", 40) + " · " + (job.status || "running")),
|
|
190
194
|
job.detail ? h("div", { className: "mkt_desc" }, job.detail) : null,
|
|
191
195
|
done && job.status === "completed"
|
|
192
|
-
? h("div", { className: "mkt_ok" }, "✓
|
|
196
|
+
? h("div", { className: "mkt_ok" }, "✓ 完成 — 重启 dsh 后生效")
|
|
193
197
|
: job.status === "failed"
|
|
194
|
-
? h("div", { className: "mkt_error" }, "
|
|
198
|
+
? h("div", { className: "mkt_error" }, "失败,见下方输出")
|
|
195
199
|
: null,
|
|
196
200
|
job.output ? h("pre", { className: "mkt_pre" }, job.output.slice(-4000)) : null
|
|
197
201
|
);
|
|
@@ -224,9 +228,15 @@ window.__ModuleLoader__.load({
|
|
|
224
228
|
: (installed.deps || []).length === 0
|
|
225
229
|
? h("div", { className: "mkt_meta" }, "还没有装过插件")
|
|
226
230
|
: h("div", { className: "mkt_depList" }, (installed.deps || []).map(function (dep) {
|
|
231
|
+
var busy = (props.removing || {})[dep.name] === true;
|
|
227
232
|
return h("div", { key: dep.name, className: "mkt_depRow" },
|
|
228
233
|
h("span", { className: "mkt_desc" }, dep.name + "@" + dep.version),
|
|
229
|
-
h("span", { className: "mkt_badge" }, kindLabel(dep.kind))
|
|
234
|
+
h("span", { className: "mkt_badge" }, kindLabel(dep.kind)),
|
|
235
|
+
h("button", {
|
|
236
|
+
className: "mkt_btn mkt_btnDanger mkt_btnSm",
|
|
237
|
+
disabled: busy,
|
|
238
|
+
onClick: function () { props.onUninstall(dep.name); },
|
|
239
|
+
}, busy ? "卸载中…" : "卸载"));
|
|
230
240
|
}))
|
|
231
241
|
);
|
|
232
242
|
}
|
|
@@ -258,6 +268,9 @@ window.__ModuleLoader__.load({
|
|
|
258
268
|
var _installing = useState({});
|
|
259
269
|
var installing = _installing[0];
|
|
260
270
|
var setInstalling = _installing[1];
|
|
271
|
+
var _removing = useState({});
|
|
272
|
+
var removing = _removing[0];
|
|
273
|
+
var setRemoving = _removing[1];
|
|
261
274
|
|
|
262
275
|
var call = useCallback(function (endpoint, payload) {
|
|
263
276
|
return rpc.call("/market", endpoint, payload || {}).then(function (res) {
|
|
@@ -325,6 +338,25 @@ window.__ModuleLoader__.load({
|
|
|
325
338
|
});
|
|
326
339
|
}, [call, track]);
|
|
327
340
|
|
|
341
|
+
var doUninstall = useCallback(function (name) {
|
|
342
|
+
setRemoving(function (prev) {
|
|
343
|
+
var next = Object.assign({}, prev, { [name]: true });
|
|
344
|
+
return next;
|
|
345
|
+
});
|
|
346
|
+
setError(null);
|
|
347
|
+
call("uninstall", { package: name }).then(function (value) {
|
|
348
|
+
track(value.jobId, name);
|
|
349
|
+
}).catch(function (e) {
|
|
350
|
+
setError(errorText(e));
|
|
351
|
+
}).finally(function () {
|
|
352
|
+
setRemoving(function (prev) {
|
|
353
|
+
var next = Object.assign({}, prev);
|
|
354
|
+
delete next[name];
|
|
355
|
+
return next;
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
}, [call, track]);
|
|
359
|
+
|
|
328
360
|
useEffect(function () {
|
|
329
361
|
refreshInstalled();
|
|
330
362
|
}, [refreshInstalled]);
|
|
@@ -332,7 +364,7 @@ window.__ModuleLoader__.load({
|
|
|
332
364
|
return h("div", { className: "mkt_root" },
|
|
333
365
|
h("div", { className: "mkt_head" },
|
|
334
366
|
h("div", { className: "mkt_title" }, "插件市场"),
|
|
335
|
-
h("div", { className: "mkt_sub" }, "搜索 GitHub dsh-plugin 话题插件 ·
|
|
367
|
+
h("div", { className: "mkt_sub" }, "搜索 GitHub dsh-plugin 话题插件 · 装/卸后需重启 dsh 生效")
|
|
336
368
|
),
|
|
337
369
|
h("div", { className: "mkt_row" },
|
|
338
370
|
h("input", {
|
|
@@ -355,14 +387,15 @@ window.__ModuleLoader__.load({
|
|
|
355
387
|
h("button", { className: "mkt_btn", onClick: refreshInstalled }, "刷新已装")
|
|
356
388
|
),
|
|
357
389
|
error ? h("div", { className: "mkt_error" }, error) : null,
|
|
390
|
+
h(InstalledPanel, { installed: installed, removing: removing, onUninstall: doUninstall }),
|
|
358
391
|
h("div", { className: "mkt_columns" },
|
|
359
392
|
h("div", { className: "mkt_list" },
|
|
360
393
|
results == null
|
|
361
|
-
? h("div", { className: "mkt_meta" }, loading ? "正在加载最热插件…" : "—")
|
|
394
|
+
? h("div", { className: "mkt_meta mkt_listHead" }, loading ? "正在加载最热插件…" : "—")
|
|
362
395
|
: results.items.length === 0
|
|
363
|
-
? h("div", { className: "mkt_meta" }, "没有匹配的仓库。")
|
|
396
|
+
? h("div", { className: "mkt_meta mkt_listHead" }, "没有匹配的仓库。")
|
|
364
397
|
: h(React.Fragment, null,
|
|
365
|
-
h("div", { className: "mkt_meta" }, "共 " + results.total + " 个仓库(显示 " + results.items.length + " 个)"),
|
|
398
|
+
h("div", { className: "mkt_meta mkt_listHead" }, "共 " + results.total + " 个仓库(显示 " + results.items.length + " 个)"),
|
|
366
399
|
results.items.map(function (item) {
|
|
367
400
|
return h(RepoCard, {
|
|
368
401
|
key: item.fullName,
|
|
@@ -375,7 +408,6 @@ window.__ModuleLoader__.load({
|
|
|
375
408
|
)
|
|
376
409
|
),
|
|
377
410
|
h("div", { className: "mkt_rail" },
|
|
378
|
-
h(InstalledPanel, { installed: installed }),
|
|
379
411
|
h(InfoPanel, { info: info, onInstall: doInstall }),
|
|
380
412
|
h(JobsPanel, { jobs: jobs })
|
|
381
413
|
)
|
package/src/index.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// A Cordis plugin mounted at the host plane (profile bundle layer), so its
|
|
4
4
|
// tools land in the tools registry's global layer and every session sees
|
|
5
|
-
// them. It exposes
|
|
5
|
+
// them. It exposes five tools:
|
|
6
6
|
// market_search search GitHub repositories tagged topic:dsh-plugin
|
|
7
7
|
// market_info inspect one repository (stars, license, package.json, dsh.bundle)
|
|
8
8
|
// market_install install a plugin into a local dsh profile (background job)
|
|
9
|
+
// market_uninstall remove a plugin from a local dsh profile (background job)
|
|
9
10
|
// market_installed list a profile's installed plugins
|
|
10
11
|
//
|
|
11
12
|
// Plugin contract (see @deepseek-ai/cordis-plugin-loader): the loader imports
|
|
@@ -18,7 +19,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
18
19
|
import { join } from "node:path";
|
|
19
20
|
import { resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
|
|
20
21
|
import { repoInfo, searchPlugins } from "./github.js";
|
|
21
|
-
import { ensureProfile, listInstalled, normalizeSpec, runInstall,
|
|
22
|
+
import { ensureProfile, listInstalled, normalizeSpec, runInstall, runRemove, createJobTracker } from "./installer.js";
|
|
22
23
|
|
|
23
24
|
export const name = "@1e0zj/dsh-plugin-mall";
|
|
24
25
|
export const inject = ["tools", "jobs", "systemPrompt"];
|
|
@@ -104,7 +105,7 @@ function renderInstalled(result, profile) {
|
|
|
104
105
|
lines.push(` ${dep.name}@${dep.version} ${markers[dep.kind] ?? markers.plain}`);
|
|
105
106
|
}
|
|
106
107
|
lines.push("");
|
|
107
|
-
lines.push(`Remove with: dsh plugin --profile ${profile} remove <name
|
|
108
|
+
lines.push(`Remove with: market_uninstall (package: "<name>"), or dsh plugin --profile ${profile} remove <name>; then restart dsh.`);
|
|
108
109
|
return lines.join("\n");
|
|
109
110
|
}
|
|
110
111
|
|
|
@@ -136,7 +137,7 @@ function rpcFail(error) {
|
|
|
136
137
|
* tools keep using ctx.jobs; the browser surface uses `tracker` because the
|
|
137
138
|
* web host plane has no job controller for ctx.jobs to serve.
|
|
138
139
|
* @param ctx - plugin context.
|
|
139
|
-
* @param endpoint - "search" | "info" | "installed" | "install" | "job" | "jobCancel".
|
|
140
|
+
* @param endpoint - "search" | "info" | "installed" | "install" | "uninstall" | "job" | "jobCancel".
|
|
140
141
|
* @param payload - endpoint arguments from the browser.
|
|
141
142
|
* @param config - the row config (defaultProfile, apiBase, perPageMax).
|
|
142
143
|
* @param token - GitHub token from the environment.
|
|
@@ -192,6 +193,25 @@ async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
|
|
|
192
193
|
return rpcFail(error);
|
|
193
194
|
}
|
|
194
195
|
}
|
|
196
|
+
case "uninstall": {
|
|
197
|
+
const profile = String(payload?.profile ?? defaultProfile).trim();
|
|
198
|
+
const packageName = String(payload?.package ?? "").trim();
|
|
199
|
+
if (packageName.length === 0) return rpcFail(new Error("uninstall: package name is required"));
|
|
200
|
+
try {
|
|
201
|
+
const profileDir = resolveProfileDir(profile);
|
|
202
|
+
if (!existsSync(join(profileDir, "package.json"))) {
|
|
203
|
+
return rpcFail(new Error(`profile "${profile}" has no package.json — nothing installed to remove`));
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return rpcFail(new Error(`invalid profile: ${error.message}`));
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const jobId = tracker.start({ profile, spec: packageName, verb: "remove" });
|
|
210
|
+
return rpcOk({ jobId, profile, package: packageName });
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return rpcFail(error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
195
215
|
case "job": {
|
|
196
216
|
try {
|
|
197
217
|
return rpcOk(tracker.get(payload?.jobId));
|
|
@@ -221,7 +241,7 @@ async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
|
|
|
221
241
|
* @param token - GitHub token from the environment.
|
|
222
242
|
*/
|
|
223
243
|
function registerRpcChannel(ctx, config, token) {
|
|
224
|
-
const tracker =
|
|
244
|
+
const tracker = createJobTracker();
|
|
225
245
|
ctx.inject(["connection"], (connectionCtx) => {
|
|
226
246
|
connectionCtx.connection.rpc.handle("/market", (endpoint, payload, signal) => rpcDispatch(ctx, endpoint, payload ?? {}, config, token, tracker), { authority: "loopback" });
|
|
227
247
|
});
|
|
@@ -234,7 +254,7 @@ export function apply(ctx, config = {}) {
|
|
|
234
254
|
ctx.systemPrompt.section({
|
|
235
255
|
name: "tool:market",
|
|
236
256
|
order: 120,
|
|
237
|
-
text: "The dsh plugin marketplace tools are available: market_search discovers plugins on the GitHub dsh-plugin topic, market_info inspects one repository, market_install installs a plugin into a dsh profile as a background job (poll with job_output), and market_installed lists a profile's plugins. A successful market_install only takes effect after the dsh process restarts — remind the user to restart. Prefer plugins with meaningful stars and a dsh.bundle declaration (market_info shows both).",
|
|
257
|
+
text: "The dsh plugin marketplace tools are available: market_search discovers plugins on the GitHub dsh-plugin topic, market_info inspects one repository, market_install installs a plugin into a dsh profile as a background job (poll with job_output), market_uninstall removes an installed plugin from a dsh profile as a background job, and market_installed lists a profile's plugins. A successful market_install or market_uninstall only takes effect after the dsh process restarts — remind the user to restart. Prefer plugins with meaningful stars and a dsh.bundle declaration (market_info shows both).",
|
|
238
258
|
});
|
|
239
259
|
|
|
240
260
|
ctx.tools.register(defineTool({
|
|
@@ -363,6 +383,56 @@ export function apply(ctx, config = {}) {
|
|
|
363
383
|
}),
|
|
364
384
|
}));
|
|
365
385
|
|
|
386
|
+
ctx.tools.register(defineTool({
|
|
387
|
+
name: "market_uninstall",
|
|
388
|
+
description: "Remove a plugin from a local dsh profile by running `pnpm remove` in that profile's directory, dropping its entry from the profile's bundle layer list, and deleting its client loader row from cordis.patch.yml if one was registered. Same flow as `dsh plugin --profile <name> remove <package>`. ALWAYS runs as a background job: the call returns a job id immediately; poll with job_output and cancel with job_kill. A successful removal only takes effect after the dsh process restarts.",
|
|
389
|
+
parameters: {
|
|
390
|
+
package: {
|
|
391
|
+
type: "string",
|
|
392
|
+
required: true,
|
|
393
|
+
description: "Installed package name to remove, e.g. \"@1e0zj/dsh-plugin-mall\" or \"dsh-at-file\".",
|
|
394
|
+
},
|
|
395
|
+
profile: {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: `Target profile under $DSH_HOME/profiles. Defaults to "${defaultProfile}".`,
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
output: {
|
|
401
|
+
schema: {
|
|
402
|
+
type: "object",
|
|
403
|
+
additionalProperties: false,
|
|
404
|
+
properties: BACKGROUND_OUTPUT_PROPERTIES,
|
|
405
|
+
},
|
|
406
|
+
render: (args, value) => [{
|
|
407
|
+
type: "text",
|
|
408
|
+
text: `started background job ${value.jobId} (${args.package} ← profile "${args.profile ?? defaultProfile}"); poll with job_output, cancel with job_kill. Restart dsh after a successful uninstall.`,
|
|
409
|
+
}],
|
|
410
|
+
},
|
|
411
|
+
async execute(args, exec) {
|
|
412
|
+
const profile = String(args.profile ?? defaultProfile).trim();
|
|
413
|
+
const packageName = String(args.package ?? "").trim();
|
|
414
|
+
if (packageName.length === 0) throw new Error("market_uninstall: package name is required");
|
|
415
|
+
try {
|
|
416
|
+
resolveProfileDir(profile);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
throw new Error(`market_uninstall: invalid profile: ${error.message}`);
|
|
419
|
+
}
|
|
420
|
+
const jobId = ctx.jobs.start({
|
|
421
|
+
kind: "dsh-plugin-uninstall",
|
|
422
|
+
label: `dsh plugin --profile ${profile} remove ${packageName}`,
|
|
423
|
+
...exec.agent ? { owner: exec.agent } : {},
|
|
424
|
+
run: () => runRemove({ profile, packageName }),
|
|
425
|
+
});
|
|
426
|
+
return { kind: "background", jobId };
|
|
427
|
+
},
|
|
428
|
+
presentCall: (args) => ({
|
|
429
|
+
card: "generic",
|
|
430
|
+
title: `dsh plugin --profile ${args.profile ?? defaultProfile} remove ${args.package}`,
|
|
431
|
+
kind: "execute",
|
|
432
|
+
content: [{ type: "text", text: "Remove a plugin from a dsh profile (background job)" }],
|
|
433
|
+
}),
|
|
434
|
+
}));
|
|
435
|
+
|
|
366
436
|
ctx.tools.register(defineTool({
|
|
367
437
|
name: "market_installed",
|
|
368
438
|
description: "List the plugins installed in a local dsh profile: every dependency with its installed version and whether it declares a dsh bundle (i.e. is an active plugin layer).",
|
package/src/installer.js
CHANGED
|
@@ -187,6 +187,34 @@ export function ensureClientRow(profileDir, packageName) {
|
|
|
187
187
|
return { added: true, rowId };
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Remove the cordis.patch.yml loader row `ensureClientRow` registered for a
|
|
192
|
+
* package. Textual and idempotent: only the exact block the register emitted
|
|
193
|
+
* is spliced out, so user-authored rows survive byte-for-byte. An emptied
|
|
194
|
+
* file falls back to the stock `[]` template.
|
|
195
|
+
* @returns {{ removed: boolean, rowId?: string }}
|
|
196
|
+
*/
|
|
197
|
+
export function removeClientRow(profileDir, packageName) {
|
|
198
|
+
const patchPath = join(profileDir, PROFILE_PATCH_FILENAME);
|
|
199
|
+
if (!existsSync(patchPath)) return { removed: false };
|
|
200
|
+
const content = readFileSync(patchPath, "utf8");
|
|
201
|
+
const rowId = clientRowId(packageName);
|
|
202
|
+
const lines = content.split("\n");
|
|
203
|
+
let removed = false;
|
|
204
|
+
for (let index = 0; index < lines.length - 2; index++) {
|
|
205
|
+
if (lines[index] !== "- insert:") continue;
|
|
206
|
+
if (lines[index + 1] !== ` - id: ${rowId}`) continue;
|
|
207
|
+
if (lines[index + 2] !== ` name: '${packageName}'`) continue;
|
|
208
|
+
lines.splice(index, 3);
|
|
209
|
+
removed = true;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
if (!removed) return { removed: false };
|
|
213
|
+
const next = lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
214
|
+
writeFileSync(patchPath, next.length === 0 ? "[]\n" : `${next}\n`);
|
|
215
|
+
return { removed: true, rowId };
|
|
216
|
+
}
|
|
217
|
+
|
|
190
218
|
// ── build-script allow-listing ──────────────────────────────────────────────
|
|
191
219
|
|
|
192
220
|
/** Extract package names from pnpm's "Ignored build scripts: ..." output. */
|
|
@@ -250,8 +278,12 @@ function ensureAllowBuilds(profileDir, names) {
|
|
|
250
278
|
|
|
251
279
|
let trackerCounter = 0;
|
|
252
280
|
|
|
253
|
-
/**
|
|
254
|
-
|
|
281
|
+
/**
|
|
282
|
+
* Create a tracker for browser-started install/uninstall jobs (see runInstall
|
|
283
|
+
* and runRemove). Same producer shape as the jobs registry ({cancel, done,
|
|
284
|
+
* readOutput}), just an independent registry.
|
|
285
|
+
*/
|
|
286
|
+
export function createJobTracker() {
|
|
255
287
|
const records = new Map();
|
|
256
288
|
const prune = () => {
|
|
257
289
|
const now = Date.now();
|
|
@@ -268,12 +300,14 @@ export function createInstallTracker() {
|
|
|
268
300
|
}
|
|
269
301
|
};
|
|
270
302
|
return {
|
|
271
|
-
start({ profile, spec }) {
|
|
303
|
+
start({ profile, spec, verb = "add" }) {
|
|
272
304
|
const id = `market-${++trackerCounter}`;
|
|
273
|
-
const
|
|
305
|
+
const kind = verb === "remove" ? "dsh-plugin-uninstall" : "dsh-plugin-install";
|
|
306
|
+
const producer = verb === "remove" ? runRemove({ profile, packageName: spec }) : runInstall({ profile, spec });
|
|
274
307
|
const record = {
|
|
275
308
|
id,
|
|
276
|
-
|
|
309
|
+
kind,
|
|
310
|
+
label: `dsh plugin --profile ${profile} ${verb} ${spec}`,
|
|
277
311
|
profile,
|
|
278
312
|
spec,
|
|
279
313
|
status: "running",
|
|
@@ -297,7 +331,7 @@ export function createInstallTracker() {
|
|
|
297
331
|
return {
|
|
298
332
|
snapshot: {
|
|
299
333
|
id: record.id,
|
|
300
|
-
kind:
|
|
334
|
+
kind: record.kind,
|
|
301
335
|
label: record.label,
|
|
302
336
|
status: record.status,
|
|
303
337
|
detail: record.detail,
|
|
@@ -427,3 +461,86 @@ export function runInstall({ profile, spec }) {
|
|
|
427
461
|
},
|
|
428
462
|
};
|
|
429
463
|
}
|
|
464
|
+
|
|
465
|
+
// ── the background uninstall job ────────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
/** A terminal producer for fast-fail cases (no pnpm spawn needed). */
|
|
468
|
+
function failedNow(detail) {
|
|
469
|
+
return {
|
|
470
|
+
cancel: () => {},
|
|
471
|
+
done: Promise.resolve({ status: "failed", detail }),
|
|
472
|
+
readOutput: () => "",
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Run `pnpm remove <package>` in the profile directory as a job producer with
|
|
478
|
+
* the same shape as `runInstall`. On success it reconciles
|
|
479
|
+
* `dsh.profile.bundles` (the removed dependency's bundle entry drops out) and
|
|
480
|
+
* deletes the client loader row `ensureClientRow` had registered for it.
|
|
481
|
+
*/
|
|
482
|
+
export function runRemove({ profile, packageName }) {
|
|
483
|
+
let profileDir;
|
|
484
|
+
try {
|
|
485
|
+
profileDir = resolveProfileDir(profile);
|
|
486
|
+
} catch (error) {
|
|
487
|
+
return failedNow(`invalid profile: ${error.message}`);
|
|
488
|
+
}
|
|
489
|
+
const manifestPath = join(profileDir, "package.json");
|
|
490
|
+
if (!existsSync(manifestPath)) {
|
|
491
|
+
return failedNow(`profile "${profile}" has no package.json — nothing installed to remove`);
|
|
492
|
+
}
|
|
493
|
+
const beforeDeps = new Set(Object.keys(readManifest(profileDir).dependencies ?? {}));
|
|
494
|
+
if (!beforeDeps.has(packageName)) {
|
|
495
|
+
return failedNow(`"${packageName}" is not a dependency of profile "${profile}" (installed: ${[...beforeDeps].join(", ") || "none"})`);
|
|
496
|
+
}
|
|
497
|
+
const deltaQueue = [];
|
|
498
|
+
const push = (text) => {
|
|
499
|
+
deltaQueue.push(text);
|
|
500
|
+
};
|
|
501
|
+
let current = undefined;
|
|
502
|
+
|
|
503
|
+
const proc = spawn("pnpm", ["remove", packageName, "--reporter=append-only"], {
|
|
504
|
+
cwd: profileDir,
|
|
505
|
+
env: process.env,
|
|
506
|
+
shell: process.platform === "win32",
|
|
507
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
508
|
+
windowsHide: true,
|
|
509
|
+
});
|
|
510
|
+
current = proc;
|
|
511
|
+
const done = new Promise((resolve) => {
|
|
512
|
+
proc.on("error", (error) => resolve({ spawnError: error }));
|
|
513
|
+
proc.on("close", (exitCode) => resolve({ exitCode, signal: proc.signalCode }));
|
|
514
|
+
}).then((outcome) => {
|
|
515
|
+
if (outcome.spawnError !== undefined) {
|
|
516
|
+
const hint = outcome.spawnError.code === "ENOENT"
|
|
517
|
+
? "pnpm not found on PATH — install pnpm (e.g. `corepack enable pnpm`) to manage profile plugins"
|
|
518
|
+
: `could not start pnpm: ${outcome.spawnError.message}`;
|
|
519
|
+
return { status: "failed", detail: hint };
|
|
520
|
+
}
|
|
521
|
+
if (outcome.exitCode === null) {
|
|
522
|
+
return { status: "killed", detail: outcome.signal ? `signal: ${outcome.signal}` : "killed before exit" };
|
|
523
|
+
}
|
|
524
|
+
if (outcome.exitCode !== 0) {
|
|
525
|
+
return { status: "failed", detail: `pnpm remove ${packageName} failed (exit code ${outcome.exitCode}). See job output.` };
|
|
526
|
+
}
|
|
527
|
+
const bundles = reconcileBundles(profileDir, beforeDeps);
|
|
528
|
+
const clientRow = removeClientRow(profileDir, packageName);
|
|
529
|
+
const notes = [`bundle layer(s) now: ${bundles.join(", ") || "none (template only)"}`];
|
|
530
|
+
if (clientRow.removed) notes.push(`removed client loader row "${clientRow.rowId}" from cordis.patch.yml`);
|
|
531
|
+
return { status: "completed", detail: `removed ${packageName} from profile "${profile}" — ${notes.join("; ")}. Restart dsh for the change to take effect.` };
|
|
532
|
+
});
|
|
533
|
+
proc.stdout?.on("data", (data) => push(data.toString()));
|
|
534
|
+
proc.stderr?.on("data", (data) => push(data.toString()));
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
cancel: () => {
|
|
538
|
+
current?.kill();
|
|
539
|
+
},
|
|
540
|
+
done,
|
|
541
|
+
readOutput: () => {
|
|
542
|
+
if (deltaQueue.length === 0) return "";
|
|
543
|
+
return deltaQueue.splice(0).join("");
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
}
|