dsh-plugin 1.1.0 → 1.1.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.
Files changed (64) hide show
  1. package/README.en.md +5 -5
  2. package/README.md +5 -5
  3. package/client/client.js +648 -190
  4. package/client/client.js.map +1 -1
  5. package/lib/http/routes.js +15 -8
  6. package/lib/index.js +4 -0
  7. package/lib/services/install-types.js +8 -2
  8. package/lib/services/install.js +1 -1
  9. package/lib/services/installed-versions.js +5 -1
  10. package/lib/services/loader.js +16 -5
  11. package/lib/services/npm-resolve.js +4 -0
  12. package/lib/services/pending-restart.js +1 -1
  13. package/lib/services/preflight.js +9 -4
  14. package/lib/services/profile.js +30 -3
  15. package/lib/services/progress.js +4 -0
  16. package/lib/services/task-queue.js +21 -10
  17. package/lib/types/index.d.ts +4 -0
  18. package/lib/types/services/install-types.d.ts +12 -4
  19. package/lib/types/services/install.d.ts +5 -1
  20. package/lib/types/services/loader.d.ts +10 -4
  21. package/lib/types/services/pending-restart.d.ts +8 -2
  22. package/lib/types/services/profile.d.ts +4 -2
  23. package/lib/types/services/progress.d.ts +4 -0
  24. package/package.json +1 -1
  25. package/src/client/components/AboutModal.tsx +76 -0
  26. package/src/client/components/CatalogControls.tsx +4 -0
  27. package/src/client/components/CatalogHeader.tsx +34 -5
  28. package/src/client/components/CatalogList.tsx +4 -0
  29. package/src/client/components/CategoryTabs.tsx +4 -0
  30. package/src/client/components/Dropdown.tsx +4 -0
  31. package/src/client/components/HubUpdateModal.tsx +88 -0
  32. package/src/client/components/NotificationsModal.tsx +25 -8
  33. package/src/client/components/PluginCard.tsx +8 -1
  34. package/src/client/components/PluginHubSection.tsx +67 -7
  35. package/src/client/components/ProgressView.tsx +4 -0
  36. package/src/client/components/icons.tsx +4 -0
  37. package/src/client/components/modals.tsx +35 -10
  38. package/src/client/hooks/useCatalog.ts +89 -17
  39. package/src/client/hooks/useTaskQueue.ts +52 -35
  40. package/src/client/index.tsx +4 -0
  41. package/src/client/lib/catalog.ts +59 -6
  42. package/src/client/lib/env.ts +4 -0
  43. package/src/client/lib/failures.ts +5 -1
  44. package/src/client/lib/format.ts +4 -0
  45. package/src/client/lib/renderMarkdown.ts +120 -0
  46. package/src/client/locales.ts +48 -1
  47. package/src/client/styles/Dropdown.module.css +5 -1
  48. package/src/client/styles/Header.module.css +88 -4
  49. package/src/client/styles/List.module.css +5 -1
  50. package/src/client/styles/Modal.module.css +279 -1
  51. package/src/client/styles/tokens.module.css +5 -1
  52. package/src/client/types.ts +7 -1
  53. package/src/server/http/routes.ts +14 -8
  54. package/src/server/index.ts +4 -0
  55. package/src/server/services/install-types.ts +12 -4
  56. package/src/server/services/install.ts +5 -1
  57. package/src/server/services/installed-versions.ts +5 -1
  58. package/src/server/services/loader.ts +16 -5
  59. package/src/server/services/npm-resolve.ts +4 -0
  60. package/src/server/services/pending-restart.ts +8 -2
  61. package/src/server/services/preflight.ts +9 -4
  62. package/src/server/services/profile.ts +27 -3
  63. package/src/server/services/progress.ts +4 -0
  64. package/src/server/services/task-queue.ts +21 -10
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * Local HTTP routes exposing real installs to the in-app Plugin Hub UI.
3
7
  * The client fetches the same-origin `/dsh-plugin-hub/*` endpoints; the
4
8
  * install handler validates the target, then spawns the official dsh CLI
@@ -8,7 +12,7 @@ import { readFileSync } from 'node:fs';
8
12
  import { homedir, release } from 'node:os';
9
13
  import { dirname, join, resolve } from 'node:path';
10
14
  import { spawn } from 'node:child_process';
11
- import { activeTask, cancelTask, dumpLoaderEntries, getTask, githubTarget, hasQueuedTarget, listPendingRestarts, readProfileArg, startPluginMutation, validPackageName } from '../services/install.js';
15
+ import { activeTask, cancelTask, dumpLoaderEntries, getTask, githubRepoOf, githubTarget, hasQueuedTarget, listPendingRestarts, readProfileArg, startPluginMutation, validPackageName } from '../services/install.js';
12
16
  import { recordInstalledVersion, recordResolvedNpmPackage, readInstalledVersions, removeInstalledVersion } from '../services/installed-versions.js';
13
17
  import { resolveNpmPackage } from '../services/npm-resolve.js';
14
18
  import { preflightTarget } from '../services/preflight.js';
@@ -64,11 +68,14 @@ function hostEnv(profile) {
64
68
  dshHome: process.env.DSH_HOME ?? join(homedir(), '.dsh'),
65
69
  };
66
70
  }
67
- /** Match an install target (`github:<owner>/<repo>`) against the installed table; returns the package name when already installed. */
71
+ /** Match a GitHub install target against the installed table; returns the package name when already installed. */
68
72
  function installedByRepo(installed, target) {
69
- const needle = target.toLowerCase();
73
+ const repo = githubRepoOf(target);
74
+ if (repo === null)
75
+ return null;
76
+ const needle = repo.toLowerCase();
70
77
  for (const [name, spec] of Object.entries(installed)) {
71
- if (spec.toLowerCase().includes(needle))
78
+ if (githubRepoOf(spec)?.toLowerCase() === needle)
72
79
  return name;
73
80
  }
74
81
  return null;
@@ -164,7 +171,7 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
164
171
  const displayRepo = typeof body === 'object' && body !== null && typeof body.display === 'string'
165
172
  ? body.display.trim()
166
173
  : rawRepo;
167
- // 目标语法两态:owner/repo → github 源(走装前预检);npm 包名(@scope/name 或 name)→ 信任 registry 直接安装
174
+ // 目标语法两态:owner/repo → 显式 HTTPS Git 源(走装前预检);npm 包名(@scope/name 或 name)→ 信任 registry 直接安装
168
175
  const repoTarget = githubTarget(rawRepo);
169
176
  let target = repoTarget ?? (repoTarget === null && validPackageName(rawRepo) ? rawRepo : null);
170
177
  if (target === null) {
@@ -195,7 +202,7 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
195
202
  // 重复安装防护:非更新请求命中已安装目标时直接拒绝,不重复跑 CLI
196
203
  // (否则 CLI 会因「已存在」失败,且用户看到的是莫名其妙的报错);
197
204
  // 更新请求(mode: 'update')放行,CLI 的 add 对已存在依赖是 pnpm 原位覆盖重装。
198
- // 命中判定双通道:npm 包名按 dependencies 键直接命中,github: 目标按 spec 值匹配。
205
+ // 命中判定双通道:npm 包名按 dependencies 键直接命中,GitHub 目标按仓库身份匹配。
199
206
  const installed = readInstalled(profile);
200
207
  const already = installedByRepo(installed, target) ?? (installed[target] !== undefined ? target : null);
201
208
  if (already !== null && mode !== 'update') {
@@ -236,7 +243,7 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
236
243
  profile,
237
244
  target,
238
245
  // npm 安装时 target 是包名:displayTarget 固定展示仓库名,待重启行/前端恢复不受通道变化影响
239
- displayTarget: githubTarget(displayRepo) ?? undefined,
246
+ displayTarget: githubRepoOf(displayRepo) ?? undefined,
240
247
  // 已尝试的安装方式(npm 反查):实际执行命令由 spawn 时追加,失败 issue 一并展示
241
248
  attempts,
242
249
  timeoutMs: COMMAND_TIMEOUT_MS,
@@ -283,7 +290,7 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
283
290
  profile,
284
291
  target: name,
285
292
  // 卸载的 mutation 目标是 npm 包名;待重启行要显示 owner/repo(与安装行一致),非法/缺失时回退包名
286
- displayTarget: githubTarget(repo) ?? undefined,
293
+ displayTarget: githubRepoOf(repo) ?? undefined,
287
294
  // 运行中 loader:卸载成功后主动移除条目,立即生效、无需重启
288
295
  uninstallLoader: loader,
289
296
  timeoutMs: COMMAND_TIMEOUT_MS,
package/lib/index.js CHANGED
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * dsh-plugin host entry: mounts the Plugin Hub HTTP routes once the host
3
7
  * web server service is available. The browser bundle (src/client) talks to
4
8
  * these same-origin routes to perform real installs from inside the app.
@@ -1,5 +1,11 @@
1
- /** 安装/卸载运行时的公共类型与常量(无依赖,供各服务模块复用)。 */
2
- /** Grammar of an accepted `github:<owner>/<repo>` install target. */
1
+ /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * 安装/卸载运行时的公共类型与常量(无依赖,供各服务模块复用)。
7
+ */
8
+ /** Grammar of a catalog GitHub repository identity (`owner/repo`). */
3
9
  export const REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
4
10
  /** Grammar of an npm package name (used for uninstall targets). */
5
11
  export const PACKAGE_RE = /^(?:@[a-z0-9._-]+\/)?[A-Za-z0-9._-]+$/;
@@ -1,4 +1,4 @@
1
1
  export { addPendingRestart, clearPendingRestart, listPendingRestarts } from './pending-restart.js';
2
2
  export { dumpLoaderEntries, removeLoadedEntry } from './loader.js';
3
- export { addAllowBuildsKey, githubTarget, parseAllowBuildsKey, readProfileArg, validPackageName } from './profile.js';
3
+ export { addAllowBuildsKey, githubRepoOf, githubTarget, parseAllowBuildsKey, readProfileArg, validPackageName } from './profile.js';
4
4
  export { activeTask, cancelTask, getTask, hasQueuedTarget, hasRunningTask, runPluginMutation, startPluginMutation } from './task-queue.js';
@@ -1,8 +1,12 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * Installed-plugin version tracking.
3
7
  *
4
8
  * The installed table (`readInstalled` in routes.ts) only knows the manifest
5
- * spec of each dependency (`github:<owner>/<repo>`), which carries no version.
9
+ * spec of each Git dependency, which carries no version.
6
10
  * To let the Plugin Hub tell "an update is available", we record the catalog
7
11
  * signal present at install time into a small per-profile JSON file, then
8
12
  * compare it against the catalog's current signal later.
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * 运行中 loader 的读取与停用:卸载成功后对匹配条目做 live-disable,
3
7
  * 使卸载立即生效(刷新页面不再加载已卸载的 client bundle,也无需重启)。
4
8
  */
@@ -44,13 +48,15 @@ function nameMatches(entryName, target) {
44
48
  return false;
45
49
  }
46
50
  /**
47
- * 卸载成功后从运行中 loader 停用指定包的条目,使卸载立即生效(刷新页面不再
48
- * 加载已卸载的 client bundle,也无需重启)。做法:按 name 扫描
51
+ * 卸载成功后从运行中 loader 停用指定包的条目。做法:按 name 扫描
49
52
  * `loader.entries()`(含嵌套子树),对每个匹配条目
50
53
  * `entry.update({ disabled: true })` 做 live-disable —— fiber 被 dispose,
51
54
  * client-modules 对账后不再把它写进 `__DSH_BOOT__`,页面刷新即恢复。
52
- * 返回 true = 无需重启(条目已停用,或本就没加载过);false = 未能停用,
53
- * 需登记「待重启清理」兜底。宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
55
+ * 返回 true = 无需重启(仅当宿主从未加载过它:磁盘已干净);
56
+ * 其余一律 false = 需要重启:要么 disable 失败需「待重启清理」兜底,
57
+ * 要么 disable 成功但该插件曾在 loader 中存活——带 UI 的插件(侧边栏面板等
58
+ * 宿主启动时渲染的槽位)disable 后不会主动摘除,不重启面板一直残留。
59
+ * 宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
54
60
  */
55
61
  export async function removeLoadedEntry(loader, name) {
56
62
  if (name === 'dsh-plugin' || name.startsWith('@deepseek-ai/'))
@@ -83,8 +89,13 @@ export async function removeLoadedEntry(loader, name) {
83
89
  if (!found) {
84
90
  // 运行中 loader 无该条目:宿主从未加载过它(装完未重启),磁盘已干净,无需重启
85
91
  console.log(`[hub-uninstall] no live entry for ${name}; nothing to disable`);
92
+ return true;
86
93
  }
87
- return true;
94
+ // 找到并停用了 live entry:逻辑层已卸载(刷新页面不再加载其 client bundle),
95
+ // 但带 UI 的插件(侧边栏面板等)是宿主启动时渲染的槽位,disable 不会主动摘除,
96
+ // 不重启面板会一直残留 → 返回「需要重启」,弹窗给出「立即重启」,面板立即消失
97
+ console.log(`[hub-uninstall] disabled live entry for ${name}; UI may persist until restart`);
98
+ return false;
88
99
  })();
89
100
  const result = await Promise.race([
90
101
  removal.catch((error) => {
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * npm 包反查:git 分发不完整(缺构建产物/子模块)导致安装失败时,用 npm
3
7
  * registry 的搜索接口按 repository 地址反查该仓库对应的官方 npm 包。
4
8
  *
@@ -2,7 +2,7 @@ const pendingRestarts = [];
2
2
  /**
3
3
  * 登记一条待重启记录(同一目标去重)。
4
4
  * - 卸载:目标刚装完未重启(已有待重启记录)→ 宿主从未加载过它,卸完磁盘已干净、
5
- * loader 无残留,无需再提醒重启,直接移除该条;否则(此前已被宿主加载过)登记「卸载后待重启清理」。
5
+ * loader 无残留,无需再提醒重启,直接移除该条;否则(已被宿主加载过)登记「卸载后待重启清理」。
6
6
  * - 安装:已有记录则刷新为安装语义(如卸载后未重启又重装),否则新登记。
7
7
  */
8
8
  export function addPendingRestart(target, kind) {
@@ -1,5 +1,9 @@
1
1
  /**
2
- * 安装前预检:对 `github:` 源的插件,检查其分发(codeload tarball)里是否
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * 安装前预检:对 GitHub 源的插件,检查其分发(codeload tarball)里是否
3
7
  * 真的包含 package.json 声明的入口文件(main / exports["."].default)。
4
8
  * git 分发常不提交构建产物(lib/ 等),这类残缺包装完会让宿主重启加载插件树时
5
9
  * ERR_MODULE_NOT_FOUND 直接崩溃 —— 预检在动手安装前就把它们拦下,避免白装一次。
@@ -13,12 +17,13 @@ import { tmpdir } from 'node:os';
13
17
  import { join } from 'node:path';
14
18
  import { pipeline } from 'node:stream/promises';
15
19
  import { promisify } from 'node:util';
20
+ import { githubRepoOf } from './profile.js';
16
21
  const execFileAsync = promisify(execFile);
17
22
  export async function preflightTarget(target) {
18
- const m = /^github:([^/]+)\/([^/]+)$/.exec(target);
19
- if (m === null)
23
+ const source = githubRepoOf(target);
24
+ if (source === null)
20
25
  return { ok: true, missing: null };
21
- const [, owner, repo] = m;
26
+ const [owner, repo] = source.split('/');
22
27
  let commit = '';
23
28
  try {
24
29
  const { stdout } = await execFileAsync('git', ['ls-remote', `https://github.com/${owner}/${repo}.git`, 'HEAD']);
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * 宿主 profile 与目标校验工具:解析当前 profile、目录定位、安装目标语法校验,
3
7
  * 以及 pnpm allowBuilds 白名单的写入(失败恢复路径)。
4
8
  */
@@ -16,11 +20,34 @@ export function readProfileArg(fallback = 'web') {
16
20
  export function profileDirectory(profile) {
17
21
  return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile);
18
22
  }
19
- /** Build a safe `github:<owner>/<repo>` target, or null when the repo is unsafe. */
23
+ /** Build a safe explicit HTTPS Git target, or null when the repo is unsafe. */
20
24
  export function githubTarget(repo) {
21
25
  if (typeof repo !== 'string' || !REPO_RE.test(repo))
22
26
  return null;
23
- return `github:${repo}`;
27
+ return `git+https://github.com/${repo}.git`;
28
+ }
29
+ /** Extract an owner/repo identity from a catalog value or an installed Git spec. */
30
+ export function githubRepoOf(value) {
31
+ if (typeof value !== 'string')
32
+ return null;
33
+ const input = value.trim();
34
+ if (REPO_RE.test(input))
35
+ return input;
36
+ const patterns = [
37
+ /^github:([^/]+)\/([^/]+)$/i,
38
+ /^(?:git\+)?https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
39
+ /^(?:git\+)?ssh:\/\/(?:git@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
40
+ /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
41
+ ];
42
+ for (const pattern of patterns) {
43
+ const match = pattern.exec(input);
44
+ if (match === null)
45
+ continue;
46
+ const repo = `${match[1]}/${match[2]}`;
47
+ if (REPO_RE.test(repo))
48
+ return repo;
49
+ }
50
+ return null;
24
51
  }
25
52
  /** Validate an npm package name (uninstall target grammar). */
26
53
  export function validPackageName(name) {
@@ -28,7 +55,7 @@ export function validPackageName(name) {
28
55
  }
29
56
  /**
30
57
  * The exact `allowBuilds` key pnpm printed in its
31
- * `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` hint. For `github:` installs pnpm
58
+ * `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` hint. For Git installs pnpm
32
59
  * matches against `name@<resolved fetch URL>` — the URL it actually fetches,
33
60
  * e.g. `https://codeload.github.com/<owner>/<repo>/tar.gz/<commit>` — not the
34
61
  * `git+ssh://`/branch spec. A bare name or a `git+` spec does not match, so
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * Pure progress-estimation helpers shared by the install runner.
3
7
  *
4
8
  * The dsh CLI wraps pnpm, which refreshes its `Progress: …` line in place
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * 后台安装/卸载任务队列:内存任务注册表 + FIFO 串行 worker + 子进程生命周期管理。
3
7
  * 每个任务一次 `dsh plugin --profile <profile> <action> <target>` 子进程,
4
8
  * 输出流式收集到任务上;卸载成功时尝试从运行中 loader 即时停用(见 loader.ts)。
@@ -10,7 +14,7 @@ import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.
10
14
  import { cleanLine, estimateProgress } from './progress.js';
11
15
  import { removeLoadedEntry } from './loader.js';
12
16
  import { addPendingRestart, clearPendingRestart } from './pending-restart.js';
13
- import { addAllowBuildsKey, parseAllowBuildsKey, profileDirectory } from './profile.js';
17
+ import { addAllowBuildsKey, githubRepoOf, parseAllowBuildsKey, profileDirectory } from './profile.js';
14
18
  /** In-memory task registry, keyed by task id. */
15
19
  const tasks = new Map();
16
20
  let nextTaskId = 1;
@@ -142,7 +146,7 @@ function stopChild(child) {
142
146
  * Progress is visible through `getTask(id)` / `activeTask()` until done.
143
147
  */
144
148
  export function startPluginMutation(options) {
145
- const task = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [] };
149
+ const task = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false };
146
150
  nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1;
147
151
  tasks.set(task.id, task);
148
152
  if (tasks.size > MAX_TASKS) {
@@ -185,7 +189,7 @@ function spawnMutation(options) {
185
189
  const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, uninstallLoader } = options;
186
190
  const invocation = cliInvocation();
187
191
  const args = [...invocation.prefixArgs, 'plugin', '--profile', profile, action, target];
188
- // 记录实际执行的命令(npm 通道显示包名,git 通道显示 github: 源):失败提 Issue 时
192
+ // 记录实际执行的命令(npm 通道显示包名,git 通道显示 HTTPS 源):失败提 Issue 时
189
193
  // 「已尝试的安装方式」如实展示完整命令链,作者一眼看清我们跑过什么
190
194
  const executed = `dsh plugin --profile ${profile} ${action} ${target}`;
191
195
  if (task && !task.attempts.includes(executed)) {
@@ -275,8 +279,7 @@ function spawnMutation(options) {
275
279
  child.stderr?.on('data', (chunk) => collect('stderr', chunk));
276
280
  child.once('exit', () => {
277
281
  // dsh 内部会 fork pnpm:pnpm 退出后仍攥着 stdout/stderr 管道时,
278
- // close 事件永不到来(历史队列卡死根因——取消路径曾因此卡死,用整组 SIGKILL 解决;
279
- // 自然失败路径没有兜底)。exit 后给 close 一段宽限,未按时触发则整组清理强制结束,
282
+ // close 事件不会到来。exit 后给 close 一段宽限,未按时触发则整组清理强制结束,
280
283
  // 保证任务必然进入终态、后续排队任务自动启动。
281
284
  exitWatch = setTimeout(() => {
282
285
  if (settled)
@@ -316,9 +319,13 @@ function spawnMutation(options) {
316
319
  if (task.status === 'done') {
317
320
  task.progress = 100;
318
321
  if (action === 'remove' && uninstallLoader) {
319
- // 卸载成功 → 主动从运行中 loader 移除该包条目,立即生效、无需重启;
320
- // 移除失败(或从未加载过但 loader 不可用)才登记「待重启清理」兜底
322
+ // 卸载成功 → 主动从运行中 loader 停用该包条目;
323
+ // 仅当宿主从未加载过它(磁盘已干净)才判「无需重启」;
324
+ // 只要曾在 loader 中存活(停用过 live entry)就仍要求重启——
325
+ // 带 UI 的插件(侧边栏面板等宿主启动时渲染的槽位)disable 后不会
326
+ // 主动消失,重启才能立即摘除面板
321
327
  const removed = await removeLoadedEntry(uninstallLoader, target);
328
+ task.needsRestart = !removed;
322
329
  if (removed)
323
330
  clearPendingRestart(displayTarget ?? target);
324
331
  else
@@ -327,6 +334,7 @@ function spawnMutation(options) {
327
334
  else {
328
335
  // 安装成功(或卸载但无 loader 引用)→ 登记待重启:
329
336
  // 插件要宿主重启才会挂载/卸载干净,重启前一直提醒;展示目标用 displayTarget(owner/repo)
337
+ task.needsRestart = true;
330
338
  addPendingRestart(displayTarget ?? target, action === 'add' ? 'install' : 'uninstall');
331
339
  }
332
340
  }
@@ -355,8 +363,11 @@ export function verifyInstalledEntry(profile, target) {
355
363
  return { name: null, missing: null };
356
364
  }
357
365
  const deps = (profilePkg?.dependencies ?? {});
358
- // target 自身是依赖名(npm 包),或通过 github: specifier 反查依赖名
359
- const name = deps[target] !== undefined ? target : Object.keys(deps).find((k) => deps[k] === target);
366
+ // target 自身是依赖名(npm 包),或通过 Git specifier 反查依赖名
367
+ const targetRepo = githubRepoOf(target)?.toLowerCase();
368
+ const name = targetRepo !== undefined
369
+ ? Object.keys(deps).find((k) => githubRepoOf(deps[k])?.toLowerCase() === targetRepo)
370
+ : deps[target] !== undefined ? target : Object.keys(deps).find((k) => deps[k] === target);
360
371
  if (!name)
361
372
  return { name: null, missing: null };
362
373
  const pkgDir = join(profileDirectory(profile), 'node_modules', name);
@@ -423,7 +434,7 @@ export async function runPluginMutation(options) {
423
434
  // 输出解析不到时回退用目标名(插件自身构建脚本被拦的 npm 场景)。
424
435
  if (output.includes('ERR_PNPM_IGNORED_BUILDS')) {
425
436
  const keys = ignoredBuildKeys(output);
426
- if (keys.length === 0 && !options.target.startsWith('github:'))
437
+ if (keys.length === 0 && githubRepoOf(options.target) === null)
427
438
  keys.push(options.target);
428
439
  if (keys.length > 0) {
429
440
  if (options.task) {
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * dsh-plugin host entry: mounts the Plugin Hub HTTP routes once the host
3
7
  * web server service is available. The browser bundle (src/client) talks to
4
8
  * these same-origin routes to perform real installs from inside the app.
@@ -1,4 +1,10 @@
1
- /** 安装/卸载运行时的公共类型与常量(无依赖,供各服务模块复用)。 */
1
+ /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * 安装/卸载运行时的公共类型与常量(无依赖,供各服务模块复用)。
7
+ */
2
8
  /** 一个 pnpm 安装子进程的结果快照。 */
3
9
  export interface InstallResult {
4
10
  exitCode: number | null;
@@ -9,7 +15,7 @@ export interface InstallResult {
9
15
  }
10
16
  export interface InstallTask {
11
17
  id: number;
12
- /** 操作目标:`github:<owner>/<repo>`(安装)或 npm 包名(卸载),冲突/恢复时需要展示给用户 */
18
+ /** 操作目标:显式 HTTPS Git URL(安装)或 npm 包名(卸载),冲突/恢复时需要展示给用户 */
13
19
  target: string;
14
20
  /** 展示用目标(owner/repo):npm 安装时 target 是包名,前端恢复用此字段显示仓库名,保持用户无感知 */
15
21
  displayTarget?: string;
@@ -26,6 +32,8 @@ export interface InstallTask {
26
32
  progress: number;
27
33
  /** Newest output lines first (consumer shows the tail). */
28
34
  lines: string[];
35
+ /** 完成后是否仍需宿主重启才生效:卸载时 loader 已即时移除 → false;否则 true(弹窗据此给重启选项) */
36
+ needsRestart: boolean;
29
37
  }
30
38
  /** 子进程启动参数(复用宿主 dsh 入口,或回退到 PATH 上的 `dsh`)。 */
31
39
  export interface Invocation {
@@ -50,14 +58,14 @@ export interface QueueItem {
50
58
  * 它自然清空,正好等于「重启后提醒消失」的语义,无需任何重启探测。
51
59
  */
52
60
  export interface PendingRestart {
53
- /** `github:<owner>/<repo>` 安装目标(与任务一致;卸载时为展示用的 owner/repo) */
61
+ /** Git 安装目标(与任务一致;卸载时为展示用的 owner/repo) */
54
62
  target: string;
55
63
  /** 待重启语义:install 装完等挂载生效 / uninstall 卸完等清理 loader 残留 */
56
64
  kind: 'install' | 'uninstall';
57
65
  /** 任务完成时刻(epoch ms) */
58
66
  at: number;
59
67
  }
60
- /** Grammar of an accepted `github:<owner>/<repo>` install target. */
68
+ /** Grammar of a catalog GitHub repository identity (`owner/repo`). */
61
69
  export declare const REPO_RE: RegExp;
62
70
  /** Grammar of an npm package name (used for uninstall targets). */
63
71
  export declare const PACKAGE_RE: RegExp;
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * Install runner 的统一出口:执行真实插件变更(spawn 官方 dsh CLI 子进程)。
3
7
  * 实现按职责拆分在同目录子模块,本文件只做公共 API 的 re-export,保持外部
4
8
  * import 面稳定(routes.ts / index.ts 仍从 `services/install.ts` 引入)。
@@ -14,5 +18,5 @@ export type { InstallResult, InstallTask, Invocation, PendingRestart, QueueItem
14
18
  export type { LoaderHandle } from './loader.ts';
15
19
  export { addPendingRestart, clearPendingRestart, listPendingRestarts } from './pending-restart.ts';
16
20
  export { dumpLoaderEntries, removeLoadedEntry } from './loader.ts';
17
- export { addAllowBuildsKey, githubTarget, parseAllowBuildsKey, readProfileArg, validPackageName } from './profile.ts';
21
+ export { addAllowBuildsKey, githubRepoOf, githubTarget, parseAllowBuildsKey, readProfileArg, validPackageName } from './profile.ts';
18
22
  export { activeTask, cancelTask, getTask, hasQueuedTarget, hasRunningTask, runPluginMutation, startPluginMutation } from './task-queue.ts';
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * 运行中 loader 的读取与停用:卸载成功后对匹配条目做 live-disable,
3
7
  * 使卸载立即生效(刷新页面不再加载已卸载的 client bundle,也无需重启)。
4
8
  */
@@ -32,12 +36,14 @@ export declare function dumpLoaderEntries(loader: LoaderHandle | undefined): Arr
32
36
  name?: string;
33
37
  }>;
34
38
  /**
35
- * 卸载成功后从运行中 loader 停用指定包的条目,使卸载立即生效(刷新页面不再
36
- * 加载已卸载的 client bundle,也无需重启)。做法:按 name 扫描
39
+ * 卸载成功后从运行中 loader 停用指定包的条目。做法:按 name 扫描
37
40
  * `loader.entries()`(含嵌套子树),对每个匹配条目
38
41
  * `entry.update({ disabled: true })` 做 live-disable —— fiber 被 dispose,
39
42
  * client-modules 对账后不再把它写进 `__DSH_BOOT__`,页面刷新即恢复。
40
- * 返回 true = 无需重启(条目已停用,或本就没加载过);false = 未能停用,
41
- * 需登记「待重启清理」兜底。宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
43
+ * 返回 true = 无需重启(仅当宿主从未加载过它:磁盘已干净);
44
+ * 其余一律 false = 需要重启:要么 disable 失败需「待重启清理」兜底,
45
+ * 要么 disable 成功但该插件曾在 loader 中存活——带 UI 的插件(侧边栏面板等
46
+ * 宿主启动时渲染的槽位)disable 后不会主动摘除,不重启面板一直残留。
47
+ * 宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
42
48
  */
43
49
  export declare function removeLoadedEntry(loader: LoaderHandle, name: string): Promise<boolean>;
@@ -1,9 +1,15 @@
1
- /** 待重启列表的内存态管理(宿主进程一重启自然清空)。 */
1
+ /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * 待重启列表的内存态管理(宿主进程一重启自然清空)。
7
+ */
2
8
  import type { PendingRestart } from './install-types.ts';
3
9
  /**
4
10
  * 登记一条待重启记录(同一目标去重)。
5
11
  * - 卸载:目标刚装完未重启(已有待重启记录)→ 宿主从未加载过它,卸完磁盘已干净、
6
- * loader 无残留,无需再提醒重启,直接移除该条;否则(此前已被宿主加载过)登记「卸载后待重启清理」。
12
+ * loader 无残留,无需再提醒重启,直接移除该条;否则(已被宿主加载过)登记「卸载后待重启清理」。
7
13
  * - 安装:已有记录则刷新为安装语义(如卸载后未重启又重装),否则新登记。
8
14
  */
9
15
  export declare function addPendingRestart(target: string, kind: 'install' | 'uninstall'): void;
@@ -2,13 +2,15 @@
2
2
  export declare function readProfileArg(fallback?: string): string;
3
3
  /** Resolve a profile directory (`DSH_HOME` or `~/.dsh`). */
4
4
  export declare function profileDirectory(profile: string): string;
5
- /** Build a safe `github:<owner>/<repo>` target, or null when the repo is unsafe. */
5
+ /** Build a safe explicit HTTPS Git target, or null when the repo is unsafe. */
6
6
  export declare function githubTarget(repo: string): string | null;
7
+ /** Extract an owner/repo identity from a catalog value or an installed Git spec. */
8
+ export declare function githubRepoOf(value: string): string | null;
7
9
  /** Validate an npm package name (uninstall target grammar). */
8
10
  export declare function validPackageName(name: string): boolean;
9
11
  /**
10
12
  * The exact `allowBuilds` key pnpm printed in its
11
- * `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` hint. For `github:` installs pnpm
13
+ * `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` hint. For Git installs pnpm
12
14
  * matches against `name@<resolved fetch URL>` — the URL it actually fetches,
13
15
  * e.g. `https://codeload.github.com/<owner>/<repo>/tar.gz/<commit>` — not the
14
16
  * `git+ssh://`/branch spec. A bare name or a `git+` spec does not match, so
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * Pure progress-estimation helpers shared by the install runner.
3
7
  *
4
8
  * The dsh CLI wraps pnpm, which refreshes its `Progress: …` line in place
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 4000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):4000+ 人工精选社区插件,每日更新。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -0,0 +1,76 @@
1
+ /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * 头部「关注我们」弹窗:介绍 DSH-Plugin Hub 是干什么的 + 用户反馈群二维码。
7
+ * 内容以 Markdown 形式由 dsh-update Worker 的 /about 接口下发(key = hub:about),
8
+ * 作者想怎么写就怎么写、随写随推,客户端只负责渲染 —— 非写死。
9
+ * 图片(反馈群二维码)经 renderMarkdown 的 ![](url) 语法嵌入,居中、最大高度受控。
10
+ */
11
+ import { createElement as h } from 'react'
12
+ import type { MouseEvent } from 'react'
13
+ import styles from '../styles/Modal.module.css'
14
+ import type { HubAboutInfo } from '../lib/catalog.ts'
15
+ import type { LocaleId, Translate } from '../types.ts'
16
+ import { renderMarkdown } from '../lib/renderMarkdown.ts'
17
+ import { CloseIcon } from './icons.tsx'
18
+
19
+ export function AboutModal({ info, lang, t, onClose }: {
20
+ info: HubAboutInfo | null
21
+ lang: LocaleId
22
+ t: Translate
23
+ onClose: () => void
24
+ }) {
25
+ // 内容:字符串直接使用;{zh,en} 对象按界面语言取,缺哪种补哪种,全缺则展示兜底文案。
26
+ // Worker 未推送(info 为 null / content 为空)时弹窗仍能打开,显示「暂未发布」而非报错。
27
+ const contentRaw = info
28
+ ? typeof info.content === 'string'
29
+ ? info.content
30
+ : info.content && typeof info.content === 'object'
31
+ ? lang === 'en'
32
+ ? (info.content.en ?? info.content.zh ?? '')
33
+ : (info.content.zh ?? info.content.en ?? '')
34
+ : ''
35
+ : ''
36
+ const contentHtml = contentRaw.trim() ? renderMarkdown(contentRaw) : null
37
+ // 更新时间:ISO 字符串转本地可读格式;非法值静默隐藏
38
+ let updated: string | null = null
39
+ if (info?.updatedAt) {
40
+ const d = new Date(info.updatedAt)
41
+ if (!Number.isNaN(d.getTime())) {
42
+ updated = d.toLocaleString(lang === 'en' ? 'en-US' : 'zh-CN')
43
+ }
44
+ }
45
+
46
+ return h('div', {
47
+ className: styles.overlay,
48
+ onClick: (e: MouseEvent<HTMLDivElement>) => {
49
+ if (e.target === e.currentTarget) onClose()
50
+ },
51
+ },
52
+ h('div', { className: `${styles.modal} ${styles.aboutModal}`, role: 'dialog', 'aria-modal': 'true' },
53
+ h('div', { className: styles.modalHead },
54
+ h('div', { className: styles.modalTitle }, t('aboutTitle')),
55
+ h('button', {
56
+ className: styles.modalClose,
57
+ 'aria-label': t('confirmCancel'),
58
+ onClick: () => onClose(),
59
+ }, h(CloseIcon)),
60
+ ),
61
+ h('div', { className: styles.modalDesc }, t('aboutDesc')),
62
+ contentHtml
63
+ ? h('div', {
64
+ className: styles.aboutContent,
65
+ dangerouslySetInnerHTML: { __html: contentHtml },
66
+ })
67
+ : h('div', { className: styles.aboutContent }, t('aboutEmpty')),
68
+ updated
69
+ ? h('div', { className: styles.aboutMeta }, `${t('aboutUpdated')} ${updated}`)
70
+ : null,
71
+ h('div', { className: styles.modalActions },
72
+ h('button', { className: styles.modalInstall, onClick: onClose }, t('doneBtn')),
73
+ ),
74
+ ),
75
+ )
76
+ }
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * Toolbar above the list: search input + sort dropdown + installed / not
3
7
  * installed filter buttons + the notifications entry. All buttons share the
4
8
  * same size (24px high, like the notification entry button).