dsh-plugin 1.1.1 → 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 (113) hide show
  1. package/README.en.md +5 -5
  2. package/README.md +5 -5
  3. package/client/client.js +4772 -1659
  4. package/client/client.js.map +1 -1
  5. package/lib/http/routes.js +451 -28
  6. package/lib/index.js +5 -1
  7. package/lib/services/{install-types.js → install/install-types.js} +8 -2
  8. package/lib/services/install/install.js +4 -0
  9. package/lib/services/install/npm-check.js +55 -0
  10. package/lib/services/{npm-resolve.js → install/npm-resolve.js} +11 -5
  11. package/lib/services/{preflight.js → install/preflight.js} +23 -7
  12. package/lib/services/{task-queue.js → install/task-queue.js} +166 -71
  13. package/lib/services/loader.js +60 -1
  14. package/lib/services/log.js +138 -0
  15. package/lib/services/probe.js +157 -0
  16. package/lib/services/{installed-versions.js → profile/installed-versions.js} +5 -1
  17. package/lib/services/{pending-restart.js → profile/pending-restart.js} +1 -1
  18. package/lib/services/{profile.js → profile/profile.js} +59 -4
  19. package/lib/services/{progress.js → profile/progress.js} +4 -0
  20. package/lib/services/settings.js +60 -0
  21. package/lib/types/http/routes.d.ts +1 -1
  22. package/lib/types/index.d.ts +5 -1
  23. package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +13 -4
  24. package/lib/types/services/{install.d.ts → install/install.d.ts} +9 -5
  25. package/lib/types/services/install/npm-check.d.ts +12 -0
  26. package/lib/types/services/{npm-resolve.d.ts → install/npm-resolve.d.ts} +2 -1
  27. package/lib/types/services/{task-queue.d.ts → install/task-queue.d.ts} +5 -1
  28. package/lib/types/services/loader.d.ts +14 -4
  29. package/lib/types/services/log.d.ts +49 -0
  30. package/lib/types/services/probe.d.ts +30 -0
  31. package/lib/types/services/{pending-restart.d.ts → profile/pending-restart.d.ts} +9 -3
  32. package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +6 -2
  33. package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +4 -0
  34. package/lib/types/services/settings.d.ts +24 -0
  35. package/package.json +1 -1
  36. package/src/client/components/PluginHubSection.tsx +346 -121
  37. package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +15 -7
  38. package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +10 -5
  39. package/src/client/components/layout/CatalogControls.tsx +80 -0
  40. package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +8 -4
  41. package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +7 -3
  42. package/src/client/components/layout/SectionTabs.tsx +60 -0
  43. package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +11 -7
  44. package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +9 -5
  45. package/src/client/components/modals/InstalledDetailModal.tsx +193 -0
  46. package/src/client/components/modals/LogsModal.tsx +301 -0
  47. package/src/client/components/modals/LogsPathModal.tsx +161 -0
  48. package/src/client/components/{NotificationsModal.tsx → modals/NotificationsModal.tsx} +51 -20
  49. package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +6 -2
  50. package/src/client/components/{modals.tsx → modals/modals.tsx} +121 -40
  51. package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +18 -6
  52. package/src/client/components/ui/Toggle.tsx +28 -0
  53. package/src/client/components/ui/icons.tsx +343 -0
  54. package/src/client/components/views/CustomInstallView.tsx +250 -0
  55. package/src/client/components/views/DiagnosticsView.tsx +172 -0
  56. package/src/client/components/views/InstalledView.tsx +328 -0
  57. package/src/client/components/views/LogsView.tsx +150 -0
  58. package/src/client/components/views/MarketView.tsx +66 -0
  59. package/src/client/components/views/SettingsView.tsx +273 -0
  60. package/src/client/data/catalog.ts +38 -0
  61. package/src/client/data/host.ts +107 -0
  62. package/src/client/data/hub.ts +41 -0
  63. package/src/client/hooks/useCatalog.ts +77 -117
  64. package/src/client/hooks/useIncrementalList.ts +46 -0
  65. package/src/client/hooks/useLanguage.ts +40 -0
  66. package/src/client/hooks/useSettings.ts +90 -0
  67. package/src/client/hooks/useTaskQueue.ts +168 -47
  68. package/src/client/index.tsx +4 -0
  69. package/src/client/locales.ts +365 -39
  70. package/src/client/logic/constants.ts +87 -0
  71. package/src/client/{lib → logic}/failures.ts +99 -74
  72. package/src/client/{lib → logic}/format.ts +4 -0
  73. package/src/client/logic/install-command.ts +56 -0
  74. package/src/client/logic/installed.ts +207 -0
  75. package/src/client/logic/normalize.ts +52 -0
  76. package/src/client/{lib → logic}/renderMarkdown.ts +4 -0
  77. package/src/client/logic/urls.ts +125 -0
  78. package/src/client/styles/CustomInstallView.module.css +191 -0
  79. package/src/client/styles/DiagnosticsView.module.css +231 -0
  80. package/src/client/styles/Dropdown.module.css +46 -41
  81. package/src/client/styles/Header.module.css +105 -133
  82. package/src/client/styles/InstalledView.module.css +447 -0
  83. package/src/client/styles/List.module.css +11 -1
  84. package/src/client/styles/LogsView.module.css +503 -0
  85. package/src/client/styles/Modal.module.css +357 -71
  86. package/src/client/styles/SectionTabs.module.css +142 -0
  87. package/src/client/styles/SettingsView.module.css +232 -0
  88. package/src/client/styles/Toggle.module.css +60 -0
  89. package/src/client/styles/tokens.module.css +6 -1
  90. package/src/client/types.ts +28 -4
  91. package/src/server/http/routes.ts +426 -28
  92. package/src/server/index.ts +5 -1
  93. package/src/server/services/{install-types.ts → install/install-types.ts} +13 -5
  94. package/src/server/services/{install.ts → install/install.ts} +9 -5
  95. package/src/server/services/install/npm-check.ts +53 -0
  96. package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +11 -5
  97. package/src/server/services/{preflight.ts → install/preflight.ts} +24 -7
  98. package/src/server/services/{task-queue.ts → install/task-queue.ts} +168 -68
  99. package/src/server/services/loader.ts +59 -1
  100. package/src/server/services/log.ts +167 -0
  101. package/src/server/services/probe.ts +157 -0
  102. package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +5 -1
  103. package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +9 -3
  104. package/src/server/services/{profile.ts → profile/profile.ts} +58 -4
  105. package/src/server/services/{progress.ts → profile/progress.ts} +4 -0
  106. package/src/server/services/settings.ts +79 -0
  107. package/lib/services/install.js +0 -4
  108. package/src/client/components/CatalogControls.tsx +0 -104
  109. package/src/client/components/icons.tsx +0 -125
  110. package/src/client/lib/catalog.ts +0 -272
  111. package/src/client/lib/env.ts +0 -16
  112. /package/lib/types/services/{preflight.d.ts → install/preflight.d.ts} +0 -0
  113. /package/lib/types/services/{installed-versions.d.ts → profile/installed-versions.d.ts} +0 -0
@@ -1,7 +1,14 @@
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
  */
9
+ import { readFileSync } from 'node:fs'
10
+ import { join } from 'node:path'
11
+ import { profileDirectory } from './profile/profile.ts'
5
12
 
6
13
  /**
7
14
  * 运行中 loader 的最小接口:卸载成功后即时移除条目用。
@@ -32,6 +39,19 @@ export function dumpLoaderEntries(loader: LoaderHandle | undefined): Array<{ id:
32
39
  return entries
33
40
  }
34
41
 
42
+ /**
43
+ * 判断某 npm 包名是否已加载进运行中 loader(宽匹配:条目名存在别名变体)。
44
+ * 官方 `ctx.loader.entries()` 遍历整棵树(reference.md §1.3),条目名 == 插件作者声明的名字。
45
+ * 安装完成但未重启时条目不在 loader → 返回 false,UI 据此标「待重启/未加载」。
46
+ */
47
+ export function isEntryLoaded(loader: LoaderHandle | undefined, name: string): boolean {
48
+ if (!loader) return false
49
+ for (const entry of Array.from(loader.entries())) {
50
+ if (nameMatches(entry.options?.name, name)) return true
51
+ }
52
+ return false
53
+ }
54
+
35
55
  /**
36
56
  * 卸载目标(npm 包名,如 `@scope/widget`)与 loader 条目名的匹配。
37
57
  * loader 条目名由插件作者/工具写入,形式不可控,常见变体:
@@ -39,7 +59,8 @@ export function dumpLoaderEntries(loader: LoaderHandle | undefined): Array<{ id:
39
59
  * - 去 `@` 前缀:`scope/widget`
40
60
  * - 去 scope 的短名:`widget`
41
61
  * - scope 分隔符变 `-`:`scope-widget`
42
- * 因此归一化后比较全串与短名两个维度。profile 内包名唯一,宽松匹配不会误伤其它包。
62
+ * - 包内模块路径:`widget/extensions/dsh/index.js`(bundle patch 的 name 常写成模块路径)
63
+ * 因此归一化后比较全串、短名、路径前缀三个维度。profile 内包名唯一,宽松匹配不会误伤其它包。
43
64
  */
44
65
  function nameMatches(entryName: string | undefined, target: string): boolean {
45
66
  if (!entryName) return false
@@ -56,6 +77,12 @@ function nameMatches(entryName: string | undefined, target: string): boolean {
56
77
  // 3) 反向变体:目标短名可能就是条目的完整形式(条目 `scope-widget`,
57
78
  // 目标短名 `widget` 是它的尾段)——用「条目名以短名结尾」收口,避免误伤。
58
79
  if (tShort.length >= 2 && e.endsWith(`-${tShort}`)) return true
80
+ // 4) 条目名是目标包内的模块路径(`aegis/extensions/dsh/index.js` 属于包 `aegis`,
81
+ // `@scope/pkg/dist/x.js` 属于 `@scope/pkg`):归一化会吞掉路径分隔符,
82
+ // 必须在归一化前用原始形态做「target/」前缀判断(大小写不敏感)。
83
+ const rawE = entryName.trim().toLowerCase()
84
+ const rawT = target.trim().toLowerCase()
85
+ if (rawE === rawT || rawE.startsWith(`${rawT}/`)) return true
59
86
  return false
60
87
  }
61
88
 
@@ -115,3 +142,34 @@ export async function removeLoadedEntry(loader: LoaderHandle, name: string): Pro
115
142
  console.log(`[hub-uninstall] disable ${name} -> ${result ? 'ok/clean' : 'timed out or failed'}`)
116
143
  return result
117
144
  }
145
+
146
+ /**
147
+ * 判断某已安装包是否是真正的 dsh 插件(宿主会加载 / 值得提示「待重启」):
148
+ * 1) 包名已写进 profile 的 `dsh.profile.bundles`(宿主启动时按清单加载);
149
+ * 2) 或包内 package.json 声明了 dsh 配置(`dsh.bundle` / `dsh.profile` 等)或 dsh 相关 keywords。
150
+ * 两者都满足不了(如 GitHub 官方示例仓库 octocat/Hello-World)说明它不是 dsh 插件,
151
+ * 装上也不会被宿主加载——UI 据此不再提示「待重启」,避免误导用户反复重启一个不会生效的东西。
152
+ */
153
+ export function isDshPlugin(profile: string, name: string): boolean {
154
+ // 1) profile 级 bundles 清单:包名在清单里 → 宿主启动会加载它
155
+ try {
156
+ const profilePkg = JSON.parse(readFileSync(join(profileDirectory(profile), 'package.json'), 'utf8')) as {
157
+ dsh?: { profile?: { bundles?: unknown } }
158
+ }
159
+ if (Array.isArray(profilePkg.dsh?.profile?.bundles)) {
160
+ if ((profilePkg.dsh.profile.bundles as unknown[]).includes(name)) return true
161
+ }
162
+ } catch { /* profile 读取失败按无 bundles 处理 */ }
163
+ // 2) 包内声明:dsh 字段(bundle patch / profile 配置)或 dsh 关键字
164
+ try {
165
+ const pkg = JSON.parse(readFileSync(join(profileDirectory(profile), 'node_modules', name, 'package.json'), 'utf8')) as {
166
+ dsh?: unknown
167
+ keywords?: unknown
168
+ }
169
+ if (pkg.dsh) return true
170
+ if (Array.isArray(pkg.keywords)) {
171
+ if ((pkg.keywords as unknown[]).some((k) => typeof k === 'string' && /^(?:dsh|dsh-plugin)$/i.test(k))) return true
172
+ }
173
+ } catch { /* 包缺失 / 无 package.json → 不是 dsh 插件 */ }
174
+ return false
175
+ }
@@ -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
+ }
@@ -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,12 +1,18 @@
1
- /** 待重启列表的内存态管理(宿主进程一重启自然清空)。 */
2
- import type { PendingRestart } from './install-types.ts'
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
+ import type { PendingRestart } from '../install/install-types.ts'
3
9
 
4
10
  const pendingRestarts: PendingRestart[] = []
5
11
 
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,11 +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
+ *
2
6
  * 宿主 profile 与目标校验工具:解析当前 profile、目录定位、安装目标语法校验,
3
7
  * 以及 pnpm allowBuilds 白名单的写入(失败恢复路径)。
4
8
  */
5
9
  import { readFileSync, writeFileSync } from 'node:fs'
6
10
  import { homedir } from 'node:os'
7
11
  import { join } from 'node:path'
8
- import { PACKAGE_RE, REPO_RE } from './install-types.ts'
12
+ import { PACKAGE_RE, REPO_RE } from '../install/install-types.ts'
9
13
 
10
14
  /** Resolve the active profile from the booted CLI args, falling back to `web`. */
11
15
  export function readProfileArg(fallback = 'web'): string {
@@ -19,10 +23,60 @@ 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
+ /**
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
+
62
+ /** Extract an owner/repo identity from a catalog value or an installed Git spec. */
63
+ export function githubRepoOf(value: string): string | null {
64
+ if (typeof value !== 'string') return null
65
+ const input = value.trim()
66
+ if (REPO_RE.test(input)) return input
67
+ const patterns = [
68
+ /^github:([^/]+)\/([^/]+)$/i,
69
+ /^(?:git\+)?https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
70
+ /^(?:git\+)?ssh:\/\/(?:git@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
71
+ /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
72
+ ]
73
+ for (const pattern of patterns) {
74
+ const match = pattern.exec(input)
75
+ if (match === null) continue
76
+ const repo = `${match[1]}/${match[2]}`
77
+ if (REPO_RE.test(repo)) return repo
78
+ }
79
+ return null
26
80
  }
27
81
 
28
82
  /** Validate an npm package name (uninstall target grammar). */
@@ -32,7 +86,7 @@ export function validPackageName(name: string): boolean {
32
86
 
33
87
  /**
34
88
  * The exact `allowBuilds` key pnpm printed in its
35
- * `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` hint. For `github:` installs pnpm
89
+ * `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` hint. For Git installs pnpm
36
90
  * matches against `name@<resolved fetch URL>` — the URL it actually fetches,
37
91
  * e.g. `https://codeload.github.com/<owner>/<repo>/tar.gz/<commit>` — not the
38
92
  * `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
@@ -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, githubTarget, parseAllowBuildsKey, readProfileArg, validPackageName } from './profile.js';
4
- export { activeTask, cancelTask, getTask, hasQueuedTarget, hasRunningTask, runPluginMutation, startPluginMutation } from './task-queue.js';