dsh-plugin-marketplace 0.1.1 → 0.2.2

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/client.js CHANGED
@@ -19,6 +19,7 @@ window.__ModuleLoader__.load({
19
19
 
20
20
  // ── CSS (theme tokens) ────────────────────────────────────────────────
21
21
  var CSS = ".__mp_grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px;padding:0;margin:0;list-style:none}" +
22
+ ".__mp_item{display:flex;flex-direction:column;gap:8px}" +
22
23
  ".__mp_card{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:6px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;text-align:left;font:inherit;color:inherit}" +
23
24
  ".__mp_card:hover{border-color:var(--dsw-alias-brand-primary)}" +
24
25
  ".__mp_cardHead{display:flex;align-items:center;gap:8px;min-width:0}" +
@@ -38,6 +39,7 @@ window.__ModuleLoader__.load({
38
39
  ".__mp_readme{font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;max-height:260px;overflow:auto;width:100%;box-sizing:border-box;min-width:0}" +
39
40
  ".__mp_code{display:block;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:6px;padding:8px 10px;font-family:ui-monospace,Consolas,monospace;font-size:12px;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-all;width:100%;box-sizing:border-box;min-width:0}" +
40
41
  ".__mp_link{color:var(--dsw-alias-brand-primary);font-size:12px;text-decoration:none}" +
42
+ ".__mp_btnPrimary{border-color:var(--dsw-alias-state-business-primary, #679efe);background:var(--dsw-alias-state-business-primary, #679efe);color:#fff}" +
41
43
  ".__mp_error{color:var(--dsw-alias-label-error);font-size:12px;margin:8px 0 0}";
42
44
  var tagId = "dsh-plugin-marketplace/main.css";
43
45
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
@@ -50,7 +52,7 @@ window.__ModuleLoader__.load({
50
52
 
51
53
  // ── locale ────────────────────────────────────────────────────────────
52
54
  var NS = "marketplace";
53
- var inject = ["slots", "locale"];
55
+ var inject = ["slots", "locale", "settingsScope", "connection"];
54
56
  var zh = {
55
57
  nav: "插件市场",
56
58
  search: "搜索插件(关键词或留空浏览全部)…",
@@ -68,7 +70,14 @@ window.__ModuleLoader__.load({
68
70
  openNpm: "在 npm 搜索同名包 ↗",
69
71
  updated: "更新",
70
72
  total: "共 {count} 个插件",
71
- ghError: "GitHub API 请求失败(未认证限流 60 次/小时),请稍后再试"
73
+ ghError: "GitHub API 请求失败(未认证限流 60 次/小时),请稍后再试",
74
+ installBtn: "一键安装",
75
+ confirmInstall: "确认安装 {pkg}?",
76
+ installing: "安装中…",
77
+ installOk: "已安装:{msg}",
78
+ installErr: "安装失败:{msg}",
79
+ installIdle: "",
80
+ installingHint: "正在后台执行 dsh plugin add,请稍候…"
72
81
  };
73
82
  var en = {
74
83
  nav: "Plugin Marketplace",
@@ -87,7 +96,14 @@ window.__ModuleLoader__.load({
87
96
  openNpm: "Search npm ↗",
88
97
  updated: "Updated",
89
98
  total: "{count} plugins",
90
- ghError: "GitHub API rate-limited (60/hr unauthenticated), try again later"
99
+ ghError: "GitHub API rate-limited (60/hr unauthenticated), try again later",
100
+ installBtn: "Install",
101
+ confirmInstall: "Install {pkg}?",
102
+ installing: "Installing…",
103
+ installOk: "Installed: {msg}",
104
+ installErr: "Install failed: {msg}",
105
+ installIdle: "",
106
+ installingHint: "Running dsh plugin add in the background…"
91
107
  };
92
108
 
93
109
  // ── GitHub API ────────────────────────────────────────────────────────
@@ -128,7 +144,11 @@ window.__ModuleLoader__.load({
128
144
  async function fetchReadme(fullName) {
129
145
  var url = "https://api.github.com/repos/" + fullName + "/readme";
130
146
  var res = await fetch(url, { headers: { Accept: "application/vnd.github+json" } });
131
- if (!res.ok) throw new Error("no readme");
147
+ if (!res.ok) {
148
+ var e = new Error("readme http " + res.status);
149
+ e.status = res.status;
150
+ throw e;
151
+ }
132
152
  var data = await res.json();
133
153
  var bin = atob(data.content.replace(/\s+/g, ""));
134
154
  var bytes = new Uint8Array(bin.length);
@@ -156,6 +176,32 @@ window.__ModuleLoader__.load({
156
176
  function DetailPanel(props) {
157
177
  var p = props.plugin;
158
178
  var installName = p.fullName.split("/")[1];
179
+ var st = props.installState; // {status, message, pkg} from the settings scope
180
+ var active = st && st.pkg === installName;
181
+ var status = active ? st.status : "idle";
182
+ var [confirming, setConfirming] = react.useState(false);
183
+ var statusNode = null;
184
+ if (status === "running") {
185
+ statusNode = h("div", { className: "__mp_status" }, props.t("installingHint"));
186
+ } else if (status === "ok") {
187
+ statusNode = h("div", { className: "__mp_status" }, props.t("installOk").replace("{msg}", st.message || ""));
188
+ } else if (status === "error") {
189
+ statusNode = h("div", { className: "__mp_error" }, props.t("installErr").replace("{msg}", st.message || ""));
190
+ }
191
+ var btn;
192
+ if (status === "running") {
193
+ btn = h("button", { type: "button", className: "__mp_more", disabled: true }, props.t("installing"));
194
+ } else if (confirming) {
195
+ btn = h("button", {
196
+ type: "button", className: "__mp_btnPrimary __mp_more",
197
+ onClick: function () { setConfirming(false); props.onInstall(installName); }
198
+ }, props.t("confirmInstall").replace("{pkg}", installName));
199
+ } else {
200
+ btn = h("button", {
201
+ type: "button", className: "__mp_btnPrimary __mp_more",
202
+ onClick: function () { setConfirming(true); }
203
+ }, props.t("installBtn"));
204
+ }
159
205
  return h("div", { className: "__mp_detail" },
160
206
  h("div", { className: "__mp_detailTitle" }, p.fullName),
161
207
  h("div", { className: "__mp_meta" },
@@ -175,9 +221,12 @@ window.__ModuleLoader__.load({
175
221
  "# then in $DSH_HOME/profiles/web/cordis.patch.yml:\n" +
176
222
  "- insert:\n - id: " + installName + "\n name: '" + installName + "'"
177
223
  ),
224
+ btn,
225
+ statusNode,
178
226
  h("div", { className: "__mp_detailTitle" }, props.t("readme")),
179
227
  h("div", { className: "__mp_readme" },
180
228
  props.readmeLoading ? props.t("loading")
229
+ : props.readmeRateLimited ? props.ghError
181
230
  : props.readmeError ? props.t("readmeEmpty")
182
231
  : (props.readme || "")
183
232
  )
@@ -186,8 +235,33 @@ window.__ModuleLoader__.load({
186
235
 
187
236
  function MarketplaceSection(props) {
188
237
  var t = props.t;
189
- var state = react.useState({ q: "", sort: "stars", page: 1, items: [], loading: false, error: null, total: 0, open: null, readme: null, readmeLoading: false, readmeError: false });
238
+ var scope = props.scope;
239
+ var api = props.api;
240
+ var state = react.useState({ q: "", sort: "stars", page: 1, items: [], loading: false, error: null, total: 0, open: null, readme: null, readmeLoading: false, readmeError: false, readmeRateLimited: false });
190
241
  var s = state[0], set = state[1];
242
+ // install-state subscription (stable pattern: useState + subscribe, never
243
+ // depend on getSnapshot() reference identity).
244
+ var [installState, setInstallState] = react.useState(null);
245
+ react.useEffect(function () {
246
+ var alive = true;
247
+ var sync = function () { if (alive) setInstallState(scope.getSnapshot()); };
248
+ sync();
249
+ var un = typeof scope.subscribe === "function" ? scope.subscribe(sync) : null;
250
+ return function () { alive = false; if (un) un(); if (scope.dispose) scope.dispose(); };
251
+ }, [scope]);
252
+ var onInstall = react.useCallback(function (pkg) {
253
+ api.settings.mutate({
254
+ ns: "plugin-marketplace",
255
+ ops: [{ op: "set", path: ["install"], value: { pkg: pkg, ts: Date.now() } }]
256
+ }).then(function (response) {
257
+ if (!response.result.ok) {
258
+ var detail = response.result.error || {};
259
+ set(function (prev) { return Object.assign({}, prev, { installError: String(detail.message || detail.code || "unknown") }); });
260
+ }
261
+ }).catch(function (e) {
262
+ set(function (prev) { return Object.assign({}, prev, { installError: String(e && e.message || e) }); });
263
+ });
264
+ }, [api]);
191
265
  var load = react.useCallback(function (q, sort, page, append) {
192
266
  set(function (prev) { return Object.assign({}, prev, { loading: true, error: null }); });
193
267
  fetchPage(q, sort, page).then(function (out) {
@@ -204,16 +278,23 @@ window.__ModuleLoader__.load({
204
278
  }, [load]);
205
279
  var openDetail = react.useCallback(function (plugin) {
206
280
  if (s.open && s.open.fullName === plugin.fullName) { set(function (prev) { return Object.assign({}, prev, { open: null }); }); return; }
207
- set(function (prev) { return Object.assign({}, prev, { open: plugin, readme: null, readmeError: false, readmeLoading: true }); });
281
+ set(function (prev) { return Object.assign({}, prev, { open: plugin, readme: null, readmeError: false, readmeRateLimited: false, readmeLoading: true }); });
282
+ // Detail expands inline under the clicked card; scroll minimally
283
+ // (nearest) only when it would fall outside the viewport.
284
+ setTimeout(function () {
285
+ var el = document.querySelector(".__mp_detail");
286
+ if (el) el.scrollIntoView({ behavior: "smooth", block: "nearest" });
287
+ }, 50);
208
288
  fetchReadme(plugin.fullName).then(function (text) {
209
289
  set(function (prev) {
210
290
  if (!prev.open || prev.open.fullName !== plugin.fullName) return prev;
211
- return Object.assign({}, prev, { readme: text.slice(0, 1200), readmeLoading: false });
291
+ return Object.assign({}, prev, { readme: text.slice(0, 1200), readmeLoading: false, readmeError: false, readmeRateLimited: false });
212
292
  });
213
- }).catch(function () {
293
+ }).catch(function (err) {
214
294
  set(function (prev) {
215
295
  if (!prev.open || prev.open.fullName !== plugin.fullName) return prev;
216
- return Object.assign({}, prev, { readmeLoading: false, readmeError: true });
296
+ var rateLimited = err && (err.status === 403 || err.status === 429);
297
+ return Object.assign({}, prev, { readmeLoading: false, readmeError: !rateLimited, readmeRateLimited: rateLimited });
217
298
  });
218
299
  });
219
300
  }, [s.open]);
@@ -232,13 +313,21 @@ window.__ModuleLoader__.load({
232
313
  ),
233
314
  s.total > 0 ? h("p", { className: "__mp_status" }, t("total").replace("{count}", String(s.total))) : null,
234
315
  h("ul", { className: "__mp_grid" },
235
- s.items.map(function (p) { return h(PluginCard, { key: p.fullName, plugin: p, t: t, onOpen: function () { openDetail(p); } }); })
316
+ s.items.map(function (p) {
317
+ // Detail expands INLINE right under the clicked card, so there is
318
+ // no jumping around the page.
319
+ var open = s.open && s.open.fullName === p.fullName;
320
+ return h("div", { key: p.fullName, className: "__mp_item" },
321
+ h(PluginCard, { plugin: p, t: t, onOpen: function () { openDetail(p); } }),
322
+ open ? h(DetailPanel, { plugin: s.open, t: t, readme: s.readme, readmeLoading: s.readmeLoading, readmeError: s.readmeError, readmeRateLimited: s.readmeRateLimited, lang: s.open.lang, installState: installState && installState.status === "ready" ? installState.value.installState : null, onInstall: onInstall, ghError: t("ghError") }) : null
323
+ );
324
+ })
236
325
  ),
237
326
  s.loading ? h("p", { className: "__mp_status" }, t("loading")) : null,
238
327
  s.error ? h("p", { className: "__mp_error" }, s.error) : null,
239
328
  s.items.length === 0 && !s.loading && !s.error ? h("p", { className: "__mp_status" }, t("empty")) : null,
240
329
  s.items.length > 0 ? h("button", { type: "button", className: "__mp_more", onClick: more, disabled: s.loading }, t("loadMore")) : null,
241
- s.open ? h(DetailPanel, { plugin: s.open, t: t, readme: s.readme, readmeLoading: s.readmeLoading, readmeError: s.readmeError, lang: s.open.lang }) : null
330
+ s.installError ? h("p", { className: "__mp_error" }, s.installError) : null
242
331
  );
243
332
  }
244
333
 
@@ -246,6 +335,8 @@ window.__ModuleLoader__.load({
246
335
  function apply(ctx) {
247
336
  var t = ctx.locale.bind(NS);
248
337
  ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-plugin-marketplace: dictionaries");
338
+ var scope = ctx.settingsScope.bind({ namespace: "plugin-marketplace" });
339
+ var api = ctx.connection.api;
249
340
  ctx.slots.inject("settings.section", function () {
250
341
  return ctx.slots.register({
251
342
  name: "settings.section",
@@ -253,7 +344,9 @@ window.__ModuleLoader__.load({
253
344
  order: 20,
254
345
  label: function () { return t("nav"); },
255
346
  locale: NS
256
- }, MarketplaceSection);
347
+ }, function (props) {
348
+ return h(MarketplaceSection, Object.assign({}, props, { scope: scope, api: api }));
349
+ });
257
350
  });
258
351
  }
259
352
 
package/index.js CHANGED
@@ -1,11 +1,162 @@
1
1
  /**
2
2
  * dsh-plugin-marketplace — node half.
3
3
  *
4
- * The empty apply exists so the plugin appears in the host Loader; the
5
- * browser half owns the marketplace section through exports["./client"],
6
- * discovered from the package.json `dsh.client` declaration.
4
+ * Host side of the marketplace: owns a `plugin-marketplace` settings
5
+ * namespace used as a message channel between the browser UI and this
6
+ * process. The client writes an `install` request; the settings watch here
7
+ * spawns `dsh plugin --profile <p> add <pkg>` (npm install into the profile)
8
+ * and appends the mount row to the profile's cordis.patch.yml, then reports
9
+ * progress/result back through `installState`.
10
+ *
11
+ * The settings section UI itself lives in the browser half (exports["./client"]).
7
12
  */
8
- /** Host plugin body — no host-side behavior for this surface plugin. */
9
- function apply() {}
13
+ import { execFile } from "node:child_process";
14
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import os from "node:os";
17
+ import z from "@deepseek-ai/schemastery";
18
+ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
19
+ import { ensureSettingsNamespaceExposed } from "./vendor/dsh-settings-expose.js";
20
+
21
+ /** Cordis plugin name. */
22
+ const name = "plugin-marketplace";
23
+ /** Settings namespace owned by this plugin (Web UI settings section + install channel). */
24
+ const NS = settingsNamespace("plugin-marketplace");
25
+
26
+ /** Runtime schema: the install request + the install state report. */
27
+ const Config = z.object({
28
+ install: z.object({
29
+ /** npm package name to install ("" = no pending request). */
30
+ pkg: z.string().default(""),
31
+ /** client-set timestamp; host dedups on it. */
32
+ ts: z.number().default(0),
33
+ }),
34
+ installState: z.object({
35
+ status: z.string().default("idle"), // idle | running | ok | error
36
+ message: z.string().default(""),
37
+ ts: z.number().default(0),
38
+ }),
39
+ });
40
+
41
+ /** npm package-name shape (scope/name, no spaces, no path chars). */
42
+ const PKG_NAME = /^@?[a-z0-9][a-z0-9-._]*(?:\/[a-z0-9][a-z0-9-._]*)?$/;
43
+
44
+ function apply(ctx, config) {
45
+ let sourceGetter = null;
46
+ let lastTs = 0;
47
+
48
+ // ── settings-backed configuration ─────────────────────────────────────────
49
+ // The browser half edits this namespace; `onChange` fires on every write,
50
+ // which is what turns an `install` request into a real install.
51
+ installSettingsSection(ctx, NS, Config, config, {
52
+ setSource: (getter) => {
53
+ sourceGetter = getter;
54
+ },
55
+ onChange: () => {
56
+ void maybeRunInstall();
57
+ },
58
+ });
59
+
60
+ // dsh-host-apiproxy hard-codes which namespaces the Web client may touch;
61
+ // patch the allowlist idempotently so the client can write install requests.
62
+ ensureSettingsNamespaceExposed(ctx, "plugin-marketplace", ctx.logger);
63
+
64
+ /** Resolved current config (settings layer over the entry). */
65
+ function current() {
66
+ return sourceGetter ? sourceGetter() : config;
67
+ }
68
+
69
+ /** Write the install-state report (and clear the consumed request). */
70
+ async function report(status, message, pkg) {
71
+ const settings = ctx.get("settings");
72
+ if (!settings) return;
73
+ const ts = Date.now();
74
+ await settings.update(NS, {
75
+ installState: { status, message, ts, pkg: pkg || "" },
76
+ install: { pkg: "", ts: 0 },
77
+ }).catch((error) => {
78
+ ctx.logger.warn(`[plugin-marketplace] state write failed: ${String(error)}`);
79
+ });
80
+ }
81
+
82
+ /** Find the active profile name from the process args ("web" default). */
83
+ function profileName() {
84
+ const argv = process.argv;
85
+ const i = argv.indexOf("--profile");
86
+ if (i >= 0 && argv[i + 1]) return argv[i + 1];
87
+ return "web";
88
+ }
89
+
90
+ /** The dsh CLI entry used to boot this process (its own bin.js). */
91
+ function dshBin() {
92
+ return process.argv[1];
93
+ }
94
+
95
+ /** Append a mount row for `pkg` to cordis.patch.yml (idempotent). */
96
+ function ensureMounted(pkg, profile) {
97
+ const dshHome = process.env.DSH_HOME || join(os.homedir(), ".dsh");
98
+ const patchPath = join(dshHome, "profiles", profile, "cordis.patch.yml");
99
+ if (!existsSync(patchPath)) return " (patch file not found; mount manually)";
100
+ const id = pkg.replace(/^@[^/]+\//, "").replace(/[^a-z0-9-]/g, "-") || pkg;
101
+ let src = readFileSync(patchPath, "utf8");
102
+ if (src.includes(`name: '${pkg}'`) || src.includes(`name: "${pkg}"`)) return " (already mounted)";
103
+ // Append under the first `- insert:` list; match its indentation (4 spaces
104
+ // is the convention used by dsh profiles).
105
+ const insertAt = src.search(/^- insert:\s*$/m);
106
+ const row = `\n - id: ${id}\n name: '${pkg}'`;
107
+ if (insertAt >= 0) {
108
+ src = src.slice(0, insertAt + "- insert:".length) + row + src.slice(insertAt + "- insert:".length);
109
+ } else {
110
+ src += `\n- insert:${row}\n`;
111
+ }
112
+ writeFileSync(patchPath, src, "utf8");
113
+ return " (mounted in cordis.patch.yml)";
114
+ }
115
+
116
+ /** Run the install; resolve {code, stdout, stderr}. */
117
+ function runInstall(pkg, profile) {
118
+ const bin = dshBin();
119
+ const node = process.execPath;
120
+ return new Promise((resolve) => {
121
+ execFile(node, [bin, "plugin", "--profile", profile, "add", pkg], {
122
+ timeout: 5 * 60 * 1000,
123
+ maxBuffer: 8 * 1024 * 1024,
124
+ windowsHide: true,
125
+ }, (error, stdout, stderr) => {
126
+ resolve({ code: error ? (error.code ?? 1) : 0, stdout, stderr });
127
+ });
128
+ });
129
+ }
130
+
131
+ async function maybeRunInstall() {
132
+ try {
133
+ const cfg = current();
134
+ const req = cfg?.install;
135
+ if (!req || !req.pkg || req.ts === lastTs || req.ts === 0) return;
136
+ lastTs = req.ts;
137
+ const pkg = String(req.pkg).trim();
138
+ if (!PKG_NAME.test(pkg)) {
139
+ await report("error", `invalid package name: ${pkg}`, pkg);
140
+ return;
141
+ }
142
+ const profile = profileName();
143
+ ctx.logger.info(`[plugin-marketplace] installing ${pkg} (profile=${profile})…`);
144
+ await report("running", `installing ${pkg}…`, pkg);
145
+ const result = await runInstall(pkg, profile);
146
+ if (result.code !== 0) {
147
+ const tail = (result.stderr || result.stdout || "").trim().split("\n").slice(-3).join(" ");
148
+ ctx.logger.warn(`[plugin-marketplace] install failed (${result.code}): ${tail}`);
149
+ await report("error", `install failed: ${tail || "unknown error"}`, pkg);
150
+ return;
151
+ }
152
+ const mountNote = ensureMounted(pkg, profile);
153
+ ctx.logger.info(`[plugin-marketplace] ${pkg} installed${mountNote}`);
154
+ await report("ok", `${pkg} installed${mountNote}. Restart dsh web to load it.`, pkg);
155
+ } catch (error) {
156
+ ctx.logger.warn(`[plugin-marketplace] install flow failed: ${String(error)}`);
157
+ await report("error", String(error instanceof Error ? error.message : error));
158
+ }
159
+ }
160
+ }
10
161
 
11
- export { apply };
162
+ export { Config, apply, name };
package/package.json CHANGED
@@ -1,46 +1,51 @@
1
- {
2
- "name": "dsh-plugin-marketplace",
3
- "version": "0.1.1",
4
- "description": "DeepSeek Harness Web UI 内置插件市场:在设置页直接浏览 github.com/topics/dsh-plugin,支持搜索、按 Star 排序、查看安装方式。",
5
- "type": "module",
6
- "main": "index.js",
7
- "exports": {
8
- ".": "./index.js",
9
- "./client": "./client.js",
10
- "./package.json": "./package.json"
11
- },
12
- "dsh": {
13
- "client": {
14
- "platform": "web"
15
- }
16
- },
17
- "files": [
18
- "index.js",
19
- "client.js",
20
- "README.md",
21
- "README.zh.md",
22
- "LICENSE"
23
- ],
24
- "homepage": "https://github.com/Scorp1o117/dsh-plugin-marketplace",
25
- "keywords": [
26
- "dsh",
27
- "dsh-plugin",
28
- "deepseek-harness",
29
- "marketplace",
30
- "web-ui"
31
- ],
32
- "license": "MIT",
33
- "engines": {
34
- "node": ">=20.18"
35
- },
36
- "repository": {
37
- "type": "git",
38
- "url": "git+https://github.com/Scorp1o117/dsh-plugin-marketplace.git"
39
- },
40
- "bugs": {
41
- "url": "https://github.com/Scorp1o117/dsh-plugin-marketplace/issues"
42
- },
43
- "peerDependencies": {
44
- "react": "^18.2.0"
45
- }
46
- }
1
+ {
2
+ "name": "dsh-plugin-marketplace",
3
+ "version": "0.2.2",
4
+ "description": "DeepSeek Harness Web UI 内置插件市场:在设置页直接浏览 github.com/topics/dsh-plugin,支持搜索、按 Star 排序、查看安装方式。",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./client": "./client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "dsh": {
13
+ "client": {
14
+ "platform": "web"
15
+ }
16
+ },
17
+ "files": [
18
+ "index.js",
19
+ "client.js",
20
+ "README.md",
21
+ "README.zh.md",
22
+ "LICENSE",
23
+ "vendor"
24
+ ],
25
+ "homepage": "https://github.com/Scorp1o117/dsh-plugin-marketplace",
26
+ "keywords": [
27
+ "dsh",
28
+ "dsh-plugin",
29
+ "deepseek-harness",
30
+ "marketplace",
31
+ "web-ui"
32
+ ],
33
+ "license": "MIT",
34
+ "engines": {
35
+ "node": "\u003e=20.18"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/Scorp1o117/dsh-plugin-marketplace.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/Scorp1o117/dsh-plugin-marketplace/issues"
43
+ },
44
+ "peerDependencies": {
45
+ "react": "^18.2.0",
46
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6"
47
+ },
48
+ "dependencies": {
49
+ "@deepseek-ai/schemastery": "^3.18.1"
50
+ }
51
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * dsh-host-apiproxy exposes only namespaces listed in its hard-coded
3
+ * WEB_SETTINGS_NAMESPACES allowlist to the Web settings client. A namespace
4
+ * registered by a third-party plugin answers `settings-not-exposed` even
5
+ * though it is registered — upstream explicitly defers letting plugins
6
+ * expose their own configuration.
7
+ *
8
+ * This module idempotently patches that allowlist in the dsh installation
9
+ * actually loaded by the host process, so the plugin's settings section
10
+ * becomes visible in the Web UI without manual edits. A dsh update
11
+ * overwrites the file; the next plugin start re-patches it.
12
+ */
13
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { createRequire } from "node:module";
15
+ import { dirname, join, sep } from "node:path";
16
+
17
+ /**
18
+ * Ensure `nsName` is present in dsh-host-apiproxy's WEB_SETTINGS_NAMESPACES.
19
+ * No-op when already exposed or when the file cannot be located/patched.
20
+ * @param {import("cordis").Context} ctx
21
+ * @param {string} nsName - settings namespace short name (e.g. "tdai-memory").
22
+ * @param {{info?: Function, warn?: Function}} logger - dsh logger.
23
+ */
24
+ export function ensureSettingsNamespaceExposed(ctx, nsName, logger) {
25
+ void ctx;
26
+ try {
27
+ const target = findApiproxyIndex();
28
+ if (!target) {
29
+ logger?.warn?.(`[settings-expose] could not locate dsh-host-apiproxy; add "${nsName}" to WEB_SETTINGS_NAMESPACES in dsh-host-apiproxy/lib/index.js to get a Web settings section`);
30
+ return;
31
+ }
32
+ let src;
33
+ try {
34
+ src = readFileSync(target, "utf8");
35
+ } catch (error) {
36
+ logger?.warn?.(`[settings-expose] cannot read ${target}: ${String(error)}`);
37
+ return;
38
+ }
39
+ const body = src.match(/const WEB_SETTINGS_NAMESPACES = \[([\s\S]*?)\];/)?.[1] ?? "";
40
+ if (body.includes(`"${nsName}"`)) return; // already exposed (manual or previous patch)
41
+ const patched = src.replace(/(const WEB_SETTINGS_NAMESPACES = \[[\s\S]*?)(\n\s*\];)/, (_, pre, post) => {
42
+ const trailingComma = /,\s*$/.test(pre) ? "" : ",";
43
+ const sep2 = pre.trimEnd().endsWith("[") ? "" : trailingComma;
44
+ return `${pre}${sep2}\n\t"${nsName}"${post}`;
45
+ });
46
+ if (patched === src) {
47
+ logger?.warn?.(`[settings-expose] allowlist pattern not found in ${target}; add "${nsName}" to WEB_SETTINGS_NAMESPACES manually`);
48
+ return;
49
+ }
50
+ writeFileSync(target, patched, "utf8");
51
+ logger?.info?.(`[settings-expose] added "${nsName}" to WEB_SETTINGS_NAMESPACES (${target}) — restart dsh web for the settings section to appear`);
52
+ } catch (error) {
53
+ logger?.warn?.(`[settings-expose] failed: ${String(error)}`);
54
+ }
55
+ }
56
+
57
+ function findApiproxyIndex() {
58
+ // 1) The host process has already loaded dsh-host-apiproxy: read the real
59
+ // module path from the CommonJS module cache (any install layout).
60
+ try {
61
+ const Module = createRequire(import.meta.url)("module");
62
+ const cache = Module._cache ?? {};
63
+ for (const key of Object.keys(cache)) {
64
+ if (key.includes(`${sep}dsh-host-apiproxy${sep}`) && key.endsWith(`${sep}index.js`)) return key;
65
+ }
66
+ } catch { /* fall through */ }
67
+ // 2) Fallback: sibling of @deepseek-ai/dsh-settings (dsh's nested layout).
68
+ try {
69
+ const require = createRequire(import.meta.url);
70
+ const settingsEntry = require.resolve("@deepseek-ai/dsh-settings");
71
+ const candidate = join(dirname(dirname(dirname(settingsEntry))), "dsh-host-apiproxy", "lib", "index.js");
72
+ if (existsSync(candidate)) return candidate;
73
+ } catch { /* fall through */ }
74
+ return "";
75
+ }