dsh-plugin 1.1.2 → 1.2.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.
Files changed (110) hide show
  1. package/client/client.js +4469 -1445
  2. package/client/client.js.map +1 -1
  3. package/lib/http/routes.js +438 -22
  4. package/lib/index.js +1 -1
  5. package/lib/services/install/install.js +4 -0
  6. package/lib/services/install/npm-check.js +55 -0
  7. package/lib/services/{npm-resolve.js → install/npm-resolve.js} +7 -5
  8. package/lib/services/{preflight.js → install/preflight.js} +15 -4
  9. package/lib/services/{task-queue.js → install/task-queue.js} +155 -66
  10. package/lib/services/loader.js +56 -1
  11. package/lib/services/log.js +138 -0
  12. package/lib/services/probe.js +157 -0
  13. package/lib/services/{profile.js → profile/profile.js} +29 -1
  14. package/lib/services/settings.js +60 -0
  15. package/lib/types/http/routes.d.ts +1 -1
  16. package/lib/types/index.d.ts +1 -1
  17. package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +3 -0
  18. package/lib/types/services/{install.d.ts → install/install.d.ts} +5 -5
  19. package/lib/types/services/install/npm-check.d.ts +12 -0
  20. package/lib/types/services/{npm-resolve.d.ts → install/npm-resolve.d.ts} +2 -1
  21. package/lib/types/services/{task-queue.d.ts → install/task-queue.d.ts} +5 -1
  22. package/lib/types/services/loader.d.ts +14 -8
  23. package/lib/types/services/log.d.ts +49 -0
  24. package/lib/types/services/probe.d.ts +30 -0
  25. package/lib/types/services/{pending-restart.d.ts → profile/pending-restart.d.ts} +1 -1
  26. package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +2 -0
  27. package/lib/types/services/settings.d.ts +24 -0
  28. package/package.json +1 -1
  29. package/src/client/components/PluginHubSection.tsx +341 -120
  30. package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +11 -7
  31. package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +5 -4
  32. package/src/client/components/layout/CatalogControls.tsx +80 -0
  33. package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +4 -4
  34. package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +3 -3
  35. package/src/client/components/layout/SectionTabs.tsx +60 -0
  36. package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +7 -7
  37. package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +5 -5
  38. package/src/client/components/modals/InstalledDetailModal.tsx +193 -0
  39. package/src/client/components/modals/LogsModal.tsx +301 -0
  40. package/src/client/components/modals/LogsPathModal.tsx +161 -0
  41. package/src/client/components/{NotificationsModal.tsx → modals/NotificationsModal.tsx} +47 -20
  42. package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +2 -2
  43. package/src/client/components/{modals.tsx → modals/modals.tsx} +117 -40
  44. package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +14 -6
  45. package/src/client/components/ui/Toggle.tsx +28 -0
  46. package/src/client/components/ui/icons.tsx +343 -0
  47. package/src/client/components/views/CustomInstallView.tsx +250 -0
  48. package/src/client/components/views/DiagnosticsView.tsx +172 -0
  49. package/src/client/components/views/InstalledView.tsx +328 -0
  50. package/src/client/components/views/LogsView.tsx +150 -0
  51. package/src/client/components/views/MarketView.tsx +66 -0
  52. package/src/client/components/views/SettingsView.tsx +273 -0
  53. package/src/client/data/catalog.ts +38 -0
  54. package/src/client/data/host.ts +107 -0
  55. package/src/client/data/hub.ts +41 -0
  56. package/src/client/hooks/useCatalog.ts +71 -115
  57. package/src/client/hooks/useIncrementalList.ts +46 -0
  58. package/src/client/hooks/useLanguage.ts +40 -0
  59. package/src/client/hooks/useSettings.ts +90 -0
  60. package/src/client/hooks/useTaskQueue.ts +156 -39
  61. package/src/client/locales.ts +358 -38
  62. package/src/client/logic/constants.ts +87 -0
  63. package/src/client/{lib → logic}/failures.ts +95 -74
  64. package/src/client/logic/install-command.ts +56 -0
  65. package/src/client/logic/installed.ts +207 -0
  66. package/src/client/logic/normalize.ts +52 -0
  67. package/src/client/logic/urls.ts +125 -0
  68. package/src/client/styles/CustomInstallView.module.css +191 -0
  69. package/src/client/styles/DiagnosticsView.module.css +231 -0
  70. package/src/client/styles/Dropdown.module.css +42 -41
  71. package/src/client/styles/Header.module.css +100 -132
  72. package/src/client/styles/InstalledView.module.css +447 -0
  73. package/src/client/styles/List.module.css +6 -0
  74. package/src/client/styles/LogsView.module.css +503 -0
  75. package/src/client/styles/Modal.module.css +352 -70
  76. package/src/client/styles/SectionTabs.module.css +142 -0
  77. package/src/client/styles/SettingsView.module.css +232 -0
  78. package/src/client/styles/Toggle.module.css +60 -0
  79. package/src/client/styles/tokens.module.css +1 -0
  80. package/src/client/types.ts +23 -3
  81. package/src/server/http/routes.ts +414 -22
  82. package/src/server/index.ts +1 -1
  83. package/src/server/services/{install-types.ts → install/install-types.ts} +3 -1
  84. package/src/server/services/{install.ts → install/install.ts} +5 -5
  85. package/src/server/services/install/npm-check.ts +53 -0
  86. package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +7 -5
  87. package/src/server/services/{preflight.ts → install/preflight.ts} +16 -4
  88. package/src/server/services/{task-queue.ts → install/task-queue.ts} +157 -63
  89. package/src/server/services/loader.ts +55 -1
  90. package/src/server/services/log.ts +167 -0
  91. package/src/server/services/probe.ts +157 -0
  92. package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +1 -1
  93. package/src/server/services/{profile.ts → profile/profile.ts} +31 -1
  94. package/src/server/services/settings.ts +79 -0
  95. package/lib/services/install.js +0 -4
  96. package/src/client/components/CatalogControls.tsx +0 -108
  97. package/src/client/components/icons.tsx +0 -129
  98. package/src/client/lib/catalog.ts +0 -293
  99. package/src/client/lib/env.ts +0 -20
  100. /package/lib/services/{install-types.js → install/install-types.js} +0 -0
  101. /package/lib/services/{installed-versions.js → profile/installed-versions.js} +0 -0
  102. /package/lib/services/{pending-restart.js → profile/pending-restart.js} +0 -0
  103. /package/lib/services/{progress.js → profile/progress.js} +0 -0
  104. /package/lib/types/services/{preflight.d.ts → install/preflight.d.ts} +0 -0
  105. /package/lib/types/services/{installed-versions.d.ts → profile/installed-versions.d.ts} +0 -0
  106. /package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +0 -0
  107. /package/src/client/{lib → logic}/format.ts +0 -0
  108. /package/src/client/{lib → logic}/renderMarkdown.ts +0 -0
  109. /package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +0 -0
  110. /package/src/server/services/{progress.ts → profile/progress.ts} +0 -0
@@ -0,0 +1,167 @@
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
+ * 系统日志:把关键操作以 JSONL 追加到 `~/.dsh/profiles/<profile>/hub.log`。
7
+ *
8
+ * 每一条日志 = 级别(严重程度)× 类别(功能域):
9
+ * level debug / info / success / warn / error —— 区分报错与正常
10
+ * category install(安装)/ uninstall(卸载)/ update(更新,预留)/
11
+ * diagnostics(诊断)/ settings(设置)/ system(系统)
12
+ * 查看器按级别与类别过滤,就能针对性排查「安装时哪步不兼容」还是
13
+ * 「卸载时哪步不兼容」。读取时超行数上限就地裁剪,防文件无限膨胀;
14
+ * 写入失败绝不影响主流程(日志是辅助设施)。
15
+ */
16
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
17
+ import { dirname, join, resolve } from 'node:path'
18
+ import { homedir } from 'node:os'
19
+ import { loadSettings } from './settings.ts'
20
+
21
+ export type LogCategory = 'install' | 'uninstall' | 'update' | 'diagnostics' | 'settings' | 'system'
22
+ export type LogLevel = 'debug' | 'info' | 'success' | 'warn' | 'error'
23
+
24
+ export interface LogEntry {
25
+ /** 毫秒时间戳 */
26
+ at: number
27
+ level: LogLevel
28
+ category: LogCategory
29
+ /** 机器可读事件码,如 install.start / settings.update */
30
+ event: string
31
+ /** 人类可读描述 */
32
+ message: string
33
+ }
34
+
35
+ /** 日志保留上限:超过后在读取时裁剪为最近 MAX_LOG_LINES 条 */
36
+ export const MAX_LOG_LINES = 2000
37
+
38
+ const CATEGORIES: LogCategory[] = ['install', 'uninstall', 'update', 'diagnostics', 'settings', 'system']
39
+
40
+ export interface ReadLogOptions {
41
+ /** 分页偏移(过滤后倒序) */
42
+ offset?: number
43
+ limit?: number
44
+ category?: LogCategory | 'all'
45
+ level?: LogLevel | 'all'
46
+ /** 关键词:大小写不敏感,匹配事件码或描述 */
47
+ query?: string
48
+ }
49
+
50
+ export interface ReadLogResult {
51
+ /** 倒序(最新在前) */
52
+ entries: LogEntry[]
53
+ /** 过滤后的总条数(分页前) */
54
+ total: number
55
+ }
56
+
57
+ function profileDirectory(profile: string): string {
58
+ return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile)
59
+ }
60
+
61
+ /** `~` 开头路径展开为用户主目录(Node 不自动展开)。 */
62
+ function expandHome(p: string): string {
63
+ if (p === '~') return homedir()
64
+ if (p.startsWith('~/') || p.startsWith('~\\')) return join(homedir(), p.slice(2))
65
+ return p
66
+ }
67
+
68
+ /**
69
+ * 把用户填写的日志位置覆盖解析成实际文件路径:
70
+ * 以 .log 结尾视为文件本身,否则视为目录并在其中写 hub.log;相对路径按当前目录归一化。
71
+ */
72
+ export function customLogFile(override: string): string {
73
+ const trimmed = override.trim()
74
+ const expanded = expandHome(trimmed)
75
+ const file = /\.log$/i.test(expanded) ? expanded : join(expanded, 'hub.log')
76
+ return resolve(file)
77
+ }
78
+
79
+ /**
80
+ * 真正默认的日志文件路径:`~/.dsh/profiles/<profile>/hub.log`(不受设置覆盖影响)。
81
+ * 弹窗「恢复默认」、目录选择器起点等需要「未自定义时的位置」都从这里取 ——
82
+ * 不同机器/平台(用户主目录不同)这个值都不同,必须动态计算而非写死。
83
+ */
84
+ export function defaultLogFilePath(profile: string): string {
85
+ return join(profileDirectory(profile), 'hub.log')
86
+ }
87
+
88
+ /**
89
+ * 当前生效的日志文件路径:设置里填了 logPath 用自定义位置(目录或文件),
90
+ * 否则默认 `~/.dsh/profiles/<profile>/hub.log`。默认位置永远合法可用,无需用户干预。
91
+ */
92
+ export function logFilePath(profile: string): string {
93
+ const custom = loadSettings(profile).logPath?.trim() ?? ''
94
+ return custom === '' ? defaultLogFilePath(profile) : customLogFile(custom)
95
+ }
96
+
97
+ /** 追加一条系统日志(JSONL 一行一条)。 */
98
+ export function appendLog(profile: string, entry: LogEntry): void {
99
+ try {
100
+ const file = logFilePath(profile)
101
+ mkdirSync(dirname(file), { recursive: true })
102
+ appendFileSync(file, `${JSON.stringify(entry)}\n`)
103
+ } catch {
104
+ // 日志失败不阻断主流程
105
+ }
106
+ }
107
+
108
+ /** 兼容旧格式:早期无 category(level 用 info/ok/fail/warn),归一化到新版。 */
109
+ function normalize(raw: unknown): LogEntry | null {
110
+ if (typeof raw !== 'object' || raw === null) return null
111
+ const e = raw as { at?: unknown; level?: unknown; category?: unknown; event?: unknown; message?: unknown }
112
+ if (typeof e.at !== 'number') return null
113
+ let level: LogLevel = 'info'
114
+ if (e.level === 'ok' || e.level === 'success') level = 'success'
115
+ else if (e.level === 'fail' || e.level === 'error') level = 'error'
116
+ else if (e.level === 'warn') level = 'warn'
117
+ else if (e.level === 'debug') level = 'debug'
118
+ const category: LogCategory = CATEGORIES.includes(e.category as LogCategory) ? e.category as LogCategory : 'system'
119
+ return {
120
+ at: e.at,
121
+ level,
122
+ category,
123
+ event: typeof e.event === 'string' ? e.event : '',
124
+ message: typeof e.message === 'string' ? e.message : '',
125
+ }
126
+ }
127
+
128
+ /** 读取系统日志(按时间正序,最新在末尾),超上限就地裁剪;支持过滤与分页。 */
129
+ export function readLog(profile: string, opts: ReadLogOptions = {}): ReadLogResult {
130
+ const { offset = 0, limit = 200 } = opts
131
+ try {
132
+ const file = logFilePath(profile)
133
+ const raw = readFileSync(file, 'utf8')
134
+ let lines = raw.split('\n').filter((l) => l.trim() !== '')
135
+ if (lines.length > MAX_LOG_LINES) {
136
+ lines = lines.slice(-MAX_LOG_LINES)
137
+ writeFileSync(file, `${lines.join('\n')}\n`)
138
+ }
139
+ // 正序解析后倒序(最新在前)再过滤分页
140
+ let entries: LogEntry[] = []
141
+ for (const line of lines) {
142
+ let parsed: unknown = null
143
+ try {
144
+ parsed = JSON.parse(line)
145
+ } catch {
146
+ continue
147
+ }
148
+ const entry = normalize(parsed)
149
+ if (entry !== null) entries.push(entry)
150
+ }
151
+ entries.reverse()
152
+ const needle = opts.query?.trim().toLowerCase() ?? ''
153
+ if (opts.category !== undefined && opts.category !== 'all') {
154
+ entries = entries.filter((e) => e.category === opts.category)
155
+ }
156
+ if (opts.level !== undefined && opts.level !== 'all') {
157
+ entries = entries.filter((e) => e.level === opts.level)
158
+ }
159
+ if (needle !== '') {
160
+ entries = entries.filter((e) => e.event.toLowerCase().includes(needle) || e.message.toLowerCase().includes(needle))
161
+ }
162
+ const total = entries.length
163
+ return { entries: entries.slice(offset, offset + limit), total }
164
+ } catch {
165
+ return { entries: [], total: 0 }
166
+ }
167
+ }
@@ -0,0 +1,157 @@
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
+ * 连通性探测:对单个 HTTPS 目标发 GET,测速并返回 HTTP 状态码。
7
+ *
8
+ * 实现方式:spawn 系统 curl 子进程并注入代理环境变量(HTTP_PROXY/HTTPS_PROXY),
9
+ * 与安装通道(pnpm/git 子进程)使用同一套代理环境。
10
+ *
11
+ * 代理来源由调用方决定(routes.ts):设置里的代理 → 系统代理 → 环境变量 → 直连。
12
+ * systemProxy() 读取操作系统级代理(macOS scutil / Windows 注册表),
13
+ * 使系统代理对 Node 进程可见。
14
+ */
15
+ import { spawn, spawnSync } from 'node:child_process'
16
+
17
+ export interface ProbeResult {
18
+ /** 是否可达(HTTP 状态码 100–399) */
19
+ ok: boolean
20
+ /** 毫秒耗时;失败为 null */
21
+ ms: number | null
22
+ /** HTTP 状态码;失败为 null */
23
+ status: number | null
24
+ }
25
+
26
+ const probeFail = (): ProbeResult => ({ ok: false, ms: null, status: null })
27
+
28
+ /** macOS 系统代理:scutil --proxy 输出里的 HTTP(S) 代理;未开启返回 null。 */
29
+ function macSystemProxy(): string | null {
30
+ try {
31
+ const out = spawnSync('scutil', ['--proxy'], { encoding: 'utf8', timeout: 1500 })
32
+ if (out.status !== 0) return null
33
+ const txt = out.stdout ?? ''
34
+ const val = (key: string): string | undefined => {
35
+ const m = new RegExp(`^\\s*${key}\\s*:\\s*(.+)$`, 'm').exec(txt)
36
+ const v = m?.[1]?.trim()
37
+ return v !== undefined && v !== '' ? v : undefined
38
+ }
39
+ const enabled = (val('HTTPSEnable') ?? val('HTTPEnable')) === '1'
40
+ const host = val('HTTPSProxy') ?? val('HTTPProxy')
41
+ if (!enabled || host === undefined) return null
42
+ const port = val('HTTPSPort') ?? val('HTTPPort') ?? '80'
43
+ if (!/^\d+$/.test(port)) return null
44
+ return `http://${host}:${port}`
45
+ } catch {
46
+ return null
47
+ }
48
+ }
49
+
50
+ /** Windows 系统代理:WinINET Internet 设置注册表;未开启返回 null。 */
51
+ function winSystemProxy(): string | null {
52
+ try {
53
+ const key = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'
54
+ const enable = spawnSync('reg', ['query', key, '/v', 'ProxyEnable'], { encoding: 'utf8', timeout: 1500 })
55
+ const server = spawnSync('reg', ['query', key, '/v', 'ProxyServer'], { encoding: 'utf8', timeout: 1500 })
56
+ if (!/0x1/i.test(enable.stdout ?? '')) return null
57
+ const m = /([^:\s=]+):(\d+)/.exec(server.stdout ?? '')
58
+ if (m === null) return null
59
+ return `http://${m[1]}:${m[2]}`
60
+ } catch {
61
+ return null
62
+ }
63
+ }
64
+
65
+ /**
66
+ * 操作系统级代理(macOS 系统网络设置 / Windows Internet 设置)。
67
+ * Node 内置 http(s) 不读系统代理,浏览器挂的代理 Node 看不见 —— 这里读出来
68
+ * 作为默认代理,保证「浏览器能开、安装/诊断就能通」。
69
+ * Linux 没有统一的系统代理入口,返回 null(交给环境变量 / 设置里的代理)。
70
+ */
71
+ export function systemProxy(): string | null {
72
+ if (process.platform === 'darwin') return macSystemProxy()
73
+ if (process.platform === 'win32') return winSystemProxy()
74
+ return null
75
+ }
76
+
77
+ /**
78
+ * curl 子进程探测:spawn curl -s -w '%{http_code}' 并注入代理 env。
79
+ * curl 读 HTTP_PROXY/HTTPS_PROXY 是原生行为,与 pnpm/git 安装通道同一套代理口径;
80
+ * 失败(curl 不存在 / 连接失败 / 超时)都归为不可达,全程不抛错。
81
+ */
82
+ function curlProbe(url: string, proxy: string, timeoutMs: number): Promise<ProbeResult> {
83
+ return new Promise((resolve) => {
84
+ const started = Date.now()
85
+ const env: NodeJS.ProcessEnv = { ...process.env }
86
+ if (proxy !== '') {
87
+ env.HTTP_PROXY = proxy
88
+ env.HTTPS_PROXY = proxy
89
+ env.http_proxy = proxy
90
+ env.https_proxy = proxy
91
+ }
92
+ const discard = process.platform === 'win32' ? 'NUL' : '/dev/null'
93
+ const child = spawn('curl', [
94
+ '-s', '-o', discard, '-w', '%{http_code}',
95
+ '--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))),
96
+ url,
97
+ ], { env })
98
+ let code = ''
99
+ child.stdout.on('data', (c: Buffer) => { code += c.toString() })
100
+ let done = false
101
+ const finish = (r: ProbeResult) => {
102
+ if (done) return
103
+ done = true
104
+ clearTimeout(timer)
105
+ resolve(r)
106
+ }
107
+ child.on('error', () => finish(probeFail()))
108
+ child.on('close', () => {
109
+ const status = /^\d+$/.test(code.trim()) ? Number(code.trim()) : null
110
+ finish({ ok: status !== null && status >= 100 && status < 400, ms: Date.now() - started, status })
111
+ })
112
+ // 超时兜底:curl --max-time 到点自行退出,此处再设一个进程级超时防止悬挂
113
+ const timer = setTimeout(() => { child.kill(); finish(probeFail()) }, timeoutMs + 1000)
114
+ })
115
+ }
116
+
117
+ /**
118
+ * 探测一个 HTTPS 目标:proxy 非空注入给 curl 子进程走代理,否则直连。
119
+ * 供系统诊断 /diagnostics 使用;目标 URL 非法返回不可达。
120
+ */
121
+ export function probeUrl(url: string, proxy: string, timeoutMs: number): Promise<ProbeResult> {
122
+ let target: URL
123
+ try { target = new URL(url) } catch { return Promise.resolve(probeFail()) }
124
+ return curlProbe(target.href, proxy, timeoutMs)
125
+ }
126
+
127
+ /**
128
+ * curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入)。
129
+ * 供服务端 /catalog 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
130
+ * 使「目录数据请求走设置里的代理」与 npm / git 安装通道口径一致。
131
+ * 失败(curl 不存在 / 连接失败 / 超时 / 非零退出)时 ok=false,全程不抛错。
132
+ */
133
+ export function fetchViaCurl(url: string, proxy: string, timeoutMs: number): Promise<{ ok: boolean; body: string }> {
134
+ return new Promise((resolve) => {
135
+ const env: NodeJS.ProcessEnv = { ...process.env }
136
+ if (proxy !== '') {
137
+ env.HTTP_PROXY = proxy
138
+ env.HTTPS_PROXY = proxy
139
+ env.http_proxy = proxy
140
+ env.https_proxy = proxy
141
+ }
142
+ const child = spawn('curl', ['-s', '--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))), url], { env })
143
+ let body = ''
144
+ child.stdout.on('data', (c: Buffer) => { body += c.toString() })
145
+ let done = false
146
+ const finish = (ok: boolean) => {
147
+ if (done) return
148
+ done = true
149
+ clearTimeout(timer)
150
+ resolve({ ok, body })
151
+ }
152
+ child.on('error', () => finish(false))
153
+ child.on('close', (code) => finish(code === 0))
154
+ // 超时兜底:curl --max-time 到点自行退出,此处再设一个进程级超时防止悬挂
155
+ const timer = setTimeout(() => { child.kill(); finish(false) }, timeoutMs + 1000)
156
+ })
157
+ }
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * 待重启列表的内存态管理(宿主进程一重启自然清空)。
7
7
  */
8
- import type { PendingRestart } from './install-types.ts'
8
+ import type { PendingRestart } from '../install/install-types.ts'
9
9
 
10
10
  const pendingRestarts: PendingRestart[] = []
11
11
 
@@ -9,7 +9,7 @@
9
9
  import { readFileSync, writeFileSync } from 'node:fs'
10
10
  import { homedir } from 'node:os'
11
11
  import { join } from 'node:path'
12
- import { PACKAGE_RE, REPO_RE } from './install-types.ts'
12
+ import { PACKAGE_RE, REPO_RE } from '../install/install-types.ts'
13
13
 
14
14
  /** Resolve the active profile from the booted CLI args, falling back to `web`. */
15
15
  export function readProfileArg(fallback = 'web'): string {
@@ -29,6 +29,36 @@ export function githubTarget(repo: string): string | null {
29
29
  return `git+https://github.com/${repo}.git`
30
30
  }
31
31
 
32
+ /**
33
+ * DSH 命令行前缀(`dsh plugin [--profile|-p <p>] add <target>`)—— 提取其中的安装目标;
34
+ * 非命令输入原样返回。兼容「直接粘贴完整命令」(防用户输错):--profile 段可选,
35
+ * 支持 `-p` 简写与 `--profile=web` 等号形式,命令大小写不敏感。
36
+ */
37
+ const DSH_PLUGIN_CMD_RE = /^dsh\s+plugin\s+(?:(?:--profile|-p)(?:=|\s+)\S+\s*)?add\s+(.+)$/i
38
+
39
+ export function installTargetOf(value: string): string {
40
+ const input = typeof value === 'string' ? value.trim() : ''
41
+ const match = DSH_PLUGIN_CMD_RE.exec(input)
42
+ return match !== null ? match[1].trim() : input
43
+ }
44
+
45
+ /**
46
+ * npm 全局安装命令(官方 README 的 `npm install -g <pkgs>`,如 dsh-tui 的安装命令)——
47
+ * 提取 -g 之后的包列表。支持 `npm install|i` 与 `-g|--global`;任一包名非法(或格式
48
+ * 不匹配)返回 null,由调用方按「不支持的目标」处理。与客户端 CustomInstallView 同口径。
49
+ */
50
+ const NPM_GLOBAL_CMD_RE = /^npm\s+(?:install|i)\s+(?:-g|--global)\s+(.+)$/i
51
+
52
+ export function globalNpmPackagesOf(value: string): string[] | null {
53
+ const input = typeof value === 'string' ? value.trim() : ''
54
+ const match = NPM_GLOBAL_CMD_RE.exec(input)
55
+ if (match === null) return null
56
+ const packages = match[1].trim().split(/\s+/).filter((p) => p !== '')
57
+ // 包名逐个校验 + 拒绝以 - 开头的 token(npm 会把 --xxx 当参数而非包名,防注入任意 CLI 参数)
58
+ if (packages.length === 0 || packages.some((p) => !validPackageName(p) || p.startsWith('-'))) return null
59
+ return packages
60
+ }
61
+
32
62
  /** Extract an owner/repo identity from a catalog value or an installed Git spec. */
33
63
  export function githubRepoOf(value: string): string | null {
34
64
  if (typeof value !== 'string') return null
@@ -0,0 +1,79 @@
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
+ * Hub settings persistence: one `hub-settings.json` per active profile
7
+ * (next to pnpm-workspace.yaml). All fields optional; missing keys fall
8
+ * back to DEFAULT_SETTINGS so old files stay readable as new options land.
9
+ */
10
+ import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
11
+ import { dirname, join } from 'node:path'
12
+ import { profileDirectory } from './profile/profile.ts'
13
+
14
+ export interface HubSettings {
15
+ /** 启动时检查目录插件更新,发现可更新时在通知中心提示(安装不自动进行) */
16
+ checkUpdatesOnStart: boolean
17
+ /** npm 镜像源 registry 地址(空串 = 官方 https://registry.npmjs.org) */
18
+ npmRegistry: string
19
+ /** HTTP(S) 代理地址,用于 npm / git / 目录请求(空串 = 直连) */
20
+ proxy: string
21
+ /** 启用命令行安装 NPM 包(默认开;关掉后输入 npm 包名安装被拦截) */
22
+ enableNpmInstall: boolean
23
+ /** 启用命令行安装 GitHub 源码(默认开;关掉后输入 GitHub 地址安装被拦截) */
24
+ enableGitInstall: boolean
25
+ /** 启用命令行粘贴 dsh plugin 命令安装(默认开;关掉后命令输入被禁用) */
26
+ enableDshInstall: boolean
27
+ /** 日志存放位置覆盖(空串 = 默认 ~/.dsh/profiles/<profile>/hub.log;可填目录或 .log 文件路径) */
28
+ logPath: string
29
+ }
30
+
31
+ export const DEFAULT_SETTINGS: HubSettings = {
32
+ checkUpdatesOnStart: true,
33
+ npmRegistry: '',
34
+ proxy: '',
35
+ enableNpmInstall: true,
36
+ enableGitInstall: true,
37
+ enableDshInstall: true,
38
+ logPath: '',
39
+ }
40
+
41
+ export function settingsFile(profile: string): string {
42
+ return join(profileDirectory(profile), 'hub-settings.json')
43
+ }
44
+
45
+ /** 读取当前 profile 的设置;文件缺失/损坏时回退默认值(只读路径绝不抛错)。 */
46
+ export function loadSettings(profile: string): HubSettings {
47
+ try {
48
+ const raw = JSON.parse(readFileSync(settingsFile(profile), 'utf8')) as Partial<HubSettings>
49
+ return {
50
+ ...DEFAULT_SETTINGS,
51
+ ...(typeof raw === 'object' && raw !== null ? raw : {}),
52
+ }
53
+ } catch {
54
+ return { ...DEFAULT_SETTINGS }
55
+ }
56
+ }
57
+
58
+ /** 合并并持久化设置(部分更新),返回合并后的完整设置。 */
59
+ export function saveSettings(profile: string, patch: Partial<HubSettings>): HubSettings {
60
+ const next = { ...loadSettings(profile), ...patch }
61
+ const file = settingsFile(profile)
62
+ try {
63
+ mkdirSync(dirname(file), { recursive: true })
64
+ writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`)
65
+ } catch {
66
+ // 持久化失败不阻断本次会话:内存值仍生效,下次写入再试
67
+ }
68
+ return next
69
+ }
70
+
71
+ /** 重置为默认设置:删除设置文件,下次读取全部回退默认值(陈旧/白名单外键一并清除)。 */
72
+ export function resetSettings(profile: string): HubSettings {
73
+ try {
74
+ rmSync(settingsFile(profile), { force: true })
75
+ } catch {
76
+ // 删除失败不阻断:loadSettings 仍回退默认值
77
+ }
78
+ return { ...DEFAULT_SETTINGS }
79
+ }
@@ -1,4 +0,0 @@
1
- export { addPendingRestart, clearPendingRestart, listPendingRestarts } from './pending-restart.js';
2
- export { dumpLoaderEntries, removeLoadedEntry } from './loader.js';
3
- export { addAllowBuildsKey, githubRepoOf, githubTarget, parseAllowBuildsKey, readProfileArg, validPackageName } from './profile.js';
4
- export { activeTask, cancelTask, getTask, hasQueuedTarget, hasRunningTask, runPluginMutation, startPluginMutation } from './task-queue.js';
@@ -1,108 +0,0 @@
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
- * Toolbar above the list: search input + sort dropdown + installed / not
7
- * installed filter buttons + the notifications entry. All buttons share the
8
- * same size (24px high, like the notification entry button).
9
- */
10
- import { createElement as h, Fragment } from 'react'
11
- import type { FormEvent } from 'react'
12
- import styles from '../styles/Header.module.css'
13
- import type { Translate } from '../types.ts'
14
- import { SORTS } from '../lib/catalog.ts'
15
- import type { SortKey } from '../lib/catalog.ts'
16
- import { Dropdown } from './Dropdown.tsx'
17
-
18
- export function CatalogControls({ query, setQuery, sort, setSort, installedFilter, setInstalledFilter, installedCount, notInstalledCount, t, resultText, noticeCount, onOpenNotifications }: {
19
- query: string
20
- setQuery: (value: string) => void
21
- sort: SortKey
22
- setSort: (value: SortKey) => void
23
- installedFilter: 'all' | 'installed' | 'notInstalled'
24
- setInstalledFilter: (value: 'all' | 'installed' | 'notInstalled') => void
25
- installedCount: number
26
- notInstalledCount: number
27
- t: Translate
28
- resultText: string | null
29
- noticeCount: number
30
- onOpenNotifications: () => void
31
- }) {
32
- return h(Fragment, null,
33
- h('div', { className: styles.searchRow },
34
- h('input', {
35
- className: styles.search,
36
- type: 'search',
37
- placeholder: t('search'),
38
- value: query,
39
- spellCheck: false,
40
- onInput: (e: FormEvent<HTMLInputElement>) =>
41
- setQuery((e.target as HTMLInputElement).value),
42
- }),
43
- ),
44
- h('div', { className: styles.controls },
45
- // 筛选结果数:位于排序下拉之前,选中分类后提示「筛选出 N 条插件」;
46
- // 数字单独等宽显示并预留 5 位宽度,数字变化时整行不左右跳动
47
- resultText ? h('span', { className: styles.filterResults },
48
- ...resultText.split(/(\d+)/).map((part, i) =>
49
- /^\d+$/.test(part)
50
- ? h('span', { key: i, className: styles.resultCount }, part)
51
- : part)) : null,
52
- // 全部 / 已安装 / 未安装:互斥单选组,点击直接切换选中态,「全部」即默认恢复项;
53
- // 已安装/未安装计数跟随当前分类,为 0 时置灰不可点
54
- h('button', {
55
- className: installedFilter === 'all'
56
- ? styles.installedBtnActive
57
- : styles.installedBtn,
58
- onClick: () => setInstalledFilter('all'),
59
- title: t('filterAllHint'),
60
- 'aria-pressed': installedFilter === 'all',
61
- }, t('all')),
62
- // 排序下拉框:紧跟「全部」之后,按需切换排序方式
63
- Dropdown<SortKey>({
64
- value: sort,
65
- options: SORTS.map((key) => ({ value: key, label: t(key) })),
66
- onChange: setSort,
67
- }),
68
- h('button', {
69
- className: installedFilter === 'installed'
70
- ? styles.installedBtnActive
71
- : installedCount === 0
72
- ? styles.installedBtnDisabled
73
- : styles.installedBtn,
74
- onClick: () => setInstalledFilter('installed'),
75
- disabled: installedCount === 0,
76
- title: installedCount === 0 ? t('filterInstalledNone') : t('filterInstalledHint'),
77
- 'aria-pressed': installedFilter === 'installed',
78
- },
79
- t('installed'),
80
- h('span', {
81
- className: installedFilter === 'installed' ? styles.segCountActive : styles.segCount,
82
- }, installedCount)),
83
- h('button', {
84
- className: installedFilter === 'notInstalled'
85
- ? styles.installedBtnActive
86
- : notInstalledCount === 0
87
- ? styles.installedBtnDisabled
88
- : styles.installedBtn,
89
- onClick: () => setInstalledFilter('notInstalled'),
90
- disabled: notInstalledCount === 0,
91
- title: notInstalledCount === 0 ? t('filterNotInstalledNone') : t('filterNotInstalledHint'),
92
- 'aria-pressed': installedFilter === 'notInstalled',
93
- },
94
- t('notInstalled'),
95
- h('span', {
96
- className: installedFilter === 'notInstalled' ? styles.segCountActive : styles.segCount,
97
- }, notInstalledCount)),
98
- // 通知中心入口:整行控件最右侧、贴右对齐;有任何通知时右上角悬浮红圈白字计数;
99
- // 按钮文字用短文案(notificationsBtn),避免英文长标题挤占工具栏空间被截断
100
- h('button', {
101
- className: styles.failBtn,
102
- onClick: onOpenNotifications,
103
- title: t('notificationsHint'),
104
- 'aria-label': t('notificationsHint'),
105
- }, t('notificationsBtn'), noticeCount > 0 ? h('span', { className: styles.failBadge }, noticeCount) : null),
106
- ),
107
- )
108
- }