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.
- package/README.en.md +5 -5
- package/README.md +5 -5
- package/client/client.js +648 -190
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +15 -8
- package/lib/index.js +4 -0
- package/lib/services/install-types.js +8 -2
- package/lib/services/install.js +1 -1
- package/lib/services/installed-versions.js +5 -1
- package/lib/services/loader.js +16 -5
- package/lib/services/npm-resolve.js +4 -0
- package/lib/services/pending-restart.js +1 -1
- package/lib/services/preflight.js +9 -4
- package/lib/services/profile.js +30 -3
- package/lib/services/progress.js +4 -0
- package/lib/services/task-queue.js +21 -10
- package/lib/types/index.d.ts +4 -0
- package/lib/types/services/install-types.d.ts +12 -4
- package/lib/types/services/install.d.ts +5 -1
- package/lib/types/services/loader.d.ts +10 -4
- package/lib/types/services/pending-restart.d.ts +8 -2
- package/lib/types/services/profile.d.ts +4 -2
- package/lib/types/services/progress.d.ts +4 -0
- package/package.json +1 -1
- package/src/client/components/AboutModal.tsx +76 -0
- package/src/client/components/CatalogControls.tsx +4 -0
- package/src/client/components/CatalogHeader.tsx +34 -5
- package/src/client/components/CatalogList.tsx +4 -0
- package/src/client/components/CategoryTabs.tsx +4 -0
- package/src/client/components/Dropdown.tsx +4 -0
- package/src/client/components/HubUpdateModal.tsx +88 -0
- package/src/client/components/NotificationsModal.tsx +25 -8
- package/src/client/components/PluginCard.tsx +8 -1
- package/src/client/components/PluginHubSection.tsx +67 -7
- package/src/client/components/ProgressView.tsx +4 -0
- package/src/client/components/icons.tsx +4 -0
- package/src/client/components/modals.tsx +35 -10
- package/src/client/hooks/useCatalog.ts +89 -17
- package/src/client/hooks/useTaskQueue.ts +52 -35
- package/src/client/index.tsx +4 -0
- package/src/client/lib/catalog.ts +59 -6
- package/src/client/lib/env.ts +4 -0
- package/src/client/lib/failures.ts +5 -1
- package/src/client/lib/format.ts +4 -0
- package/src/client/lib/renderMarkdown.ts +120 -0
- package/src/client/locales.ts +48 -1
- package/src/client/styles/Dropdown.module.css +5 -1
- package/src/client/styles/Header.module.css +88 -4
- package/src/client/styles/List.module.css +5 -1
- package/src/client/styles/Modal.module.css +279 -1
- package/src/client/styles/tokens.module.css +5 -1
- package/src/client/types.ts +7 -1
- package/src/server/http/routes.ts +14 -8
- package/src/server/index.ts +4 -0
- package/src/server/services/install-types.ts +12 -4
- package/src/server/services/install.ts +5 -1
- package/src/server/services/installed-versions.ts +5 -1
- package/src/server/services/loader.ts +16 -5
- package/src/server/services/npm-resolve.ts +4 -0
- package/src/server/services/pending-restart.ts +8 -2
- package/src/server/services/preflight.ts +9 -4
- package/src/server/services/profile.ts +27 -3
- package/src/server/services/progress.ts +4 -0
- 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
|
|
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
|
|
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
|
|
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 →
|
|
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 键直接命中,
|
|
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:
|
|
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:
|
|
303
|
+
displayTarget: githubRepoOf(repo) ?? undefined,
|
|
298
304
|
// 运行中 loader:卸载成功后主动移除条目,立即生效、无需重启
|
|
299
305
|
uninstallLoader: loader,
|
|
300
306
|
timeoutMs: COMMAND_TIMEOUT_MS,
|
package/src/server/index.ts
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,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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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 =
|
|
69
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
27
|
-
if (
|
|
28
|
-
const [
|
|
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
|
|
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
|
|
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
|
|
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 通道显示
|
|
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
|
|
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
|
-
//
|
|
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 包),或通过
|
|
384
|
-
const
|
|
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 &&
|
|
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'
|