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
@@ -9,7 +13,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
9
13
  import { homedir, release } from 'node:os'
10
14
  import { dirname, join, resolve } from 'node:path'
11
15
  import { spawn } from 'node:child_process'
12
- import { activeTask, cancelTask, dumpLoaderEntries, getTask, githubTarget, hasQueuedTarget, listPendingRestarts, readProfileArg, startPluginMutation, validPackageName, type LoaderHandle } from '../services/install.ts'
16
+ import { activeTask, cancelTask, dumpLoaderEntries, getTask, githubRepoOf, githubTarget, hasQueuedTarget, listPendingRestarts, readProfileArg, startPluginMutation, validPackageName, type LoaderHandle } from '../services/install.ts'
13
17
  import { recordInstalledVersion, recordResolvedNpmPackage, readInstalledVersions, removeInstalledVersion } from '../services/installed-versions.ts'
14
18
  import { resolveNpmPackage } from '../services/npm-resolve.ts'
15
19
  import { preflightTarget } from '../services/preflight.ts'
@@ -82,11 +86,13 @@ function hostEnv(profile: string): Record<string, string | null> {
82
86
  }
83
87
  }
84
88
 
85
- /** Match an install target (`github:<owner>/<repo>`) against the installed table; returns the package name when already installed. */
89
+ /** Match a GitHub install target against the installed table; returns the package name when already installed. */
86
90
  function installedByRepo(installed: Record<string, string>, target: string): string | null {
87
- const needle = target.toLowerCase()
91
+ const repo = githubRepoOf(target)
92
+ if (repo === null) return null
93
+ const needle = repo.toLowerCase()
88
94
  for (const [name, spec] of Object.entries(installed)) {
89
- if (spec.toLowerCase().includes(needle)) return name
95
+ if (githubRepoOf(spec)?.toLowerCase() === needle) return name
90
96
  }
91
97
  return null
92
98
  }
@@ -178,7 +184,7 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
178
184
  const displayRepo = typeof body === 'object' && body !== null && typeof (body as { display?: unknown }).display === 'string'
179
185
  ? (body as { display: string }).display.trim()
180
186
  : rawRepo
181
- // 目标语法两态:owner/repo → github 源(走装前预检);npm 包名(@scope/name 或 name)→ 信任 registry 直接安装
187
+ // 目标语法两态:owner/repo → 显式 HTTPS Git 源(走装前预检);npm 包名(@scope/name 或 name)→ 信任 registry 直接安装
182
188
  const repoTarget = githubTarget(rawRepo)
183
189
  let target: string | null = repoTarget ?? (repoTarget === null && validPackageName(rawRepo) ? rawRepo : null)
184
190
  if (target === null) {
@@ -208,7 +214,7 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
208
214
  // 重复安装防护:非更新请求命中已安装目标时直接拒绝,不重复跑 CLI
209
215
  // (否则 CLI 会因「已存在」失败,且用户看到的是莫名其妙的报错);
210
216
  // 更新请求(mode: 'update')放行,CLI 的 add 对已存在依赖是 pnpm 原位覆盖重装。
211
- // 命中判定双通道:npm 包名按 dependencies 键直接命中,github: 目标按 spec 值匹配。
217
+ // 命中判定双通道:npm 包名按 dependencies 键直接命中,GitHub 目标按仓库身份匹配。
212
218
  const installed = readInstalled(profile)
213
219
  const already = installedByRepo(installed, target) ?? (installed[target] !== undefined ? target : null)
214
220
  if (already !== null && mode !== 'update') {
@@ -249,7 +255,7 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
249
255
  profile,
250
256
  target,
251
257
  // npm 安装时 target 是包名:displayTarget 固定展示仓库名,待重启行/前端恢复不受通道变化影响
252
- displayTarget: githubTarget(displayRepo) ?? undefined,
258
+ displayTarget: githubRepoOf(displayRepo) ?? undefined,
253
259
  // 已尝试的安装方式(npm 反查):实际执行命令由 spawn 时追加,失败 issue 一并展示
254
260
  attempts,
255
261
  timeoutMs: COMMAND_TIMEOUT_MS,
@@ -294,7 +300,7 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
294
300
  profile,
295
301
  target: name,
296
302
  // 卸载的 mutation 目标是 npm 包名;待重启行要显示 owner/repo(与安装行一致),非法/缺失时回退包名
297
- displayTarget: githubTarget(repo) ?? undefined,
303
+ displayTarget: githubRepoOf(repo) ?? undefined,
298
304
  // 运行中 loader:卸载成功后主动移除条目,立即生效、无需重启
299
305
  uninstallLoader: loader,
300
306
  timeoutMs: COMMAND_TIMEOUT_MS,
@@ -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
 
3
9
  /** 一个 pnpm 安装子进程的结果快照。 */
4
10
  export interface InstallResult {
@@ -11,7 +17,7 @@ export interface InstallResult {
11
17
 
12
18
  export interface InstallTask {
13
19
  id: number
14
- /** 操作目标:`github:<owner>/<repo>`(安装)或 npm 包名(卸载),冲突/恢复时需要展示给用户 */
20
+ /** 操作目标:显式 HTTPS Git URL(安装)或 npm 包名(卸载),冲突/恢复时需要展示给用户 */
15
21
  target: string
16
22
  /** 展示用目标(owner/repo):npm 安装时 target 是包名,前端恢复用此字段显示仓库名,保持用户无感知 */
17
23
  displayTarget?: string
@@ -28,6 +34,8 @@ export interface InstallTask {
28
34
  progress: number
29
35
  /** Newest output lines first (consumer shows the tail). */
30
36
  lines: string[]
37
+ /** 完成后是否仍需宿主重启才生效:卸载时 loader 已即时移除 → false;否则 true(弹窗据此给重启选项) */
38
+ needsRestart: boolean
31
39
  }
32
40
 
33
41
  /** 子进程启动参数(复用宿主 dsh 入口,或回退到 PATH 上的 `dsh`)。 */
@@ -49,7 +57,7 @@ export interface QueueItem {
49
57
  * 它自然清空,正好等于「重启后提醒消失」的语义,无需任何重启探测。
50
58
  */
51
59
  export interface PendingRestart {
52
- /** `github:<owner>/<repo>` 安装目标(与任务一致;卸载时为展示用的 owner/repo) */
60
+ /** Git 安装目标(与任务一致;卸载时为展示用的 owner/repo) */
53
61
  target: string
54
62
  /** 待重启语义:install 装完等挂载生效 / uninstall 卸完等清理 loader 残留 */
55
63
  kind: 'install' | 'uninstall'
@@ -57,7 +65,7 @@ export interface PendingRestart {
57
65
  at: number
58
66
  }
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 const REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/
62
70
  /** Grammar of an npm package name (used for uninstall targets). */
63
71
  export const PACKAGE_RE = /^(?:@[a-z0-9._-]+\/)?[A-Za-z0-9._-]+$/
@@ -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,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
  */
@@ -60,13 +64,15 @@ function nameMatches(entryName: string | undefined, target: string): boolean {
60
64
  }
61
65
 
62
66
  /**
63
- * 卸载成功后从运行中 loader 停用指定包的条目,使卸载立即生效(刷新页面不再
64
- * 加载已卸载的 client bundle,也无需重启)。做法:按 name 扫描
67
+ * 卸载成功后从运行中 loader 停用指定包的条目。做法:按 name 扫描
65
68
  * `loader.entries()`(含嵌套子树),对每个匹配条目
66
69
  * `entry.update({ disabled: true })` 做 live-disable —— fiber 被 dispose,
67
70
  * client-modules 对账后不再把它写进 `__DSH_BOOT__`,页面刷新即恢复。
68
- * 返回 true = 无需重启(条目已停用,或本就没加载过);false = 未能停用,
69
- * 需登记「待重启清理」兜底。宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
71
+ * 返回 true = 无需重启(仅当宿主从未加载过它:磁盘已干净);
72
+ * 其余一律 false = 需要重启:要么 disable 失败需「待重启清理」兜底,
73
+ * 要么 disable 成功但该插件曾在 loader 中存活——带 UI 的插件(侧边栏面板等
74
+ * 宿主启动时渲染的槽位)disable 后不会主动摘除,不重启面板一直残留。
75
+ * 宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
70
76
  */
71
77
  export async function removeLoadedEntry(loader: LoaderHandle, name: string): Promise<boolean> {
72
78
  if (name === 'dsh-plugin' || name.startsWith('@deepseek-ai/')) return false
@@ -95,8 +101,13 @@ export async function removeLoadedEntry(loader: LoaderHandle, name: string): Pro
95
101
  if (!found) {
96
102
  // 运行中 loader 无该条目:宿主从未加载过它(装完未重启),磁盘已干净,无需重启
97
103
  console.log(`[hub-uninstall] no live entry for ${name}; nothing to disable`)
104
+ return true
98
105
  }
99
- return true
106
+ // 找到并停用了 live entry:逻辑层已卸载(刷新页面不再加载其 client bundle),
107
+ // 但带 UI 的插件(侧边栏面板等)是宿主启动时渲染的槽位,disable 不会主动摘除,
108
+ // 不重启面板会一直残留 → 返回「需要重启」,弹窗给出「立即重启」,面板立即消失
109
+ console.log(`[hub-uninstall] disabled live entry for ${name}; UI may persist until restart`)
110
+ return false
100
111
  })()
101
112
  const result = await Promise.race([
102
113
  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
  *
@@ -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
  import type { PendingRestart } from './install-types.ts'
3
9
 
4
10
  const pendingRestarts: PendingRestart[] = []
@@ -6,7 +12,7 @@ const pendingRestarts: PendingRestart[] = []
6
12
  /**
7
13
  * 登记一条待重启记录(同一目标去重)。
8
14
  * - 卸载:目标刚装完未重启(已有待重启记录)→ 宿主从未加载过它,卸完磁盘已干净、
9
- * loader 无残留,无需再提醒重启,直接移除该条;否则(此前已被宿主加载过)登记「卸载后待重启清理」。
15
+ * loader 无残留,无需再提醒重启,直接移除该条;否则(已被宿主加载过)登记「卸载后待重启清理」。
10
16
  * - 安装:已有记录则刷新为安装语义(如卸载后未重启又重装),否则新登记。
11
17
  */
12
18
  export function addPendingRestart(target: string, kind: 'install' | 'uninstall'): void {
@@ -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,6 +17,7 @@ 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.ts'
16
21
 
17
22
  const execFileAsync = promisify(execFile)
18
23
 
@@ -23,9 +28,9 @@ export interface PreflightResult {
23
28
  }
24
29
 
25
30
  export async function preflightTarget(target: string): Promise<PreflightResult> {
26
- const m = /^github:([^/]+)\/([^/]+)$/.exec(target)
27
- if (m === null) return { ok: true, missing: null }
28
- const [, owner, repo] = m
31
+ const source = githubRepoOf(target)
32
+ if (source === null) return { ok: true, missing: null }
33
+ const [owner, repo] = source.split('/')
29
34
  let commit = ''
30
35
  try {
31
36
  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
  */
@@ -19,10 +23,30 @@ export function profileDirectory(profile: string): string {
19
23
  return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile)
20
24
  }
21
25
 
22
- /** Build a safe `github:<owner>/<repo>` target, or null when the repo is unsafe. */
26
+ /** Build a safe explicit HTTPS Git target, or null when the repo is unsafe. */
23
27
  export function githubTarget(repo: string): string | null {
24
28
  if (typeof repo !== 'string' || !REPO_RE.test(repo)) return null
25
- return `github:${repo}`
29
+ return `git+https://github.com/${repo}.git`
30
+ }
31
+
32
+ /** Extract an owner/repo identity from a catalog value or an installed Git spec. */
33
+ export function githubRepoOf(value: string): string | null {
34
+ if (typeof value !== 'string') return null
35
+ const input = value.trim()
36
+ if (REPO_RE.test(input)) return input
37
+ const patterns = [
38
+ /^github:([^/]+)\/([^/]+)$/i,
39
+ /^(?:git\+)?https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
40
+ /^(?:git\+)?ssh:\/\/(?:git@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
41
+ /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
42
+ ]
43
+ for (const pattern of patterns) {
44
+ const match = pattern.exec(input)
45
+ if (match === null) continue
46
+ const repo = `${match[1]}/${match[2]}`
47
+ if (REPO_RE.test(repo)) return repo
48
+ }
49
+ return null
26
50
  }
27
51
 
28
52
  /** Validate an npm package name (uninstall target grammar). */
@@ -32,7 +56,7 @@ export function validPackageName(name: string): boolean {
32
56
 
33
57
  /**
34
58
  * The exact `allowBuilds` key pnpm printed in its
35
- * `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` hint. For `github:` installs pnpm
59
+ * `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` hint. For Git installs pnpm
36
60
  * matches against `name@<resolved fetch URL>` — the URL it actually fetches,
37
61
  * e.g. `https://codeload.github.com/<owner>/<repo>/tar.gz/<commit>` — not the
38
62
  * `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)。
@@ -12,7 +16,7 @@ import { cleanLine, estimateProgress } from './progress.ts'
12
16
  import type { LoaderHandle } from './loader.ts'
13
17
  import { removeLoadedEntry } from './loader.ts'
14
18
  import { addPendingRestart, clearPendingRestart } from './pending-restart.ts'
15
- import { addAllowBuildsKey, parseAllowBuildsKey, profileDirectory } from './profile.ts'
19
+ import { addAllowBuildsKey, githubRepoOf, parseAllowBuildsKey, profileDirectory } from './profile.ts'
16
20
 
17
21
  /** In-memory task registry, keyed by task id. */
18
22
  const tasks = new Map<number, InstallTask>()
@@ -170,7 +174,7 @@ export function startPluginMutation(options: {
170
174
  /** 入队前已尝试的安装方式(npm registry 反查等):失败提 Issue 时如实展示;实际执行的命令由 spawn 时追加 */
171
175
  attempts?: string[]
172
176
  }): InstallTask {
173
- const task: InstallTask = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [] }
177
+ const task: InstallTask = { 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 }
174
178
  nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1
175
179
  tasks.set(task.id, task)
176
180
  if (tasks.size > MAX_TASKS) {
@@ -223,7 +227,7 @@ function spawnMutation(options: {
223
227
  const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, uninstallLoader } = options
224
228
  const invocation = cliInvocation()
225
229
  const args = [...invocation.prefixArgs, 'plugin', '--profile', profile, action, target]
226
- // 记录实际执行的命令(npm 通道显示包名,git 通道显示 github: 源):失败提 Issue 时
230
+ // 记录实际执行的命令(npm 通道显示包名,git 通道显示 HTTPS 源):失败提 Issue 时
227
231
  // 「已尝试的安装方式」如实展示完整命令链,作者一眼看清我们跑过什么
228
232
  const executed = `dsh plugin --profile ${profile} ${action} ${target}`
229
233
  if (task && !task.attempts.includes(executed)) {
@@ -305,8 +309,7 @@ function spawnMutation(options: {
305
309
 
306
310
  child.once('exit', () => {
307
311
  // dsh 内部会 fork pnpm:pnpm 退出后仍攥着 stdout/stderr 管道时,
308
- // close 事件永不到来(历史队列卡死根因——取消路径曾因此卡死,用整组 SIGKILL 解决;
309
- // 自然失败路径没有兜底)。exit 后给 close 一段宽限,未按时触发则整组清理强制结束,
312
+ // close 事件不会到来。exit 后给 close 一段宽限,未按时触发则整组清理强制结束,
310
313
  // 保证任务必然进入终态、后续排队任务自动启动。
311
314
  exitWatch = setTimeout(() => {
312
315
  if (settled) return
@@ -345,14 +348,19 @@ function spawnMutation(options: {
345
348
  if (task.status === 'done') {
346
349
  task.progress = 100
347
350
  if (action === 'remove' && uninstallLoader) {
348
- // 卸载成功 → 主动从运行中 loader 移除该包条目,立即生效、无需重启;
349
- // 移除失败(或从未加载过但 loader 不可用)才登记「待重启清理」兜底
351
+ // 卸载成功 → 主动从运行中 loader 停用该包条目;
352
+ // 仅当宿主从未加载过它(磁盘已干净)才判「无需重启」;
353
+ // 只要曾在 loader 中存活(停用过 live entry)就仍要求重启——
354
+ // 带 UI 的插件(侧边栏面板等宿主启动时渲染的槽位)disable 后不会
355
+ // 主动消失,重启才能立即摘除面板
350
356
  const removed = await removeLoadedEntry(uninstallLoader, target)
357
+ task.needsRestart = !removed
351
358
  if (removed) clearPendingRestart(displayTarget ?? target)
352
359
  else addPendingRestart(displayTarget ?? target, 'uninstall')
353
360
  } else {
354
361
  // 安装成功(或卸载但无 loader 引用)→ 登记待重启:
355
362
  // 插件要宿主重启才会挂载/卸载干净,重启前一直提醒;展示目标用 displayTarget(owner/repo)
363
+ task.needsRestart = true
356
364
  addPendingRestart(displayTarget ?? target, action === 'add' ? 'install' : 'uninstall')
357
365
  }
358
366
  }
@@ -380,8 +388,11 @@ export function verifyInstalledEntry(profile: string, target: string): { name: s
380
388
  return { name: null, missing: null }
381
389
  }
382
390
  const deps = (profilePkg?.dependencies ?? {}) as Record<string, string>
383
- // target 自身是依赖名(npm 包),或通过 github: specifier 反查依赖名
384
- const name = deps[target] !== undefined ? target : Object.keys(deps).find((k) => deps[k] === target)
391
+ // target 自身是依赖名(npm 包),或通过 Git specifier 反查依赖名
392
+ const targetRepo = githubRepoOf(target)?.toLowerCase()
393
+ const name = targetRepo !== undefined
394
+ ? Object.keys(deps).find((k) => githubRepoOf(deps[k])?.toLowerCase() === targetRepo)
395
+ : deps[target] !== undefined ? target : Object.keys(deps).find((k) => deps[k] === target)
385
396
  if (!name) return { name: null, missing: null }
386
397
  const pkgDir = join(profileDirectory(profile), 'node_modules', name)
387
398
  let entry: string | null = null
@@ -460,7 +471,7 @@ export async function runPluginMutation(options: {
460
471
  // 输出解析不到时回退用目标名(插件自身构建脚本被拦的 npm 场景)。
461
472
  if (output.includes('ERR_PNPM_IGNORED_BUILDS')) {
462
473
  const keys = ignoredBuildKeys(output)
463
- if (keys.length === 0 && !options.target.startsWith('github:')) keys.push(options.target)
474
+ if (keys.length === 0 && githubRepoOf(options.target) === null) keys.push(options.target)
464
475
  if (keys.length > 0) {
465
476
  if (options.task) {
466
477
  options.task.status = 'running'