dsh-plugin-marketplace 0.3.7 → 0.4.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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Scorp1o117
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Scorp1o117
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # dsh-plugin-marketplace
2
2
 
3
+ ## Configuration page (DSH 0.2.0-rc.2 and later)
4
+
5
+ Open **Plugins → Installed → dsh-plugin-marketplace** from the homepage sidebar to configure and save this plugin. The page uses the official `plugins.bundle.config` interface, without a duplicate entry in global Settings. Web and Desktop share the page. This version requires DSH 0.2.0-rc.2 or a later 0.2.x host; existing configuration is retained.
6
+
3
7
  [![中文文档](https://img.shields.io/badge/%E4%B8%AD%E6%96%87%E6%96%87%E6%A1%A3-blue)](README.zh.md)
4
8
 
5
9
  **GitHub**: [Scorp1o117/dsh-plugin-marketplace](https://github.com/Scorp1o117/dsh-plugin-marketplace) · **npm**: [dsh-plugin-marketplace](https://www.npmjs.com/package/dsh-plugin-marketplace)
package/README.zh.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # dsh-plugin-marketplace
2
2
 
3
+ ## 配置入口(DSH 0.2.0-rc.2 起)
4
+
5
+ 在首页侧边栏打开 **插件 → 已安装 → dsh-plugin-marketplace**,直接在插件详情页配置并保存。配置页注册到官方的 `plugins.bundle.config` 接口;全局设置页不再重复显示配置入口。Web 与桌面版使用相同界面,本版要求 DSH 0.2.0-rc.2 或更新的 0.2.x 版本。现有配置无需迁移。
6
+
3
7
  **GitHub**: [Scorp1o117/dsh-plugin-marketplace](https://github.com/Scorp1o117/dsh-plugin-marketplace) · **npm**: [dsh-plugin-marketplace](https://www.npmjs.com/package/dsh-plugin-marketplace) · [English](README.md)
4
8
 
5
9
  [![Enhancement Suite](https://img.shields.io/badge/part%20of-Enhancement%20Suite-3964fe)](https://github.com/Scorp1o117/dsh-enhancement-suite) [![npm](https://img.shields.io/npm/v/dsh-enhancement-suite)](https://www.npmjs.com/package/dsh-enhancement-suite)
@@ -54,7 +58,7 @@ dsh plugin --profile web add dsh-plugin-marketplace
54
58
  > `cordis.patch.yml` 里那一行**——bundle 层自己会插入条目,两行同 id 会让启动
55
59
  > 直接报 `duplicate loader entry id: plugin-marketplace`。两种方式二选一,别同时用。
56
60
 
57
- 然后重启 `dsh web`(新客户端插件需要重启进程才会被扫描进浏览器清单),打开 **设置 → 插件市场**。
61
+ 然后重启 `dsh web`(新客户端插件需要重启进程才会被扫描进浏览器清单),打开 **插件 → dsh-plugin-marketplace**。
58
62
 
59
63
  DSH `0.1.0-rc.7` 起会公开全部已注册的 settings 命名空间,插件市场无需再修改官方文件。
60
64
  因此从 `0.2.8` 起最低支持 DSH `0.1.0-rc.7`。仍使用 DSH `0.1.0-rc.6` 的用户
package/client.js CHANGED
@@ -1,465 +1,463 @@
1
- /**
2
- * dsh-plugin-marketplace — browser half.
3
- *
4
- * A marketplace section inside the Web UI settings page: browses
5
- * github.com/topics/dsh-plugin through the public GitHub search API
6
- * (CORS-enabled), with keyword search, star/update sorting, paging, and a
7
- * per-plugin detail panel (README summary + install instructions).
8
- *
9
- * Hand-written ModuleLoader bundle — no build step required.
10
- */
11
- window.__ModuleLoader__.load({
12
- id: "dsh-plugin-marketplace",
13
- factory: (require) => {
14
- var module = { exports: {} };
15
- var exports = module.exports;
16
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
17
- var react = require("react");
18
- var h = react.createElement;
19
-
20
- // ── CSS (theme tokens) ────────────────────────────────────────────────
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}" +
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}" +
24
- ".__mp_card:hover{border-color:var(--dsw-alias-brand-primary)}" +
25
- ".__mp_cardHead{display:flex;align-items:center;gap:8px;min-width:0}" +
26
- ".__mp_name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}" +
27
- ".__mp_star{flex:none;font-size:12px;color:var(--dsw-alias-label-secondary)}" +
28
- ".__mp_desc{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}" +
29
- ".__mp_meta{display:flex;gap:10px;font-size:11px;color:var(--dsw-alias-label-tertiary)}" +
30
- ".__mp_toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px}" +
31
- ".__mp_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;min-width:200px;flex:1}" +
32
- ".__mp_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 8px;font-size:13px}" +
33
- ".__mp_more{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 14px;font:inherit;font-size:13px;cursor:pointer;margin-top:12px}" +
34
- ".__mp_more:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary)}" +
35
- ".__mp_more:disabled{opacity:.5;cursor:default}" +
36
- ".__mp_status{font-size:12px;color:var(--dsw-alias-label-tertiary);margin:8px 0 0}" +
37
- ".__mp_detail{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:14px;margin-top:10px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px;width:100%;box-sizing:border-box}" +
38
- ".__mp_detailTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}" +
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}" +
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}" +
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}" +
43
- ".__mp_error{color:var(--dsw-alias-label-error);font-size:12px;margin:8px 0 0}";
44
- var tagId = "dsh-plugin-marketplace/main.css";
45
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
46
- var tag = document.createElement("style");
47
- tag.dataset.plugin = "dsh-plugin-marketplace";
48
- tag.dataset.pluginCss = tagId;
49
- tag.textContent = CSS;
50
- document.head.appendChild(tag);
51
- }
52
-
53
- // ── locale ────────────────────────────────────────────────────────────
54
- var NS = "marketplace";
55
- var inject = ["slots", "locale", "configForms", "connection"];
56
- var zh = {
57
- nav: "插件市场",
58
- search: "搜索插件(关键词或留空浏览全部)…",
59
- sortStars: "按 Star 排序",
60
- sortUpdated: "按更新时间排序",
61
- loadMore: "加载更多",
62
- loading: "加载中…",
63
- error: "加载失败,请稍后重试",
64
- empty: "没有找到插件",
65
- install: "安装方式",
66
- installHint: "在 profile 目录执行 pnpm 安装,然后在 cordis.patch.yml 挂载:",
67
- installHintBundle: "标准 bundle 插件:dsh plugin add 后由 bundle 层自动挂载,无需手动改 cordis.patch.yml:",
68
- readme: "README 摘要",
69
- readmeEmpty: "(该仓库没有 README)",
70
- openRepo: "打开 GitHub 仓库 ↗",
71
- openNpm: "在 npm 搜索同名包 ↗",
72
- updated: "更新",
73
- total: "共 {count} 个插件",
74
- ghError: "GitHub API 请求失败(未认证限流 60 次/小时),请稍后再试",
75
- installBtn: "一键安装",
76
- confirmInstall: "确认安装 {pkg}?",
77
- installing: "安装中…",
78
- installOk: "已安装:{msg}",
79
- installErr: "安装失败:{msg}",
80
- installIdle: "",
81
- installingHint: "正在后台执行 dsh plugin add,请稍候…",
82
- aiExplain: "🤖 AI 解释",
83
- aiExplaining: "AI 解释中,请稍候…",
84
- aiExplainErr: "AI 解释失败:{msg}",
85
- notExposed: "无法写入插件市场的设置通道:DSH 版本过旧(需 0.1.0-rc.7+)、宿主半端未挂载,或当前页面不是本机回环访问;请通过 127.0.0.1 打开 dsh web 后重试。"
86
- };
87
- var en = {
88
- nav: "Plugin Marketplace",
89
- search: "Search plugins (keyword, or empty to browse all)…",
90
- sortStars: "Sort by stars",
91
- sortUpdated: "Sort by updated",
92
- loadMore: "Load more",
93
- loading: "Loading…",
94
- error: "Failed to load, try again later",
95
- empty: "No plugins found",
96
- install: "Install",
97
- installHint: "Run pnpm in your profile dir, then mount in cordis.patch.yml:",
98
- installHintBundle: "Standard bundle plugin: dsh plugin add auto-mounts it via its bundle layer — no manual cordis.patch.yml edit needed:",
99
- readme: "README summary",
100
- readmeEmpty: "(no README in this repo)",
101
- openRepo: "Open GitHub repo ↗",
102
- openNpm: "Search npm ↗",
103
- updated: "Updated",
104
- total: "{count} plugins",
105
- ghError: "GitHub API rate-limited (60/hr unauthenticated), try again later",
106
- installBtn: "Install",
107
- confirmInstall: "Install {pkg}?",
108
- installing: "Installing…",
109
- installOk: "Installed: {msg}",
110
- installErr: "Install failed: {msg}",
111
- installIdle: "",
112
- installingHint: "Running dsh plugin add in the background…",
113
- aiExplain: "🤖 AI Explain",
114
- aiExplaining: "AI is explaining…",
115
- aiExplainErr: "AI explain failed: {msg}",
116
- notExposed: "The plugin-marketplace settings channel is not writable here: the DSH version is too old (0.1.0-rc.7+ required), the host half is not mounted, or this page is not a loopback connection. Open dsh web via 127.0.0.1 and try again."
117
- };
118
-
119
- // ── GitHub API ────────────────────────────────────────────────────────
120
- var BASE = "https://api.github.com/search/repositories";
121
- var cache = new Map(); // queryKey -> {items, incomplete}
122
- function queryKey(q, sort, page) { return q + "|" + sort + "|" + page; }
123
- function fmtTime(iso) {
124
- try { return new Date(iso).toISOString().slice(0, 10); } catch { return ""; }
125
- }
126
- async function fetchPage(q, sort, page) {
127
- var key = queryKey(q, sort, page);
128
- var hit = cache.get(key);
129
- if (hit) return hit;
130
- var params = new URLSearchParams({
131
- q: "topic:dsh-plugin" + (q ? " " + q : ""),
132
- sort: sort,
133
- order: "desc",
134
- per_page: "20",
135
- page: String(page)
136
- });
137
- var res = await fetch(BASE + "?" + params.toString(), { headers: { Accept: "application/vnd.github+json" } });
138
- if (!res.ok) throw new Error("HTTP " + res.status);
139
- var data = await res.json();
140
- var items = (data.items || []).map(function (r) {
141
- return {
142
- fullName: r.full_name,
143
- desc: r.description || "",
144
- stars: r.stargazers_count || 0,
145
- updated: r.updated_at || "",
146
- lang: r.language || "",
147
- htmlUrl: r.html_url
148
- };
149
- });
150
- var out = { items: items, total: data.total_count || 0 };
151
- cache.set(key, out);
152
- return out;
153
- }
154
- async function fetchReadme(fullName) {
155
- var url = "https://api.github.com/repos/" + fullName + "/readme";
156
- var res = await fetch(url, { headers: { Accept: "application/vnd.github+json" } });
157
- if (!res.ok) {
158
- var e = new Error("readme http " + res.status);
159
- e.status = res.status;
160
- throw e;
161
- }
162
- var data = await res.json();
163
- var bin = atob(data.content.replace(/\s+/g, ""));
164
- var bytes = new Uint8Array(bin.length);
165
- for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
166
- return new TextDecoder("utf-8").decode(bytes);
167
- }
168
- // npm metadata (bundle detection for the install hint); CORS-enabled, cached.
169
- var npmCache = new Map(); // pkg -> {isBundle} | null
170
- async function fetchNpmInfo(pkg) {
171
- if (npmCache.has(pkg)) return npmCache.get(pkg);
172
- var promise = fetch("https://registry.npmjs.org/" + encodeURIComponent(pkg) + "/latest", {
173
- headers: { Accept: "application/vnd.npm.install-v1+json" }
174
- }).then(function (res) {
175
- if (!res.ok) return null;
176
- return res.json();
177
- }).then(function (data) {
178
- if (!data || !data.dsh) return { isBundle: false };
179
- return { isBundle: typeof data.dsh.bundle === "object" && data.dsh.bundle !== null && typeof data.dsh.bundle.patch === "string" && data.dsh.bundle.patch.length > 0 };
180
- }).catch(function () {
181
- return null;
182
- });
183
- npmCache.set(pkg, promise);
184
- return promise;
185
- }
186
-
187
- // ── settings write channel ────────────────────────────────────────────
188
- /**
189
- * Write one namespace field through whichever settings channel this host
190
- * exposes. dsh 0.1.2-rc.1 dropped `connection.api` (the old raw settings
191
- * RPC); there the bound scope's own `mutate` (the remote.settings RPC) is
192
- * the only write path. Older hosts (0.1.0-rc.7 / 0.1.1.x) have no public
193
- * scope mutate but keep `connection.api`, whose structured error responses
194
- * the UI can show verbatim. Always resolves `{ok, error?}` — refused or
195
- * failed writes surface as a result, never as a rejection.
196
- */
197
- function writeField(scope, api, field, value) {
198
- if (scope && typeof scope.mutate === "function") {
199
- return scope.mutate([{ op: "set", path: [field], value: value }]).then(function () {
200
- // A settled write is not an applied write: one the Host refused
201
- // (settings/conflict) still resolves. Compare the section itself —
202
- // checking only `status === "ready"` passes after a refusal, because
203
- // the namespace stays registered and the recovery read re-renders it
204
- // as ready.
205
- var snap = scope.getSnapshot();
206
- if (!snap || snap.status !== "ready" || snap.value === void 0) {
207
- return { ok: false, error: { code: "settings-not-exposed" } };
208
- }
209
- if (JSON.stringify(snap.value[field]) !== JSON.stringify(value)) {
210
- return { ok: false, error: { code: "settings-not-applied" } };
211
- }
212
- return { ok: true };
213
- }, function (e) {
214
- return { ok: false, error: { message: String(e && e.message || e) } };
215
- });
216
- }
217
- return api.settings.mutate({
218
- ns: "plugin-marketplace",
219
- ops: [{ op: "set", path: [field], value: value }]
220
- }).then(function (response) {
221
- return (response && response.result) ? response.result : { ok: false, error: { message: "malformed settings response" } };
222
- }, function (e) {
223
- return { ok: false, error: { message: String(e && e.message || e) } };
224
- });
225
- }
226
-
227
- // ── components ────────────────────────────────────────────────────────
228
- function PluginCard(props) {
229
- return h("li", null,
230
- h("button", { type: "button", className: "__mp_card", onClick: props.onOpen },
231
- h("div", { className: "__mp_cardHead" },
232
- h("span", { className: "__mp_name", title: props.plugin.fullName }, props.plugin.fullName),
233
- h("span", { className: "__mp_star" }, "★ " + props.plugin.stars)
234
- ),
235
- props.plugin.desc ? h("div", { className: "__mp_desc" }, props.plugin.desc) : null,
236
- h("div", { className: "__mp_meta" },
237
- props.plugin.lang ? h("span", null, props.plugin.lang) : null,
238
- h("span", null, props.t("updated") + " " + fmtTime(props.plugin.updated))
239
- )
240
- )
241
- );
242
- }
243
-
244
- function DetailPanel(props) {
245
- var p = props.plugin;
246
- var installName = p.fullName.split("/")[1];
247
- var st = props.installState; // {status, message, pkg} from the settings scope
248
- var active = st && st.pkg === installName;
249
- var status = active ? st.status : "idle";
250
- var explainState = props.explainState; // {status, text, repo} from the settings scope
251
- // The settings field is namespace-global and persists across sessions,
252
- // so only show it when it belongs to the plugin currently open.
253
- var explainMine = !!explainState && explainState.repo === p.fullName;
254
- var [confirming, setConfirming] = react.useState(false);
255
- var statusNode = null;
256
- if (status === "running") {
257
- statusNode = h("div", { className: "__mp_status" }, props.t("installingHint"));
258
- } else if (status === "ok") {
259
- statusNode = h("div", { className: "__mp_status" }, props.t("installOk").replace("{msg}", st.message || ""));
260
- } else if (status === "error") {
261
- statusNode = h("div", { className: "__mp_error" }, props.t("installErr").replace("{msg}", st.message || ""));
262
- }
263
- var btn;
264
- if (status === "running") {
265
- btn = h("button", { type: "button", className: "__mp_more", disabled: true }, props.t("installing"));
266
- } else if (confirming) {
267
- btn = h("button", {
268
- type: "button", className: "__mp_btnPrimary __mp_more",
269
- onClick: function () { setConfirming(false); props.onInstall(installName); }
270
- }, props.t("confirmInstall").replace("{pkg}", installName));
271
- } else {
272
- btn = h("button", {
273
- type: "button", className: "__mp_btnPrimary __mp_more",
274
- onClick: function () { setConfirming(true); }
275
- }, props.t("installBtn"));
276
- }
277
- return h("div", { className: "__mp_detail" },
278
- h("div", { className: "__mp_detailTitle" }, p.fullName),
279
- h("div", { className: "__mp_meta" },
280
- h("span", null, "★ " + p.stars),
281
- h("span", null, props.t("updated") + " " + fmtTime(p.updated)),
282
- props.lang ? h("span", null, p.lang) : null
283
- ),
284
- h("div", null,
285
- h("a", { className: "__mp_link", href: p.htmlUrl, target: "_blank", rel: "noreferrer" }, props.t("openRepo")),
286
- " · ",
287
- h("a", { className: "__mp_link", href: "https://www.npmjs.com/search?q=" + encodeURIComponent(installName), target: "_blank", rel: "noreferrer" }, props.t("openNpm"))
288
- ),
289
- h("div", { className: "__mp_detailTitle" }, props.t("install")),
290
- h("div", { className: "__mp_readme" }, props.bundleInfo === true ? props.t("installHintBundle") : props.t("installHint")),
291
- h("code", { className: "__mp_code" },
292
- "dsh plugin --profile web add " + installName + "\n" +
293
- (props.bundleInfo === true
294
- ? "# standard bundle: auto-mounted via dsh.profile.bundles"
295
- : "# then in $DSH_HOME/profiles/web/cordis.patch.yml:\n" +
296
- "- insert:\n - id: " + installName + "\n name: '" + installName + "'")
297
- ),
298
- btn,
299
- statusNode,
300
- h("div", { className: "__mp_detailTitle" }, props.t("readme")),
301
- h("div", { className: "__mp_readme" },
302
- props.readmeLoading ? props.t("loading")
303
- : props.readmeRateLimited ? props.ghError
304
- : props.readmeError ? props.t("readmeEmpty")
305
- : (props.readme || "")
306
- ),
307
- h("div", { className: "__mp_translateRow" },
308
- h("button", { type: "button", className: "__mp_translateBtn",
309
- style: { marginTop: "8px", padding: "4px 10px", fontSize: "12px", cursor: "pointer", background: "var(--dsw-alias-bg-hover, #21262d)", color: "var(--dsw-alias-label-primary, #e6edf3)", border: "1px solid var(--dsw-alias-border, #30363d)", borderRadius: "6px" },
310
- disabled: explainMine && explainState.status === "running",
311
- onClick: function () { props.onExplain(p.fullName, p.desc, props.readme || ""); }
312
- }, props.t("aiExplain")),
313
- h("div", { id: "__mp_translateOut", style: { fontSize: "12px", lineHeight: "1.6", color: "var(--dsw-alias-label-secondary, #8b949e)", whiteSpace: "pre-wrap", wordBreak: "break-word", marginTop: "8px", borderTop: "1px solid var(--dsw-alias-border, #21262d)", paddingTop: "8px" } },
314
- explainMine && props.explainError ? h("span", { style: { color: "var(--dsw-alias-state-danger-text, #f85149)" } }, props.explainError)
315
- : explainMine && explainState.status === "running" ? props.t("aiExplaining")
316
- : explainMine && explainState.status === "error" ? props.t("aiExplainErr").replace("{msg}", explainState.text || "unknown")
317
- : explainMine && explainState.status === "ok" ? explainState.text
318
- : ""
319
- )
320
- )
321
- );
322
- }
323
-
324
- function MarketplaceSection(props) {
325
- var t = props.t;
326
- var scope = props.scope;
327
- var api = props.api;
328
- 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, bundleInfo: null });
329
- var s = state[0], set = state[1];
330
- // install-state subscription (stable pattern: useState + subscribe, never
331
- // depend on getSnapshot() reference identity).
332
- var [installState, setInstallState] = react.useState(null);
333
- react.useEffect(function () {
334
- var alive = true;
335
- var sync = function () { if (alive) setInstallState(scope.getSnapshot()); };
336
- sync();
337
- var un = typeof scope.subscribe === "function" ? scope.subscribe(sync) : null;
338
- return function () { alive = false; if (un) un(); if (scope.dispose) scope.dispose(); };
339
- }, [scope]);
340
- // Older hosts can still return settings-not-exposed. Recommend upgrading
341
- // instead of mutating the installed DSH package on disk.
342
- var mutateError = function (detail, fallback) {
343
- if (detail && detail.code === "settings-not-exposed") return t("notExposed");
344
- return String(detail && (detail.message || detail.code) || fallback);
345
- };
346
- var onInstall = react.useCallback(function (pkg) {
347
- set(function (prev) { return Object.assign({}, prev, { installError: null }); });
348
- writeField(scope, api, "install", { pkg: pkg, ts: Date.now() }).then(function (result) {
349
- if (!result.ok) {
350
- set(function (prev) { return Object.assign({}, prev, { installError: mutateError(result.error, "unknown") }); });
351
- }
352
- });
353
- }, [scope, api, t]);
354
- // AI-explain request: the host answers over the same settings channel.
355
- var onExplain = react.useCallback(function (repo, desc, readme) {
356
- set(function (prev) { return Object.assign({}, prev, { explainError: null }); });
357
- writeField(scope, api, "aiExplain", { repo: repo, desc: desc, readme: readme, ts: Date.now() }).then(function (result) {
358
- if (!result.ok) {
359
- set(function (prev) { return Object.assign({}, prev, { explainError: mutateError(result.error, "unknown") }); });
360
- }
361
- });
362
- }, [scope, api, t]);
363
- var load = react.useCallback(function (q, sort, page, append) {
364
- set(function (prev) { return Object.assign({}, prev, { loading: true, error: null }); });
365
- fetchPage(q, sort, page).then(function (out) {
366
- set(function (prev) {
367
- var items = append ? prev.items.concat(out.items) : out.items;
368
- return Object.assign({}, prev, { items: items, total: out.total, loading: false, page: page, q: q, sort: sort });
369
- });
370
- }).catch(function () {
371
- set(function (prev) { return Object.assign({}, prev, { loading: false, error: t("ghError") }); });
372
- });
373
- }, [t]);
374
- react.useEffect(function () {
375
- load("", "stars", 1, false);
376
- }, [load]);
377
- var openDetail = react.useCallback(function (plugin) {
378
- if (s.open && s.open.fullName === plugin.fullName) { set(function (prev) { return Object.assign({}, prev, { open: null }); }); return; }
379
- set(function (prev) { return Object.assign({}, prev, { open: plugin, readme: null, readmeError: false, readmeRateLimited: false, readmeLoading: true }); });
380
- // Detail expands inline under the clicked card; scroll minimally
381
- // (nearest) only when it would fall outside the viewport.
382
- setTimeout(function () {
383
- var el = document.querySelector(".__mp_detail");
384
- if (el) el.scrollIntoView({ behavior: "smooth", block: "nearest" });
385
- }, 50);
386
- fetchReadme(plugin.fullName).then(function (text) {
387
- set(function (prev) {
388
- if (!prev.open || prev.open.fullName !== plugin.fullName) return prev;
389
- return Object.assign({}, prev, { readme: text.slice(0, 1200), readmeLoading: false, readmeError: false, readmeRateLimited: false });
390
- });
391
- }).catch(function (err) {
392
- set(function (prev) {
393
- if (!prev.open || prev.open.fullName !== plugin.fullName) return prev;
394
- var rateLimited = err && (err.status === 403 || err.status === 429);
395
- return Object.assign({}, prev, { readmeLoading: false, readmeError: !rateLimited, readmeRateLimited: rateLimited });
396
- });
397
- });
398
- // npm metadata: does the target package declare dsh.bundle.patch?
399
- // (determines whether the manual cordis.patch.yml row is needed)
400
- fetchNpmInfo(plugin.fullName.split("/")[1]).then(function (info) {
401
- set(function (prev) {
402
- if (!prev.open || prev.open.fullName !== plugin.fullName) return prev;
403
- return Object.assign({}, prev, { bundleInfo: info ? Boolean(info.isBundle) : null });
404
- });
405
- });
406
- }, [s.open]);
407
- var submit = function (e) {
408
- e.preventDefault();
409
- load(s.q, s.sort, 1, false);
410
- };
411
- var more = function () { load(s.q, s.sort, s.page + 1, true); };
412
- return h("div", null,
413
- h("form", { className: "__mp_toolbar", onSubmit: submit },
414
- h("input", { className: "__mp_input", type: "search", value: s.q, placeholder: t("search"), onChange: function (e) { set(function (prev) { return Object.assign({}, prev, { q: e.target.value }); }); } }),
415
- h("select", { className: "__mp_select", value: s.sort, onChange: function (e) { load(s.q, e.target.value, 1, false); } },
416
- h("option", { value: "stars" }, t("sortStars")),
417
- h("option", { value: "updated" }, t("sortUpdated"))
418
- )
419
- ),
420
- s.total > 0 ? h("p", { className: "__mp_status" }, t("total").replace("{count}", String(s.total))) : null,
421
- h("ul", { className: "__mp_grid" },
422
- s.items.map(function (p) {
423
- // Detail expands INLINE right under the clicked card, so there is
424
- // no jumping around the page.
425
- var open = s.open && s.open.fullName === p.fullName;
426
- return h("div", { key: p.fullName, className: "__mp_item" },
427
- h(PluginCard, { plugin: p, t: t, onOpen: function () { openDetail(p); } }),
428
- 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"), explainState: installState && installState.status === "ready" ? installState.value.aiExplainResult : null, explainError: s.explainError, onExplain: onExplain, bundleInfo: s.bundleInfo }) : null
429
- );
430
- })
431
- ),
432
- s.loading ? h("p", { className: "__mp_status" }, t("loading")) : null,
433
- s.error ? h("p", { className: "__mp_error" }, s.error) : null,
434
- s.items.length === 0 && !s.loading && !s.error ? h("p", { className: "__mp_status" }, t("empty")) : null,
435
- s.items.length > 0 ? h("button", { type: "button", className: "__mp_more", onClick: more, disabled: s.loading }, t("loadMore")) : null,
436
- s.installError ? h("p", { className: "__mp_error" }, s.installError) : null
437
- );
438
- }
439
-
440
- // ── plugin ────────────────────────────────────────────────────────────
441
- function apply(ctx) {
442
- var t = ctx.locale.bind(NS);
443
- ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-plugin-marketplace: dictionaries");
444
- var scope = ctx.configForms.get("plugin-marketplace");
445
- // dsh 0.1.2-rc.1 dropped connection.api; only older hosts still carry
446
- // the raw settings face there. writeField picks whichever exists.
447
- var api = (ctx.connection && ctx.connection.api) ? ctx.connection.api : null;
448
- ctx.slots.inject("settings.section", function () {
449
- return ctx.slots.register({
450
- name: "settings.section",
451
- id: "marketplace",
452
- order: 20,
453
- label: function () { return t("nav"); },
454
- locale: NS
455
- }, function (props) {
456
- return h(MarketplaceSection, Object.assign({}, props, { scope: scope, api: api }));
457
- });
458
- });
459
- }
460
-
461
- exports.apply = apply;
462
- exports.inject = inject;
463
- return module.exports;
464
- }
465
- });
1
+ /**
2
+ * dsh-plugin-marketplace — browser half.
3
+ *
4
+ * A marketplace section inside the Web UI settings page: browses
5
+ * github.com/topics/dsh-plugin through the public GitHub search API
6
+ * (CORS-enabled), with keyword search, star/update sorting, paging, and a
7
+ * per-plugin detail panel (README summary + install instructions).
8
+ *
9
+ * Hand-written ModuleLoader bundle — no build step required.
10
+ */
11
+ window.__ModuleLoader__.load({
12
+ id: "dsh-plugin-marketplace",
13
+ factory: (require) => {
14
+ var module = { exports: {} };
15
+ var exports = module.exports;
16
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
17
+ var react = require("react");
18
+ var h = react.createElement;
19
+
20
+ // ── CSS (theme tokens) ────────────────────────────────────────────────
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}" +
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}" +
24
+ ".__mp_card:hover{border-color:var(--dsw-alias-brand-primary)}" +
25
+ ".__mp_cardHead{display:flex;align-items:center;gap:8px;min-width:0}" +
26
+ ".__mp_name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}" +
27
+ ".__mp_star{flex:none;font-size:12px;color:var(--dsw-alias-label-secondary)}" +
28
+ ".__mp_desc{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}" +
29
+ ".__mp_meta{display:flex;gap:10px;font-size:11px;color:var(--dsw-alias-label-tertiary)}" +
30
+ ".__mp_toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px}" +
31
+ ".__mp_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;min-width:200px;flex:1}" +
32
+ ".__mp_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 8px;font-size:13px}" +
33
+ ".__mp_more{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 14px;font:inherit;font-size:13px;cursor:pointer;margin-top:12px}" +
34
+ ".__mp_more:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary)}" +
35
+ ".__mp_more:disabled{opacity:.5;cursor:default}" +
36
+ ".__mp_status{font-size:12px;color:var(--dsw-alias-label-tertiary);margin:8px 0 0}" +
37
+ ".__mp_detail{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:14px;margin-top:10px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px;width:100%;box-sizing:border-box}" +
38
+ ".__mp_detailTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}" +
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}" +
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}" +
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}" +
43
+ ".__mp_error{color:var(--dsw-alias-label-error);font-size:12px;margin:8px 0 0}";
44
+ var tagId = "dsh-plugin-marketplace/main.css";
45
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
46
+ var tag = document.createElement("style");
47
+ tag.dataset.plugin = "dsh-plugin-marketplace";
48
+ tag.dataset.pluginCss = tagId;
49
+ tag.textContent = CSS;
50
+ document.head.appendChild(tag);
51
+ }
52
+
53
+ // ── locale ────────────────────────────────────────────────────────────
54
+ var NS = "marketplace";
55
+ var inject = ["slots", "locale", "configForms", "connection"];
56
+ var zh = {
57
+ nav: "插件市场",
58
+ search: "搜索插件(关键词或留空浏览全部)…",
59
+ sortStars: "按 Star 排序",
60
+ sortUpdated: "按更新时间排序",
61
+ loadMore: "加载更多",
62
+ loading: "加载中…",
63
+ error: "加载失败,请稍后重试",
64
+ empty: "没有找到插件",
65
+ install: "安装方式",
66
+ installHint: "在 profile 目录执行 pnpm 安装,然后在 cordis.patch.yml 挂载:",
67
+ installHintBundle: "标准 bundle 插件:dsh plugin add 后由 bundle 层自动挂载,无需手动改 cordis.patch.yml:",
68
+ readme: "README 摘要",
69
+ readmeEmpty: "(该仓库没有 README)",
70
+ openRepo: "打开 GitHub 仓库 ↗",
71
+ openNpm: "在 npm 搜索同名包 ↗",
72
+ updated: "更新",
73
+ total: "共 {count} 个插件",
74
+ ghError: "GitHub API 请求失败(未认证限流 60 次/小时),请稍后再试",
75
+ installBtn: "一键安装",
76
+ confirmInstall: "确认安装 {pkg}?",
77
+ installing: "安装中…",
78
+ installOk: "已安装:{msg}",
79
+ installErr: "安装失败:{msg}",
80
+ installIdle: "",
81
+ installingHint: "正在后台执行 dsh plugin add,请稍候…",
82
+ aiExplain: "🤖 AI 解释",
83
+ aiExplaining: "AI 解释中,请稍候…",
84
+ aiExplainErr: "AI 解释失败:{msg}",
85
+ notExposed: "无法写入插件市场的设置通道:DSH 版本过旧(需 0.1.0-rc.7+)、宿主半端未挂载,或当前页面不是本机回环访问;请通过 127.0.0.1 打开 dsh web 后重试。"
86
+ };
87
+ var en = {
88
+ nav: "Plugin Marketplace",
89
+ search: "Search plugins (keyword, or empty to browse all)…",
90
+ sortStars: "Sort by stars",
91
+ sortUpdated: "Sort by updated",
92
+ loadMore: "Load more",
93
+ loading: "Loading…",
94
+ error: "Failed to load, try again later",
95
+ empty: "No plugins found",
96
+ install: "Install",
97
+ installHint: "Run pnpm in your profile dir, then mount in cordis.patch.yml:",
98
+ installHintBundle: "Standard bundle plugin: dsh plugin add auto-mounts it via its bundle layer — no manual cordis.patch.yml edit needed:",
99
+ readme: "README summary",
100
+ readmeEmpty: "(no README in this repo)",
101
+ openRepo: "Open GitHub repo ↗",
102
+ openNpm: "Search npm ↗",
103
+ updated: "Updated",
104
+ total: "{count} plugins",
105
+ ghError: "GitHub API rate-limited (60/hr unauthenticated), try again later",
106
+ installBtn: "Install",
107
+ confirmInstall: "Install {pkg}?",
108
+ installing: "Installing…",
109
+ installOk: "Installed: {msg}",
110
+ installErr: "Install failed: {msg}",
111
+ installIdle: "",
112
+ installingHint: "Running dsh plugin add in the background…",
113
+ aiExplain: "🤖 AI Explain",
114
+ aiExplaining: "AI is explaining…",
115
+ aiExplainErr: "AI explain failed: {msg}",
116
+ notExposed: "The plugin-marketplace settings channel is not writable here: the DSH version is too old (0.1.0-rc.7+ required), the host half is not mounted, or this page is not a loopback connection. Open dsh web via 127.0.0.1 and try again."
117
+ };
118
+
119
+ // ── GitHub API ────────────────────────────────────────────────────────
120
+ var BASE = "https://api.github.com/search/repositories";
121
+ var cache = new Map(); // queryKey -> {items, incomplete}
122
+ function queryKey(q, sort, page) { return q + "|" + sort + "|" + page; }
123
+ function fmtTime(iso) {
124
+ try { return new Date(iso).toISOString().slice(0, 10); } catch { return ""; }
125
+ }
126
+ async function fetchPage(q, sort, page) {
127
+ var key = queryKey(q, sort, page);
128
+ var hit = cache.get(key);
129
+ if (hit) return hit;
130
+ var params = new URLSearchParams({
131
+ q: "topic:dsh-plugin" + (q ? " " + q : ""),
132
+ sort: sort,
133
+ order: "desc",
134
+ per_page: "20",
135
+ page: String(page)
136
+ });
137
+ var res = await fetch(BASE + "?" + params.toString(), { headers: { Accept: "application/vnd.github+json" } });
138
+ if (!res.ok) throw new Error("HTTP " + res.status);
139
+ var data = await res.json();
140
+ var items = (data.items || []).map(function (r) {
141
+ return {
142
+ fullName: r.full_name,
143
+ desc: r.description || "",
144
+ stars: r.stargazers_count || 0,
145
+ updated: r.updated_at || "",
146
+ lang: r.language || "",
147
+ htmlUrl: r.html_url
148
+ };
149
+ });
150
+ var out = { items: items, total: data.total_count || 0 };
151
+ cache.set(key, out);
152
+ return out;
153
+ }
154
+ async function fetchReadme(fullName) {
155
+ var url = "https://api.github.com/repos/" + fullName + "/readme";
156
+ var res = await fetch(url, { headers: { Accept: "application/vnd.github+json" } });
157
+ if (!res.ok) {
158
+ var e = new Error("readme http " + res.status);
159
+ e.status = res.status;
160
+ throw e;
161
+ }
162
+ var data = await res.json();
163
+ var bin = atob(data.content.replace(/\s+/g, ""));
164
+ var bytes = new Uint8Array(bin.length);
165
+ for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
166
+ return new TextDecoder("utf-8").decode(bytes);
167
+ }
168
+ // npm metadata (bundle detection for the install hint); CORS-enabled, cached.
169
+ var npmCache = new Map(); // pkg -> {isBundle} | null
170
+ async function fetchNpmInfo(pkg) {
171
+ if (npmCache.has(pkg)) return npmCache.get(pkg);
172
+ var promise = fetch("https://registry.npmjs.org/" + encodeURIComponent(pkg) + "/latest", {
173
+ headers: { Accept: "application/vnd.npm.install-v1+json" }
174
+ }).then(function (res) {
175
+ if (!res.ok) return null;
176
+ return res.json();
177
+ }).then(function (data) {
178
+ if (!data || !data.dsh) return { isBundle: false };
179
+ return { isBundle: typeof data.dsh.bundle === "object" && data.dsh.bundle !== null && typeof data.dsh.bundle.patch === "string" && data.dsh.bundle.patch.length > 0 };
180
+ }).catch(function () {
181
+ return null;
182
+ });
183
+ npmCache.set(pkg, promise);
184
+ return promise;
185
+ }
186
+
187
+ // ── settings write channel ────────────────────────────────────────────
188
+ /**
189
+ * Write one namespace field through whichever settings channel this host
190
+ * exposes. dsh 0.1.2-rc.1 dropped `connection.api` (the old raw settings
191
+ * RPC); there the bound scope's own `mutate` (the remote.settings RPC) is
192
+ * the only write path. Older hosts (0.1.0-rc.7 / 0.1.1.x) have no public
193
+ * scope mutate but keep `connection.api`, whose structured error responses
194
+ * the UI can show verbatim. Always resolves `{ok, error?}` — refused or
195
+ * failed writes surface as a result, never as a rejection.
196
+ */
197
+ function writeField(scope, api, field, value) {
198
+ if (scope && typeof scope.mutate === "function") {
199
+ return scope.mutate([{ op: "set", path: [field], value: value }]).then(function () {
200
+ // A settled write is not an applied write: one the Host refused
201
+ // (settings/conflict) still resolves. Compare the section itself —
202
+ // checking only `status === "ready"` passes after a refusal, because
203
+ // the namespace stays registered and the recovery read re-renders it
204
+ // as ready.
205
+ var snap = scope.getSnapshot();
206
+ if (!snap || snap.status !== "ready" || snap.value === void 0) {
207
+ return { ok: false, error: { code: "settings-not-exposed" } };
208
+ }
209
+ if (JSON.stringify(snap.value[field]) !== JSON.stringify(value)) {
210
+ return { ok: false, error: { code: "settings-not-applied" } };
211
+ }
212
+ return { ok: true };
213
+ }, function (e) {
214
+ return { ok: false, error: { message: String(e && e.message || e) } };
215
+ });
216
+ }
217
+ return api.settings.mutate({
218
+ ns: "plugin-marketplace",
219
+ ops: [{ op: "set", path: [field], value: value }]
220
+ }).then(function (response) {
221
+ return (response && response.result) ? response.result : { ok: false, error: { message: "malformed settings response" } };
222
+ }, function (e) {
223
+ return { ok: false, error: { message: String(e && e.message || e) } };
224
+ });
225
+ }
226
+
227
+ // ── components ────────────────────────────────────────────────────────
228
+ function PluginCard(props) {
229
+ return h("li", null,
230
+ h("button", { type: "button", className: "__mp_card", onClick: props.onOpen },
231
+ h("div", { className: "__mp_cardHead" },
232
+ h("span", { className: "__mp_name", title: props.plugin.fullName }, props.plugin.fullName),
233
+ h("span", { className: "__mp_star" }, "★ " + props.plugin.stars)
234
+ ),
235
+ props.plugin.desc ? h("div", { className: "__mp_desc" }, props.plugin.desc) : null,
236
+ h("div", { className: "__mp_meta" },
237
+ props.plugin.lang ? h("span", null, props.plugin.lang) : null,
238
+ h("span", null, props.t("updated") + " " + fmtTime(props.plugin.updated))
239
+ )
240
+ )
241
+ );
242
+ }
243
+
244
+ function DetailPanel(props) {
245
+ var p = props.plugin;
246
+ var installName = p.fullName.split("/")[1];
247
+ var st = props.installState; // {status, message, pkg} from the settings scope
248
+ var active = st && st.pkg === installName;
249
+ var status = active ? st.status : "idle";
250
+ var explainState = props.explainState; // {status, text, repo} from the settings scope
251
+ // The settings field is namespace-global and persists across sessions,
252
+ // so only show it when it belongs to the plugin currently open.
253
+ var explainMine = !!explainState && explainState.repo === p.fullName;
254
+ var [confirming, setConfirming] = react.useState(false);
255
+ var statusNode = null;
256
+ if (status === "running") {
257
+ statusNode = h("div", { className: "__mp_status" }, props.t("installingHint"));
258
+ } else if (status === "ok") {
259
+ statusNode = h("div", { className: "__mp_status" }, props.t("installOk").replace("{msg}", st.message || ""));
260
+ } else if (status === "error") {
261
+ statusNode = h("div", { className: "__mp_error" }, props.t("installErr").replace("{msg}", st.message || ""));
262
+ }
263
+ var btn;
264
+ if (status === "running") {
265
+ btn = h("button", { type: "button", className: "__mp_more", disabled: true }, props.t("installing"));
266
+ } else if (confirming) {
267
+ btn = h("button", {
268
+ type: "button", className: "__mp_btnPrimary __mp_more",
269
+ onClick: function () { setConfirming(false); props.onInstall(installName); }
270
+ }, props.t("confirmInstall").replace("{pkg}", installName));
271
+ } else {
272
+ btn = h("button", {
273
+ type: "button", className: "__mp_btnPrimary __mp_more",
274
+ onClick: function () { setConfirming(true); }
275
+ }, props.t("installBtn"));
276
+ }
277
+ return h("div", { className: "__mp_detail" },
278
+ h("div", { className: "__mp_detailTitle" }, p.fullName),
279
+ h("div", { className: "__mp_meta" },
280
+ h("span", null, "★ " + p.stars),
281
+ h("span", null, props.t("updated") + " " + fmtTime(p.updated)),
282
+ props.lang ? h("span", null, p.lang) : null
283
+ ),
284
+ h("div", null,
285
+ h("a", { className: "__mp_link", href: p.htmlUrl, target: "_blank", rel: "noreferrer" }, props.t("openRepo")),
286
+ " · ",
287
+ h("a", { className: "__mp_link", href: "https://www.npmjs.com/search?q=" + encodeURIComponent(installName), target: "_blank", rel: "noreferrer" }, props.t("openNpm"))
288
+ ),
289
+ h("div", { className: "__mp_detailTitle" }, props.t("install")),
290
+ h("div", { className: "__mp_readme" }, props.bundleInfo === true ? props.t("installHintBundle") : props.t("installHint")),
291
+ h("code", { className: "__mp_code" },
292
+ "dsh plugin --profile web add " + installName + "\n" +
293
+ (props.bundleInfo === true
294
+ ? "# standard bundle: auto-mounted via dsh.profile.bundles"
295
+ : "# then in $DSH_HOME/profiles/web/cordis.patch.yml:\n" +
296
+ "- insert:\n - id: " + installName + "\n name: '" + installName + "'")
297
+ ),
298
+ btn,
299
+ statusNode,
300
+ h("div", { className: "__mp_detailTitle" }, props.t("readme")),
301
+ h("div", { className: "__mp_readme" },
302
+ props.readmeLoading ? props.t("loading")
303
+ : props.readmeRateLimited ? props.ghError
304
+ : props.readmeError ? props.t("readmeEmpty")
305
+ : (props.readme || "")
306
+ ),
307
+ h("div", { className: "__mp_translateRow" },
308
+ h("button", { type: "button", className: "__mp_translateBtn",
309
+ style: { marginTop: "8px", padding: "4px 10px", fontSize: "12px", cursor: "pointer", background: "var(--dsw-alias-bg-hover, #21262d)", color: "var(--dsw-alias-label-primary, #e6edf3)", border: "1px solid var(--dsw-alias-border, #30363d)", borderRadius: "6px" },
310
+ disabled: explainMine && explainState.status === "running",
311
+ onClick: function () { props.onExplain(p.fullName, p.desc, props.readme || ""); }
312
+ }, props.t("aiExplain")),
313
+ h("div", { id: "__mp_translateOut", style: { fontSize: "12px", lineHeight: "1.6", color: "var(--dsw-alias-label-secondary, #8b949e)", whiteSpace: "pre-wrap", wordBreak: "break-word", marginTop: "8px", borderTop: "1px solid var(--dsw-alias-border, #21262d)", paddingTop: "8px" } },
314
+ explainMine && props.explainError ? h("span", { style: { color: "var(--dsw-alias-state-danger-text, #f85149)" } }, props.explainError)
315
+ : explainMine && explainState.status === "running" ? props.t("aiExplaining")
316
+ : explainMine && explainState.status === "error" ? props.t("aiExplainErr").replace("{msg}", explainState.text || "unknown")
317
+ : explainMine && explainState.status === "ok" ? explainState.text
318
+ : ""
319
+ )
320
+ )
321
+ );
322
+ }
323
+
324
+ function MarketplaceSection(props) {
325
+ var t = props.t;
326
+ var scope = props.scope;
327
+ var api = props.api;
328
+ 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, bundleInfo: null });
329
+ var s = state[0], set = state[1];
330
+ // install-state subscription (stable pattern: useState + subscribe, never
331
+ // depend on getSnapshot() reference identity).
332
+ var [installState, setInstallState] = react.useState(null);
333
+ react.useEffect(function () {
334
+ var alive = true;
335
+ var sync = function () { if (alive) setInstallState(scope.getSnapshot()); };
336
+ sync();
337
+ var un = typeof scope.subscribe === "function" ? scope.subscribe(sync) : null;
338
+ return function () { alive = false; if (un) un(); if (scope.dispose) scope.dispose(); };
339
+ }, [scope]);
340
+ // Older hosts can still return settings-not-exposed. Recommend upgrading
341
+ // instead of mutating the installed DSH package on disk.
342
+ var mutateError = function (detail, fallback) {
343
+ if (detail && detail.code === "settings-not-exposed") return t("notExposed");
344
+ return String(detail && (detail.message || detail.code) || fallback);
345
+ };
346
+ var onInstall = react.useCallback(function (pkg) {
347
+ set(function (prev) { return Object.assign({}, prev, { installError: null }); });
348
+ writeField(scope, api, "install", { pkg: pkg, ts: Date.now() }).then(function (result) {
349
+ if (!result.ok) {
350
+ set(function (prev) { return Object.assign({}, prev, { installError: mutateError(result.error, "unknown") }); });
351
+ }
352
+ });
353
+ }, [scope, api, t]);
354
+ // AI-explain request: the host answers over the same settings channel.
355
+ var onExplain = react.useCallback(function (repo, desc, readme) {
356
+ set(function (prev) { return Object.assign({}, prev, { explainError: null }); });
357
+ writeField(scope, api, "aiExplain", { repo: repo, desc: desc, readme: readme, ts: Date.now() }).then(function (result) {
358
+ if (!result.ok) {
359
+ set(function (prev) { return Object.assign({}, prev, { explainError: mutateError(result.error, "unknown") }); });
360
+ }
361
+ });
362
+ }, [scope, api, t]);
363
+ var load = react.useCallback(function (q, sort, page, append) {
364
+ set(function (prev) { return Object.assign({}, prev, { loading: true, error: null }); });
365
+ fetchPage(q, sort, page).then(function (out) {
366
+ set(function (prev) {
367
+ var items = append ? prev.items.concat(out.items) : out.items;
368
+ return Object.assign({}, prev, { items: items, total: out.total, loading: false, page: page, q: q, sort: sort });
369
+ });
370
+ }).catch(function () {
371
+ set(function (prev) { return Object.assign({}, prev, { loading: false, error: t("ghError") }); });
372
+ });
373
+ }, [t]);
374
+ react.useEffect(function () {
375
+ load("", "stars", 1, false);
376
+ }, [load]);
377
+ var openDetail = react.useCallback(function (plugin) {
378
+ if (s.open && s.open.fullName === plugin.fullName) { set(function (prev) { return Object.assign({}, prev, { open: null }); }); return; }
379
+ set(function (prev) { return Object.assign({}, prev, { open: plugin, readme: null, readmeError: false, readmeRateLimited: false, readmeLoading: true }); });
380
+ // Detail expands inline under the clicked card; scroll minimally
381
+ // (nearest) only when it would fall outside the viewport.
382
+ setTimeout(function () {
383
+ var el = document.querySelector(".__mp_detail");
384
+ if (el) el.scrollIntoView({ behavior: "smooth", block: "nearest" });
385
+ }, 50);
386
+ fetchReadme(plugin.fullName).then(function (text) {
387
+ set(function (prev) {
388
+ if (!prev.open || prev.open.fullName !== plugin.fullName) return prev;
389
+ return Object.assign({}, prev, { readme: text.slice(0, 1200), readmeLoading: false, readmeError: false, readmeRateLimited: false });
390
+ });
391
+ }).catch(function (err) {
392
+ set(function (prev) {
393
+ if (!prev.open || prev.open.fullName !== plugin.fullName) return prev;
394
+ var rateLimited = err && (err.status === 403 || err.status === 429);
395
+ return Object.assign({}, prev, { readmeLoading: false, readmeError: !rateLimited, readmeRateLimited: rateLimited });
396
+ });
397
+ });
398
+ // npm metadata: does the target package declare dsh.bundle.patch?
399
+ // (determines whether the manual cordis.patch.yml row is needed)
400
+ fetchNpmInfo(plugin.fullName.split("/")[1]).then(function (info) {
401
+ set(function (prev) {
402
+ if (!prev.open || prev.open.fullName !== plugin.fullName) return prev;
403
+ return Object.assign({}, prev, { bundleInfo: info ? Boolean(info.isBundle) : null });
404
+ });
405
+ });
406
+ }, [s.open]);
407
+ var submit = function (e) {
408
+ e.preventDefault();
409
+ load(s.q, s.sort, 1, false);
410
+ };
411
+ var more = function () { load(s.q, s.sort, s.page + 1, true); };
412
+ return h("div", null,
413
+ h("form", { className: "__mp_toolbar", onSubmit: submit },
414
+ h("input", { className: "__mp_input", type: "search", value: s.q, placeholder: t("search"), onChange: function (e) { set(function (prev) { return Object.assign({}, prev, { q: e.target.value }); }); } }),
415
+ h("select", { className: "__mp_select", value: s.sort, onChange: function (e) { load(s.q, e.target.value, 1, false); } },
416
+ h("option", { value: "stars" }, t("sortStars")),
417
+ h("option", { value: "updated" }, t("sortUpdated"))
418
+ )
419
+ ),
420
+ s.total > 0 ? h("p", { className: "__mp_status" }, t("total").replace("{count}", String(s.total))) : null,
421
+ h("ul", { className: "__mp_grid" },
422
+ s.items.map(function (p) {
423
+ // Detail expands INLINE right under the clicked card, so there is
424
+ // no jumping around the page.
425
+ var open = s.open && s.open.fullName === p.fullName;
426
+ return h("div", { key: p.fullName, className: "__mp_item" },
427
+ h(PluginCard, { plugin: p, t: t, onOpen: function () { openDetail(p); } }),
428
+ 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"), explainState: installState && installState.status === "ready" ? installState.value.aiExplainResult : null, explainError: s.explainError, onExplain: onExplain, bundleInfo: s.bundleInfo }) : null
429
+ );
430
+ })
431
+ ),
432
+ s.loading ? h("p", { className: "__mp_status" }, t("loading")) : null,
433
+ s.error ? h("p", { className: "__mp_error" }, s.error) : null,
434
+ s.items.length === 0 && !s.loading && !s.error ? h("p", { className: "__mp_status" }, t("empty")) : null,
435
+ s.items.length > 0 ? h("button", { type: "button", className: "__mp_more", onClick: more, disabled: s.loading }, t("loadMore")) : null,
436
+ s.installError ? h("p", { className: "__mp_error" }, s.installError) : null
437
+ );
438
+ }
439
+
440
+ // ── plugin ────────────────────────────────────────────────────────────
441
+ function apply(ctx) {
442
+ var t = ctx.locale.bind(NS);
443
+ ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-plugin-marketplace: dictionaries");
444
+ var scope = ctx.configForms.get("plugin-marketplace");
445
+ // dsh 0.1.2-rc.1 dropped connection.api; only older hosts still carry
446
+ // the raw settings face there. writeField picks whichever exists.
447
+ var api = (ctx.connection && ctx.connection.api) ? ctx.connection.api : null;
448
+ ctx.slots.inject("plugins.bundle.config", function () {
449
+ return ctx.slots.register({
450
+ name: "plugins.bundle.config",
451
+ key: "dsh-plugin-marketplace",
452
+ locale: NS
453
+ }, function (props) {
454
+ return h(MarketplaceSection, Object.assign({}, props, { scope: scope, api: api }));
455
+ });
456
+ });
457
+ }
458
+
459
+ exports.apply = apply;
460
+ exports.inject = inject;
461
+ return module.exports;
462
+ }
463
+ });
package/index.js CHANGED
@@ -32,11 +32,11 @@ const Config = z.object({
32
32
  /** client-set timestamp; host dedups on it. */
33
33
  ts: z.number().default(0),
34
34
  }),
35
- installState: z.object({
36
- status: z.string().default("idle"), // idle | running | ok | error
37
- message: z.string().default(""),
38
- pkg: z.string().default(""),
39
- ts: z.number().default(0),
35
+ installState: z.object({
36
+ status: z.string().default("idle"), // idle | running | ok | error
37
+ message: z.string().default(""),
38
+ pkg: z.string().default(""),
39
+ ts: z.number().default(0),
40
40
  }),
41
41
  /** AI-explain request (client writes repo/desc/readme, host answers). */
42
42
  aiExplain: z.object({
@@ -52,13 +52,13 @@ const Config = z.object({
52
52
  repo: z.string().default(""),
53
53
  ts: z.number().default(0),
54
54
  }),
55
- }).volatile();
55
+ }).volatile();
56
56
 
57
57
  /** npm package-name shape (scope/name, no spaces, no path chars). */
58
58
  const PKG_NAME = /^@?[a-z0-9][a-z0-9-._]*(?:\/[a-z0-9][a-z0-9-._]*)?$/;
59
59
 
60
- function apply(ctx, config) {
61
- const sourceGetter = () => typeof config.get === "function" ? config.get() : config;
60
+ function apply(ctx, config) {
61
+ const sourceGetter = () => typeof config.get === "function" ? config.get() : config;
62
62
  let lastTs = 0;
63
63
  let lastExplainTs = 0;
64
64
 
@@ -70,23 +70,23 @@ function apply(ctx, config) {
70
70
  // `installSettingsSection` export (the provider now lives at ctx.settings).
71
71
  // Inline the same logic via ctx.inject(["settings"]) — works on both
72
72
  // 0.1.1 (module export wrapper) and 0.1.2 (ctx.settings) hosts.
73
- // DSH 0.1.7 stores live plugin fields in the profile patch. Config is a
74
- // volatile proxy, and the settings service emits after a committed edit.
75
- ctx.on("settings/document-updated", (id) => {
76
- if (id !== NS) return;
77
- void maybeRunInstall();
78
- void maybeRunExplain();
79
- });
80
- queueMicrotask(() => {
81
- void maybeRunInstall();
82
- void maybeRunExplain();
83
- });
73
+ // DSH 0.1.7 stores live plugin fields in the profile patch. Config is a
74
+ // volatile proxy, and the settings service emits after a committed edit.
75
+ ctx.on("settings/document-updated", (id) => {
76
+ if (id !== NS) return;
77
+ void maybeRunInstall();
78
+ void maybeRunExplain();
79
+ });
80
+ queueMicrotask(() => {
81
+ void maybeRunInstall();
82
+ void maybeRunExplain();
83
+ });
84
84
 
85
85
  // DSH 0.1.0-rc.7+ exposes registered settings namespaces natively, allowing
86
86
  // the browser client to write install requests without modifying host files.
87
87
  /** Resolved current config (settings layer over the entry). */
88
88
  function current() {
89
- return sourceGetter();
89
+ return sourceGetter();
90
90
  }
91
91
 
92
92
  /** Write the install-state report (and clear the consumed request). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-marketplace",
3
- "version": "0.3.7",
3
+ "version": "0.4.0",
4
4
  "description": "DeepSeek Harness Web UI 内置插件市场:在设置页直接浏览 github.com/topics/dsh-plugin,支持搜索、按 Star 排序、查看安装方式。",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -17,7 +17,7 @@
17
17
  "platform": "web"
18
18
  },
19
19
  "compatibility": {
20
- "dsh": ">=0.1.7-rc.1 <0.2.0 || 0.2.0-rc.2",
20
+ "dsh": ">=0.2.0-rc.2 <0.3.0",
21
21
  "dshReleases": {
22
22
  "0.1.5-rc.2": "incompatible",
23
23
  "0.1.5-rc.3": "incompatible",
@@ -25,8 +25,8 @@
25
25
  "0.1.6-alpha.2": "unknown",
26
26
  "0.1.7-alpha.1": "unknown",
27
27
  "0.1.7-alpha.2": "unknown",
28
- "0.1.7-rc.1": "compatible",
29
- "0.1.7-rc.2": "compatible",
28
+ "0.1.7-rc.1": "incompatible",
29
+ "0.1.7-rc.2": "incompatible",
30
30
  "0.2.0-rc.2": "compatible"
31
31
  },
32
32
  "node": ">=20.18",
@@ -37,7 +37,7 @@
37
37
  }
38
38
  },
39
39
  "scripts": {
40
- "test": "node --test tests/settings-write-verify.test.mjs"
40
+ "test": "node --test tests/settings-write-verify.test.mjs && node --test tests/plugin-page.test.mjs"
41
41
  },
42
42
  "files": [
43
43
  "index.js",
@@ -69,8 +69,8 @@
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "^18.2.0",
72
- "@deepseek-ai/dsh-settings": "^0.1.7-rc.1 || 0.2.0-rc.2",
73
- "@deepseek-ai/dsh-llm": "^0.1.7-rc.1 || 0.2.0-rc.2"
72
+ "@deepseek-ai/dsh-settings": ">=0.2.0-rc.2 <0.3.0",
73
+ "@deepseek-ai/dsh-llm": ">=0.2.0-rc.2 <0.3.0"
74
74
  },
75
75
  "dependencies": {
76
76
  "@deepseek-ai/schemastery": "^3.18.4"