dsh-plugin 0.1.0 → 1.0.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 (64) hide show
  1. package/README.en.md +52 -27
  2. package/README.md +52 -27
  3. package/client/client.js +2148 -229
  4. package/client/client.js.map +1 -1
  5. package/lib/http/routes.js +396 -0
  6. package/lib/index.js +17 -8
  7. package/lib/services/install-types.js +11 -0
  8. package/lib/services/install.js +4 -0
  9. package/lib/services/installed-versions.js +51 -0
  10. package/lib/services/loader.js +98 -0
  11. package/lib/services/pending-restart.js +34 -0
  12. package/lib/services/preflight.js +94 -0
  13. package/lib/services/profile.js +88 -0
  14. package/lib/services/progress.js +50 -0
  15. package/lib/services/task-queue.js +417 -0
  16. package/lib/types/http/routes.d.ts +23 -0
  17. package/lib/types/index.d.ts +27 -0
  18. package/lib/types/services/install-types.d.ts +63 -0
  19. package/lib/types/services/install.d.ts +18 -0
  20. package/lib/types/services/installed-versions.d.ts +14 -0
  21. package/lib/types/services/loader.d.ts +43 -0
  22. package/lib/types/services/pending-restart.d.ts +13 -0
  23. package/lib/types/services/preflight.d.ts +6 -0
  24. package/lib/types/services/profile.d.ts +24 -0
  25. package/lib/types/services/progress.d.ts +17 -0
  26. package/lib/types/services/task-queue.d.ts +76 -0
  27. package/package.json +27 -6
  28. package/src/client/components/CatalogControls.tsx +104 -0
  29. package/src/client/components/CatalogHeader.tsx +78 -0
  30. package/src/client/components/CatalogList.tsx +75 -0
  31. package/src/client/components/CategoryTabs.tsx +35 -0
  32. package/src/client/components/Dropdown.tsx +80 -0
  33. package/src/client/components/NotificationsModal.tsx +230 -0
  34. package/src/client/components/PluginCard.tsx +104 -0
  35. package/src/client/components/PluginHubSection.tsx +339 -0
  36. package/src/client/components/ProgressView.tsx +23 -0
  37. package/src/client/components/icons.tsx +125 -0
  38. package/src/client/components/modals.tsx +365 -0
  39. package/src/client/hooks/useCatalog.ts +192 -0
  40. package/src/client/hooks/useTaskQueue.ts +405 -0
  41. package/src/client/index.tsx +8 -422
  42. package/src/client/lib/catalog.ts +204 -0
  43. package/src/client/lib/env.ts +16 -0
  44. package/src/client/lib/failures.ts +153 -0
  45. package/src/client/lib/format.ts +22 -0
  46. package/src/client/locales.ts +186 -18
  47. package/src/client/styles/Dropdown.module.css +123 -0
  48. package/src/client/styles/Header.module.css +432 -0
  49. package/src/client/styles/List.module.css +324 -0
  50. package/src/client/styles/Modal.module.css +965 -0
  51. package/src/client/styles/tokens.module.css +86 -0
  52. package/src/client/types.ts +80 -0
  53. package/src/server/http/routes.ts +397 -0
  54. package/src/server/index.ts +35 -0
  55. package/src/server/services/install-types.ts +63 -0
  56. package/src/server/services/install.ts +18 -0
  57. package/src/server/services/installed-versions.ts +65 -0
  58. package/src/server/services/loader.ts +110 -0
  59. package/src/server/services/pending-restart.ts +36 -0
  60. package/src/server/services/preflight.ts +97 -0
  61. package/src/server/services/profile.ts +89 -0
  62. package/src/server/services/progress.ts +48 -0
  63. package/src/server/services/task-queue.ts +450 -0
  64. package/src/client/Section.module.css +0 -569
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Persistent install/remove notifications.
3
+ *
4
+ * Every settled task — success or failure — is appended to localStorage the
5
+ * moment the backend reports it, so a result is never lost even when the
6
+ * dialog was dismissed or nobody was watching the progress strip. Newest
7
+ * first, capped at MAX entries. Failures keep their full output for the
8
+ * issue-filing flow; successes are lightweight (no log).
9
+ */
10
+ export interface NotificationRecord {
11
+ id: number
12
+ kind: 'install' | 'uninstall'
13
+ /** true = 成功(轻量记录);false = 失败(message 携带完整错误日志) */
14
+ ok: boolean
15
+ /** owner/repo(可能为空:请求层失败但拿不到仓库时) */
16
+ repo: string
17
+ /** 失败时的完整错误日志;成功记录为空串 */
18
+ message: string
19
+ /** 结束时间(epoch ms) */
20
+ at: number
21
+ }
22
+
23
+ const KEY = 'gro.ngilp-hsd.failure-records'
24
+ const MAX = 50
25
+
26
+ export type FailureKind = 'pnpmIgnoredBuild' | 'pluginPrepare' | 'repo'
27
+
28
+ /**
29
+ * 失败归类,三态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
30
+ * 对用户而言结果都一样 —— 用官方默认安装方式装不上,就是插件仓库的问题,一律引导提 Issue:
31
+ * - pnpmIgnoredBuild:插件依赖里的原生模块构建脚本被 pnpm 默认拦截(如 node-pty,
32
+ * `ERR_PNPM_IGNORED_BUILDS`)。只影响带原生模块的插件,其他插件不受影响 —— 差异在
33
+ * 插件的依赖选择,属插件依赖/打包问题 → 引导去仓库提 Issue(建议改用预编译版本)
34
+ * - pluginPrepare:插件的 prepare/构建脚本实际执行失败(git tarball 常因缺失子模块或
35
+ * 构建产物导致)—— 属插件打包/分发问题,应引导去仓库提 Issue
36
+ * - repo:其余失败(含 git prepare 被 pnpm 白名单拦截等),默认按插件仓库问题引导提 Issue
37
+ */
38
+ export function classifyFailure(message: string): FailureKind {
39
+ // 装后校验拦截(服务端 verifyInstalledEntry 标记):入口文件缺失 = git 分发缺构建产物,
40
+ // 与 pluginPrepare 同类(插件打包/分发问题),引导去仓库提 Issue
41
+ if (/\[packaging\]|entry file missing/i.test(message)) return 'pluginPrepare'
42
+ // 原生模块构建被忽略(IGNORED_BUILDS)要在 prepare 判定之前:dsh 尾部固定提示语带
43
+ // allowBuilds/pnpm-workspace.yaml,不能据此把插件自身问题误判成通用失败
44
+ if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts:/i.test(message)) return 'pnpmIgnoredBuild'
45
+ // 再判 prepare 实际执行失败:只有构建脚本真的跑挂了才是插件问题
46
+ if (/ERR_PNPM_PREPARE_PACKAGE|ELIFECYCLE|Command failed|prepare-guard/i.test(message)) return 'pluginPrepare'
47
+ // 其余失败(含 git prepare 被 pnpm 白名单拦截):官方方式装不上 = 插件仓库的问题,一律提 Issue
48
+ return 'repo'
49
+ }
50
+
51
+ /** 核心行特征:错误代码 / 生命周期脚本失败 / prepare 失败 / 描述性报错(子模块缺失、找不到等)/ 退出与宿主提示信息。 */
52
+ const CORE_LINE_RE = /ERR_[A-Z_]+|ELIFECYCLE|Command failed|prepare-guard|Failed to prepare|exit code|\bprepare\b|pnpm failed in profile|git-hosted plugins build|submodule|not found|cannot find|no such|unable to|fatal|missing|error/i
53
+ /** 提交 issue 时正文里错误摘要的上限字符数。GitHub 请求行上限 8192 字节,
54
+ * 固定模板与 URL 编码开销约 1~2K,核心错误(以 ASCII 日志为主)可安全带到 ~5K;
55
+ * 仍超长时 pluginIssueUrl 会逐档缩小核心预算,最终 URL 不会超限。 */
56
+ export const MAX_CORE_CHARS = 5000
57
+ /** 摘要里单行上限:允许构建 key 等长行也被截短。 */
58
+ const MAX_LINE_CHARS = 400
59
+
60
+ /**
61
+ * 核心错误收集器:从完整安装输出里挑出真正说明问题的行(错误代码、构建脚本失败、
62
+ * 退出与宿主提示),去重后拼接,单行与总量都截断 —— 只把「重点 + 原因」带进 issue 正文,
63
+ * 避免完整日志塞进 URL 导致请求过长。无关键行时退化为「头部 + 尾部」快照。
64
+ * maxChars 可由调用方按最终 URL 长度收紧(pluginIssueUrl 超限时逐档缩小)。
65
+ */
66
+ export function summarizeError(message: string, maxChars: number = MAX_CORE_CHARS): string {
67
+ const seen = new Set<string>()
68
+ const core: string[] = []
69
+ for (const raw of message.split(/\r?\n/)) {
70
+ const line = raw.trim()
71
+ if (!line || !CORE_LINE_RE.test(line)) continue
72
+ const short = line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}…` : line
73
+ if (!seen.has(short)) {
74
+ seen.add(short)
75
+ core.push(short)
76
+ }
77
+ }
78
+ let out: string
79
+ if (core.length === 0) {
80
+ // 无关键行时退化为「头部 + 尾部」快照:优先保尾部完整,头部按预算压缩,总长不超上限
81
+ const tail = message.trimEnd().slice(-1000)
82
+ const sep = '\n…\n'
83
+ const headBudget = Math.max(maxChars - tail.length - sep.length, 0)
84
+ const head = message.slice(0, 500).trim()
85
+ out = `${head.length > headBudget ? head.slice(0, headBudget) : head}${sep}${tail}`
86
+ } else {
87
+ out = core.join('\n')
88
+ }
89
+ return out.length > maxChars ? `${out.slice(0, maxChars)}\n… (truncated)` : out
90
+ }
91
+
92
+ /** 提取首个错误代码(如 ERR_PNPM_PREPARE_PACKAGE),无则 null。 */
93
+ export function coreErrorCode(message: string): string | null {
94
+ const m = message.match(/\[?ERR_[A-Z_]+\]?/)
95
+ return m ? m[0].replace(/^\[|\]$/g, '') : null
96
+ }
97
+
98
+ /** 运行时 localStorage 访问:类型上不依赖 DOM lib(Node 测试环境也能编译),浏览器里取 window。 */
99
+ const storage = (): Storage | undefined =>
100
+ (globalThis as { localStorage?: Storage }).localStorage
101
+
102
+ /** 读取本地通知记录(损坏/不可用时返回空列表,不抛错)。列表最新在前,全部保留,只由「清空」按钮手动移除。 */
103
+ export function loadNotifications(): NotificationRecord[] {
104
+ try {
105
+ const raw = storage()?.getItem(KEY)
106
+ if (!raw) return []
107
+ const list = JSON.parse(raw) as unknown
108
+ if (!Array.isArray(list)) return []
109
+ return list
110
+ .filter((r): r is NotificationRecord =>
111
+ !!r && typeof r === 'object' && typeof (r as { message?: unknown }).message === 'string')
112
+ // 历史失败记录没有 ok 字段:缺省按失败渲染,保证旧数据照常可查
113
+ .map((r) => ({ ...r, ok: r.ok === true }))
114
+ } catch {
115
+ return []
116
+ }
117
+ }
118
+
119
+ function save(list: NotificationRecord[]): void {
120
+ try {
121
+ storage()?.setItem(KEY, JSON.stringify(list))
122
+ } catch {
123
+ /* storage full / unavailable:仅保留内存态,界面仍可查看 */
124
+ }
125
+ }
126
+
127
+ /** 追加一条通知记录并持久化,返回更新后的完整列表(最新在前,超上限裁剪)。
128
+ * 每次成功/失败都各自留痕:同一插件的安装/卸载记录都完整保留、只由「清空」按钮手动移除,
129
+ * 不会因后续操作被自动覆盖清除。 */
130
+ export function addNotification(record: Omit<NotificationRecord, 'id' | 'at'>): NotificationRecord[] {
131
+ const prev = loadNotifications()
132
+ let id = Date.now()
133
+ while (prev.some((r) => r.id === id)) id += 1
134
+ const next = [{ ...record, id, at: id }, ...prev].slice(0, MAX)
135
+ save(next)
136
+ return next
137
+ }
138
+
139
+ /** 记录一次失败:携带完整错误日志,供通知中心查看/复制/提 Issue。 */
140
+ export function addFailure(record: Omit<NotificationRecord, 'id' | 'at' | 'ok'>): NotificationRecord[] {
141
+ return addNotification({ ...record, ok: false })
142
+ }
143
+
144
+ /** 记录一次成功:轻量记录,不带日志。 */
145
+ export function addSuccess(record: Omit<NotificationRecord, 'id' | 'at' | 'ok' | 'message'>): NotificationRecord[] {
146
+ return addNotification({ ...record, ok: true, message: '' })
147
+ }
148
+
149
+ /** 清空全部通知记录,返回空列表。 */
150
+ export function clearNotifications(): NotificationRecord[] {
151
+ save([])
152
+ return []
153
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Presentation formatters shared across the catalog list and the cards.
3
+ */
4
+ import type { Translate } from '../types.ts'
5
+
6
+ /** 星数/分支数的紧凑展示:<1k 原样;千级 1.2k;十万级取整。 */
7
+ export function fmtStars(count?: number): string {
8
+ if (!count || count <= 0) return '0'
9
+ if (count < 1000) return String(count)
10
+ const k = count / 1000
11
+ return `${k >= 100 ? Math.round(k) : k.toFixed(1).replace(/\.0$/, '')}k`
12
+ }
13
+
14
+ /** 相对时间:今天 / N 天前 / N 个月前 / N 年前。 */
15
+ export function relTime(iso: string | undefined, t: Translate): string {
16
+ if (!iso) return ''
17
+ const days = Math.floor((Date.now() - new Date(iso).getTime()) / 86400000)
18
+ if (days <= 0) return t('today')
19
+ if (days < 30) return t('daysAgo', { days })
20
+ if (days < 365) return t('monthsAgo', { months: Math.floor(days / 30) })
21
+ return t('yearsAgo', { years: Math.floor(days / 365) })
22
+ }
@@ -1,27 +1,106 @@
1
1
  /** zh/en dictionaries for the DSH-Plugin Hub settings section. */
2
2
  export const zh = {
3
3
  nav: '插件中心',
4
- title: 'DSH-Plugin Hub for DeepSeek Harness',
4
+ title: 'DSH-Plugin Hub',
5
5
  // 收录/精选数量由组件从官网 /api/stats.json 实时拉取填充({total}/{verified}),不写死
6
- tagline: 'DSH plugin 插件中心:收录 {total} 个 DeepSeek Harness 插件,人工精选验证 {verified},每日更新 · 免费安装',
6
+ tagline: 'DSH-Plugin 插件中心:面向 DeepSeek Harness 插件收录平台,人工审验、来源可溯,免费安装',
7
7
  adBadge: '推荐',
8
- ad: 'DSH-Plugin 插件中心:收录 {total} 个 DeepSeek Harness 插件,其中 {verified} 人工精选验证,每日更新,免费按分类浏览、搜索、下载与安装,人工验证、来源可溯',
8
+ // 紫色广告横幅:突出数量(显眼)+ 更新节奏,与 tagline 职责分开、不重复
9
+ ad: 'DSH-Plugin 插件中心:收录 {total} 款插件,{verified} 款人工精选验证,每日更新',
9
10
  search: '搜索插件名称、描述、标签…',
10
11
  all: '全部',
11
12
  sortStars: 'Star 最多',
13
+ sortForks: 'Fork 最多',
12
14
  sortUpdated: '最近更新',
13
15
  sortNewest: '最新收录',
14
- open: '在浏览器中打开',
15
16
  openHint: '打开 dsh-plugin.org',
16
- openBtn: 'DSH-Plugin 官网',
17
- toEn: 'Switch to English',
17
+ toggleLangHint: '切换界面语言',
18
18
  fork: 'Fork',
19
19
  loading: '正在加载插件数据…',
20
20
  failed: '插件数据加载失败',
21
21
  failedDesc: '请稍后重试,或点击右上角按钮在浏览器中打开插件中心。',
22
22
  retry: '重试',
23
- copy: '复制安装命令',
23
+ install: '安装',
24
24
  toastCopied: '安装命令已复制,去 dsh 终端粘贴即可安装',
25
+ installed: '已安装',
26
+ notInstalled: '未安装',
27
+ filterAllHint: '显示全部插件',
28
+ filterInstalledHint: '筛选当前分类下的已安装插件',
29
+ filterNotInstalledHint: '筛选当前分类下的未安装插件',
30
+ filterInstalledNone: '当前分类下没有已安装插件',
31
+ filterNotInstalledNone: '当前分类下没有未安装插件',
32
+ uninstall: '卸载',
33
+ uninstalling: '卸载中…',
34
+ uninstallTitle: '确认卸载',
35
+ uninstallDesc: '将从当前环境卸载该插件。',
36
+ uninstallDone: '卸载成功',
37
+ uninstallFail: '卸载失败,请稍后重试',
38
+ uninstallResultTitle: '卸载完成',
39
+ uninstallResultDesc: '插件已从当前环境移除。',
40
+ confirmTitle: '确认安装',
41
+ confirmDesc: '安装前请前往插件仓库仔细阅读源码,确认可信后再安装',
42
+ confirmPlugin: '插件',
43
+ confirmSource: '来源',
44
+ confirmCommand: '安装命令',
45
+ copyInstallCommand: '复制安装命令',
46
+ copyCmdLabel: '复制',
47
+ copyUninstallCommand: '复制卸载命令',
48
+ installNow: '直接安装',
49
+ installing: '安装中…',
50
+ installDone: '安装成功',
51
+ installResultTitle: '安装完成',
52
+ installResultDesc: '插件已安装。',
53
+ // 更新语义:已安装插件点「更新」走同一条 add 命令原位覆盖重装,弹窗文案区分安装/更新
54
+ confirmUpdateTitle: '确认更新',
55
+ updateNow: '直接更新',
56
+ updating: '更新中…',
57
+ queuedUpdateTitle: '已加入更新排队',
58
+ updateResultTitle: '更新完成',
59
+ updateResultDesc: '插件已更新到最新版本。',
60
+ installFail: '安装失败',
61
+ confirmCancel: '取消',
62
+ doneBtn: '完成',
63
+ // 结果视图:部分插件需重启后才挂载,提供「立即重启 / 稍后重启」选择
64
+ restartNow: '立即重启',
65
+ restartLater: '稍后重启',
66
+ restarting: '正在重启…',
67
+ restartHint: '部分插件(如插件市场、顶层 bundle)需重启后才会生效。',
68
+ // 待重启:装完没重启的插件常驻提醒,直到宿主真正重启
69
+ restartPendingHint: '安装成功,重启后生效',
70
+ restartPendingHintUninstall: '卸载成功,重启后移除',
71
+ sectionPendingRestart: '待重启',
72
+ sectionInProgress: '进行中',
73
+ // 串行队列:安装/卸载弹窗内可后台关闭,排队任务自动串行执行
74
+ queuedTitle: '已加入安装排队',
75
+ queuedUninstallTitle: '已加入卸载排队',
76
+ queuedHint: '已加入排队,前序任务完成后将自动开始,可关闭本窗口继续浏览。',
77
+ cancelTask: '取消任务',
78
+ cancelling: '正在取消…',
79
+ errorTitle: '操作失败',
80
+ errorTitleInstall: '安装失败',
81
+ errorTitleUninstall: '卸载失败',
82
+ errorPlugin: '插件',
83
+ errorClose: '知道了',
84
+ errorCopy: '复制错误信息',
85
+ errCopied: '错误信息已复制',
86
+ // 任务通知中心:安装/卸载的成败自动落盘,随时可回来查看
87
+ notifications: '通知',
88
+ // 通知入口按钮文字:工具栏空间有限,与弹窗标题(notifications)区分,短文案不截断
89
+ notificationsBtn: '通知',
90
+ notificationsHint: '查看安装/卸载通知(含失败记录)',
91
+ notificationsDesc: '安装、卸载任务的成败都会记录在这里,即使错过提示也能随时回来查看。',
92
+ notificationsEmpty: '暂无通知记录',
93
+ notificationsClear: '清空通知',
94
+ failCopy: '复制完整错误信息',
95
+ failIssueHint: '带着错误日志去作者仓库一键提交 Issue(正文含官网收录链接)',
96
+ failIssueBig: '一键提交 BUG 到 GitHub Issue 为开源作贡献',
97
+ // prepare 构建脚本实际执行失败(git 分发缺子模块/构建产物):插件打包分发问题,应提 Issue
98
+ failPrepareHint: '该插件安装时的构建脚本执行失败(git 分发常缺失子模块或构建产物),属插件打包分发问题。请向作者仓库提交 Issue 反馈。',
99
+ // 安装前预检 / 装后校验拦截([packaging]):git 分发缺构建产物、入口文件不存在 = 插件未适配官方默认安装方式
100
+ failPackagingHint: '该插件不支持官方默认安装方式:其 git 分发缺少构建产物(package.json 声明的入口文件在仓库中不存在),说明作者未适配官方安装流程。请到作者仓库提交 Issue 反馈,请其提交构建产物或发布 npm 版。',
101
+ // 原生依赖构建脚本被 pnpm 默认拦截(如 node-pty 的 ERR_PNPM_IGNORED_BUILDS):只影响带原生模块的插件,属插件依赖打包问题,应提 Issue
102
+ failIgnoredBuild: '该插件依赖了需要编译的原生模块(如 node-pty),pnpm 默认拦截这类构建脚本导致安装失败 —— 其他插件不受影响,属该插件的依赖/打包问题。建议向作者仓库一键反馈,请作者改用预编译版本(如 node-pty-prebuilt-multiarch)。',
103
+ reportIssue: '去插件仓库提交 Issue',
25
104
  detail: '打开详情',
26
105
  noResult: '没有找到匹配的插件',
27
106
  noResultDesc: '换个关键词试试,或前往网站浏览全部插件。',
@@ -29,38 +108,121 @@ export const zh = {
29
108
  daysAgo: '更新于 {days} 天前',
30
109
  monthsAgo: '更新于 {months} 个月前',
31
110
  yearsAgo: '更新于 {years} 年前',
32
- pluginsTotal: '共 {n} 个插件',
33
- filteredCount: '匹配 {n} 个',
111
+ pluginsTotal: '结果 {n} 条插件',
112
+ // 分类按钮不带数量,选中分类后在列表头部显示筛选结果数
113
+ filterResults: '结果 {n} 条插件',
34
114
  more: '还有 {n} 个插件,去网站查看全部',
35
115
  browseAll: '浏览全部 {n} 个插件',
36
116
  verified: '已验证',
117
+ // 版本与更新:有 release 比版本号;无 release 的仓库比仓库最近更新时间(有新提交即提示更新)
118
+ version: '版本',
119
+ update: '可更新',
120
+ updateAvailable: '有更新',
121
+ updateAvailableHint: '检测到更新:有版本号的仓库比对版本号,无版本号的仓库比对仓库最近更新时间',
37
122
  empty: '该分类暂无插件',
38
123
  dataFrom: '数据源 dsh-plugin.org · 每日人工更新',
39
124
  }
40
125
 
41
126
  export const en = {
42
127
  nav: 'Plugin Hub',
43
- title: 'DSH-Plugin Hub for DeepSeek Harness',
128
+ title: 'DSH-Plugin Hub',
44
129
  // Plugin counts are filled from the live /api/stats.json ({total}/{verified}), never hardcoded
45
- tagline: 'DSH plugin marketplace · {total} DeepSeek Harness plugins indexed · {verified} human-verified · updated daily · free to install',
130
+ tagline: 'DSH-Plugin Hub: a human-verified, free marketplace for DeepSeek Harness plugins',
46
131
  adBadge: 'Featured',
47
- ad: 'DSH-Plugin Hub: {total} DeepSeek Harness plugins indexed, {verified} of which are human-verified. Updated daily — browse, search & install for free.',
132
+ ad: 'DSH-Plugin Hub: {total} plugins indexed, {verified} human-verified, updated daily',
48
133
  search: 'Search plugins by name, description, tags…',
49
134
  all: 'All',
50
135
  sortStars: 'Most stars',
136
+ sortForks: 'Most forks',
51
137
  sortUpdated: 'Recently updated',
52
138
  sortNewest: 'Newest added',
53
- open: 'Open in browser',
54
139
  openHint: 'Open dsh-plugin.org',
55
- openBtn: 'dsh-plugin.org',
56
- toZh: 'Switch to Chinese',
140
+ toggleLangHint: 'Switch language',
141
+ githubHint: 'View source on GitHub',
57
142
  fork: 'Fork',
58
143
  loading: 'Loading plugin data…',
59
144
  failed: 'Failed to load plugin data',
60
145
  failedDesc: 'Please retry, or open the hub in your browser with the button above.',
61
146
  retry: 'Retry',
62
- copy: 'Copy install command',
147
+ install: 'Install',
63
148
  toastCopied: 'Install command copied — paste it in your dsh terminal',
149
+ installed: 'Installed',
150
+ notInstalled: 'Not installed',
151
+ filterAllHint: 'Show all plugins',
152
+ filterInstalledHint: 'Filter to installed plugins in this category',
153
+ filterNotInstalledHint: 'Filter to uninstalled plugins in this category',
154
+ filterInstalledNone: 'No installed plugins in this category',
155
+ filterNotInstalledNone: 'No uninstalled plugins in this category',
156
+ uninstall: 'Uninstall',
157
+ uninstalling: 'Removing…',
158
+ uninstallTitle: 'Confirm uninstall',
159
+ uninstallDesc: 'The plugin will be removed from this environment.',
160
+ uninstallDone: 'Uninstalled',
161
+ uninstallFail: 'Removal failed — try again',
162
+ uninstallResultTitle: 'Removed',
163
+ uninstallResultDesc: 'The plugin has been removed from this environment.',
164
+ confirmTitle: 'Confirm install',
165
+ confirmDesc: 'Review the plugin repository source code carefully before installing.',
166
+ confirmPlugin: 'Plugin',
167
+ confirmSource: 'Source',
168
+ confirmCommand: 'Command',
169
+ copyInstallCommand: 'Copy install command',
170
+ copyCmdLabel: 'Copy',
171
+ copyUninstallCommand: 'Copy uninstall command',
172
+ installNow: 'Install',
173
+ installing: 'Installing…',
174
+ installDone: 'Installed',
175
+ installResultTitle: 'Installed',
176
+ installResultDesc: 'The plugin has been installed.',
177
+ // Update semantics: the "update" action re-runs add in place — copy differentiates install vs update
178
+ confirmUpdateTitle: 'Confirm update',
179
+ updateNow: 'Update',
180
+ updating: 'Updating…',
181
+ queuedUpdateTitle: 'Queued for update…',
182
+ updateResultTitle: 'Updated',
183
+ updateResultDesc: 'The plugin has been updated to the latest version.',
184
+ installFail: 'Install failed — copy the command instead',
185
+ confirmCancel: 'Cancel',
186
+ doneBtn: 'Done',
187
+ // Result view: some plugins need a restart to mount — offer restart now / later
188
+ restartNow: 'Restart now',
189
+ restartLater: 'Later',
190
+ restarting: 'Restarting…',
191
+ restartHint: 'Some plugins (such as plugin markets / top-level bundles) only take effect after a restart.',
192
+ restartPendingHint: 'Installed — takes effect after restart',
193
+ restartPendingHintUninstall: 'Removed — cleaned up after restart',
194
+ sectionPendingRestart: 'Pending restart',
195
+ sectionInProgress: 'In progress',
196
+ queuedTitle: 'Queued…',
197
+ queuedUninstallTitle: 'Queued for removal…',
198
+ queuedHint: 'Queued — starts automatically after earlier tasks finish. You may close this window.',
199
+ cancelTask: 'Cancel task',
200
+ cancelling: 'Cancelling…',
201
+ errorTitle: 'Operation failed',
202
+ errorTitleInstall: 'Install failed',
203
+ errorTitleUninstall: 'Removal failed',
204
+ errorPlugin: 'Plugin',
205
+ errorClose: 'Got it',
206
+ errorCopy: 'Copy error',
207
+ errCopied: 'Error copied',
208
+ // Task notification center: install/remove results are auto-logged, viewable later
209
+ notifications: 'Notifications',
210
+ // Entry button label: toolbar space is tight, keep it short so it never truncates; the modal title stays full
211
+ notificationsBtn: 'Notice',
212
+ notificationsHint: 'View install/remove notifications (including failures)',
213
+ notificationsDesc: 'Every install and remove result is logged here — even if you missed the prompt, you can always check back.',
214
+ notificationsEmpty: 'No notifications yet',
215
+ notificationsClear: 'Clear notifications',
216
+ failCopy: 'Copy full log',
217
+ failIssueHint: 'Open a pre-filled issue on the author repo with this error log (includes catalog links)',
218
+ failIssueBig: 'Report this bug to GitHub — contribute to open source',
219
+ // prepare script actually failed (git tarball misses submodules/build output): a packaging issue of the plugin itself — file an issue
220
+ failPrepareHint: 'This plugin failed while running its build scripts during install (git tarballs often miss submodules or build output). This is a packaging issue of the plugin itself — please report it to the author repository.',
221
+ // Pre-install / post-install guard hit ([packaging]): git distribution lacks build output, entry file missing = not adapted to the official default install method
222
+ failPackagingHint: 'This plugin does not support the official default install method: its git distribution lacks the build output (the entry file declared in package.json is not in the repository), so it has not been adapted to the official install flow. Please file an Issue on the author\'s repo asking for committed build output or an npm release.',
223
+ // native-module build scripts ignored by pnpm by default (e.g. node-pty, ERR_PNPM_IGNORED_BUILDS): only affects plugins with native deps — a dependency/packaging issue of the plugin itself, file an issue
224
+ failIgnoredBuild: 'This plugin depends on a native module (e.g. node-pty) whose build script pnpm blocks by default, so the install fails — other plugins are unaffected. This is a dependency/packaging issue of the plugin itself. Please report it to the author repository and ask the author to switch to a prebuilt variant (e.g. node-pty-prebuilt-multiarch).',
225
+ reportIssue: 'Report an Issue to the plugin repo',
64
226
  detail: 'Open details',
65
227
  noResult: 'No plugins match your search',
66
228
  noResultDesc: 'Try another keyword, or browse all plugins on the website.',
@@ -68,11 +230,17 @@ export const en = {
68
230
  daysAgo: 'updated {days}d ago',
69
231
  monthsAgo: 'updated {months}mo ago',
70
232
  yearsAgo: 'updated {years}y ago',
71
- pluginsTotal: '{n} plugins',
72
- filteredCount: '{n} matched',
233
+ pluginsTotal: 'Results: {n} plugins',
234
+ // Category chips carry no counts; the filtered result count shows in the list header
235
+ filterResults: 'Results: {n} plugins',
73
236
  more: '{n} more on the website',
74
237
  browseAll: 'Browse all {n} plugins',
75
238
  verified: 'Verified',
239
+ // Version & updates: repos with a release are compared by tag; repos without one by last push time (new commits → update)
240
+ version: 'Version',
241
+ update: 'Update',
242
+ updateAvailable: 'Update available',
243
+ updateAvailableHint: 'An update was detected: repos with a release are compared by tag, repos without one by last push time',
76
244
  empty: 'No plugins in this category yet',
77
245
  dataFrom: 'Data from dsh-plugin.org · curated daily',
78
246
  }
@@ -0,0 +1,123 @@
1
+ /* 自绘下拉(替换原生 <select>)。颜色跟随宿主主题:经 --hub-* 设计令牌解析,
2
+ * 与 Header 的控件(排序/分段)保持一致的无边框 + 圆角语言。 */
3
+ .dropdown {
4
+ position: relative;
5
+ display: inline-flex;
6
+ flex-shrink: 0;
7
+ }
8
+
9
+ .dropdownBtn {
10
+ display: inline-flex;
11
+ align-items: center;
12
+ gap: 6px;
13
+ height: 24px;
14
+ padding: 0 8px;
15
+ font-size: 12px;
16
+ line-height: 22px;
17
+ color: var(--hub-text-primary);
18
+ background: transparent;
19
+ border: none;
20
+ border-radius: 6px;
21
+ cursor: pointer;
22
+ user-select: none;
23
+ white-space: nowrap;
24
+ transition: background 120ms ease;
25
+ }
26
+
27
+ .dropdownBtn:hover {
28
+ background: var(--hub-bg-hover);
29
+ }
30
+
31
+ .dropdownLabel {
32
+ min-width: 0;
33
+ overflow: hidden;
34
+ text-overflow: ellipsis;
35
+ }
36
+
37
+ .dropdownArrow,
38
+ .dropdownArrowOpen {
39
+ display: inline-flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ color: var(--hub-text-tertiary);
43
+ transition: transform 120ms ease;
44
+ }
45
+
46
+ .dropdownArrowOpen {
47
+ transform: rotate(180deg);
48
+ }
49
+
50
+ .dropdownPanel {
51
+ position: absolute;
52
+ top: calc(100% + 4px);
53
+ left: 0;
54
+ z-index: 50;
55
+ min-width: 100%;
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: 2px;
59
+ padding: 4px;
60
+ background: var(--hub-bg-1);
61
+ border: 1px solid var(--hub-border-2);
62
+ border-radius: 8px;
63
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
64
+ }
65
+
66
+ .dropdownItem,
67
+ .dropdownItemActive {
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: space-between;
71
+ gap: 10px;
72
+ font-size: 12px;
73
+ line-height: 18px;
74
+ border: none;
75
+ border-radius: 6px;
76
+ padding: 4px 9px;
77
+ cursor: pointer;
78
+ user-select: none;
79
+ white-space: nowrap;
80
+ text-align: left;
81
+ transition: background 120ms ease, color 120ms ease;
82
+ }
83
+
84
+ .dropdownItem {
85
+ color: var(--hub-text-primary);
86
+ background: transparent;
87
+ }
88
+
89
+ /* 悬浮选项:明显加重底色,保证「看得见、点得准」。 */
90
+ .dropdownItem:hover {
91
+ background: var(--hub-bg-hover);
92
+ }
93
+
94
+ .dropdownItemActive {
95
+ color: var(--hub-text-on-fill);
96
+ background: var(--hub-btn-fill);
97
+ }
98
+
99
+ .dropdownItemLabel {
100
+ min-width: 0;
101
+ overflow: hidden;
102
+ text-overflow: ellipsis;
103
+ }
104
+
105
+ .dropdownCount,
106
+ .dropdownCountActive {
107
+ font-size: 10px;
108
+ line-height: 14px;
109
+ border-radius: 999px;
110
+ padding: 0 6px;
111
+ min-width: 16px;
112
+ text-align: center;
113
+ }
114
+
115
+ .dropdownCount {
116
+ color: var(--hub-text-tertiary);
117
+ background: var(--hub-bg-btn);
118
+ }
119
+
120
+ .dropdownItemActive .dropdownCountActive {
121
+ color: var(--hub-text-on-fill);
122
+ background: var(--hub-bg-on-fill);
123
+ }