dsh-plugin 1.6.11 → 2.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.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/client/client.js +705 -803
- package/client/client.js.map +1 -1
- package/lib/http/catalog.js +2 -3
- package/lib/http/diagnostics.js +6 -7
- package/lib/http/install.js +47 -213
- package/lib/http/logs.js +1 -2
- package/lib/http/npm-release-age.js +42 -0
- package/lib/http/routes.js +8 -7
- package/lib/http/settings.js +2 -2
- package/lib/http/shared.js +2 -2
- package/lib/http/task.js +1 -2
- package/lib/index.js +5 -6
- package/lib/services/install/install-types.js +4 -4
- package/lib/services/install/install.js +1 -1
- package/lib/services/install/npm-check.js +3 -2
- package/lib/services/install/npm-resolve.js +9 -96
- package/lib/services/install/package-entry.js +6 -11
- package/lib/services/install/registry-check.js +5 -16
- package/lib/services/install/release-target.js +8 -9
- package/lib/services/install/task-queue.js +74 -57
- package/lib/services/npm-release-age.js +119 -0
- package/lib/services/probe.js +4 -4
- package/lib/services/profile/installed-versions.js +15 -21
- package/lib/services/profile/profile.js +16 -44
- package/lib/services/profile/progress.js +8 -11
- package/lib/services/settings.js +2 -5
- package/lib/types/http/catalog.d.ts +1 -1
- package/lib/types/http/install.d.ts +2 -2
- package/lib/types/http/npm-release-age.d.ts +2 -0
- package/lib/types/http/routes.d.ts +10 -12
- package/lib/types/http/shared.d.ts +5 -6
- package/lib/types/index.d.ts +7 -8
- package/lib/types/services/install/install-types.d.ts +12 -13
- package/lib/types/services/install/install.d.ts +1 -1
- package/lib/types/services/install/npm-resolve.d.ts +4 -16
- package/lib/types/services/install/package-entry.d.ts +6 -11
- package/lib/types/services/install/release-target.d.ts +7 -8
- package/lib/types/services/install/task-queue.d.ts +32 -31
- package/lib/types/services/npm-release-age.d.ts +15 -0
- package/lib/types/services/probe.d.ts +4 -4
- package/lib/types/services/profile/installed-versions.d.ts +5 -7
- package/lib/types/services/profile/profile.d.ts +14 -18
- package/lib/types/services/profile/progress.d.ts +7 -10
- package/lib/types/services/settings.d.ts +0 -4
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +23 -27
- package/src/client/components/catalog/CatalogList.tsx +2 -4
- package/src/client/components/catalog/PluginCard.tsx +2 -3
- package/src/client/components/layout/CatalogControls.tsx +5 -6
- package/src/client/components/layout/CatalogHeader.tsx +1 -2
- package/src/client/components/layout/CategoryTabs.tsx +2 -3
- package/src/client/components/layout/SectionTabs.tsx +3 -5
- package/src/client/components/modals/ConfirmDialog.tsx +4 -6
- package/src/client/components/modals/InstalledDetailModal.tsx +3 -6
- package/src/client/components/modals/LogsModal.tsx +7 -12
- package/src/client/components/modals/NotificationsModal.tsx +52 -24
- package/src/client/components/modals/ProgressView.tsx +3 -4
- package/src/client/components/modals/modals.tsx +58 -30
- package/src/client/components/ui/Dropdown.tsx +4 -6
- package/src/client/components/ui/Toggle.tsx +2 -3
- package/src/client/components/ui/icons.tsx +1 -2
- package/src/client/components/views/CustomInstallView.tsx +20 -178
- package/src/client/components/views/DiagnosticsView.tsx +3 -6
- package/src/client/components/views/InstalledView.tsx +6 -11
- package/src/client/components/views/MarketView.tsx +2 -4
- package/src/client/components/views/SettingsView.tsx +9 -32
- package/src/client/css-modules.d.ts +1 -1
- package/src/client/data/catalog.ts +2 -3
- package/src/client/data/host.ts +26 -3
- package/src/client/data/hub.ts +2 -3
- package/src/client/hooks/useCatalog.ts +79 -24
- package/src/client/hooks/useIncrementalList.ts +3 -5
- package/src/client/hooks/useLanguage.ts +4 -6
- package/src/client/hooks/useSettings.ts +4 -11
- package/src/client/hooks/useTaskQueue.ts +36 -42
- package/src/client/index.tsx +2 -3
- package/src/client/locales.ts +96 -123
- package/src/client/logic/constants.ts +1 -2
- package/src/client/logic/ensure-plugin-css.ts +8 -12
- package/src/client/logic/failures.ts +41 -28
- package/src/client/logic/format.ts +1 -1
- package/src/client/logic/install-command.ts +14 -35
- package/src/client/logic/installed.ts +15 -10
- package/src/client/logic/normalize.ts +3 -3
- package/src/client/logic/release-age.ts +19 -0
- package/src/client/logic/urls.ts +55 -50
- package/src/client/styles/CustomInstallView.module.css +3 -30
- package/src/client/styles/DiagnosticsView.module.css +3 -5
- package/src/client/styles/Dropdown.module.css +2 -4
- package/src/client/styles/Header.module.css +6 -7
- package/src/client/styles/InstalledView.module.css +4 -7
- package/src/client/styles/List.module.css +4 -5
- package/src/client/styles/LogsView.module.css +2 -3
- package/src/client/styles/Modal.module.css +4 -5
- package/src/client/styles/SectionTabs.module.css +2 -4
- package/src/client/styles/SettingsView.module.css +3 -5
- package/src/client/styles/Toggle.module.css +2 -4
- package/src/client/styles/tokens.module.css +14 -18
- package/src/client/types.ts +11 -11
- package/src/server/http/catalog.ts +2 -3
- package/src/server/http/diagnostics.ts +6 -7
- package/src/server/http/install.ts +47 -208
- package/src/server/http/logs.ts +1 -2
- package/src/server/http/npm-release-age.ts +42 -0
- package/src/server/http/routes.ts +12 -12
- package/src/server/http/settings.ts +2 -2
- package/src/server/http/shared.ts +5 -6
- package/src/server/http/task.ts +1 -2
- package/src/server/index.ts +7 -8
- package/src/server/services/install/install-types.ts +12 -13
- package/src/server/services/install/install.ts +0 -1
- package/src/server/services/install/npm-check.ts +3 -2
- package/src/server/services/install/npm-resolve.ts +9 -99
- package/src/server/services/install/package-entry.ts +6 -11
- package/src/server/services/install/registry-check.ts +5 -16
- package/src/server/services/install/release-target.ts +8 -9
- package/src/server/services/install/task-queue.ts +79 -63
- package/src/server/services/npm-release-age.ts +107 -0
- package/src/server/services/probe.ts +4 -4
- package/src/server/services/profile/installed-versions.ts +16 -22
- package/src/server/services/profile/profile.ts +16 -42
- package/src/server/services/profile/progress.ts +8 -11
- package/src/server/services/settings.ts +2 -9
- package/lib/services/install/preflight.js +0 -178
- package/lib/types/services/install/preflight.d.ts +0 -23
- package/src/server/services/install/preflight.ts +0 -184
|
@@ -12,7 +12,7 @@ import { basename, dirname, join } from 'node:path'
|
|
|
12
12
|
import { PACKAGE_RE, REPO_RE } from '../install/install-types.ts'
|
|
13
13
|
import { githubReleaseTarget } from '../install/release-target.ts'
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/** 从启动参数解析当前 profile,取不到落回 `web`。 */
|
|
16
16
|
export function readProfileArg(fallback = 'web'): string {
|
|
17
17
|
return profileFromArgv(process.argv, fallback)
|
|
18
18
|
}
|
|
@@ -44,43 +44,19 @@ export function profileFromArgv(argv: readonly string[], fallback = 'web'): stri
|
|
|
44
44
|
return fallback
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/**
|
|
47
|
+
/** 解析 profile 目录(`DSH_HOME` 或 `~/.dsh`)。 */
|
|
48
48
|
export function profileDirectory(profile: string): string {
|
|
49
49
|
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
* explicitly pinned revision reaches the installer instead of being dropped. */
|
|
52
|
+
/** 把仓库身份转成规范的 GitHub 目标(git+https),仓库不合法时返回 null。
|
|
53
|
+
* 带 ref 时按 Git ref(`#tag` / `#commit`)拼接;安装通道已不再使用 git 目标,此函数仅做身份规范化。 */
|
|
55
54
|
export function githubTarget(repo: string, ref?: string | null): string | null {
|
|
56
55
|
if (typeof repo !== 'string' || !REPO_RE.test(repo)) return null
|
|
57
56
|
const url = `git+https://github.com/${repo}.git`
|
|
58
57
|
return ref === undefined || ref === null || ref === '' ? url : `${url}#${ref}`
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
/**
|
|
62
|
-
* Extract a validated explicit Git ref (`#<ref>`) from a GitHub spec, or null.
|
|
63
|
-
* Only the fragment of a Git-style address qualifies — a pinned tag/commit is
|
|
64
|
-
* user intent and must survive target normalization, whereas `#…` on a plain
|
|
65
|
-
* web URL (`https://github.com/o/r#readme`) is a page anchor, not a ref.
|
|
66
|
-
* Git-style = `git+…` / `github:` / `git@…` / `….git` / bare `owner/repo`.
|
|
67
|
-
* Grammar mirrors a Git ref: no leading `.`/`-`, no `#`/whitespace.
|
|
68
|
-
*/
|
|
69
|
-
const GIT_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/
|
|
70
|
-
|
|
71
|
-
export function githubRefOf(value: string): string | null {
|
|
72
|
-
if (typeof value !== 'string') return null
|
|
73
|
-
const input = value.trim()
|
|
74
|
-
const hash = input.indexOf('#')
|
|
75
|
-
if (hash <= 0) return null
|
|
76
|
-
const base = input.slice(0, hash)
|
|
77
|
-
if (githubRepoOf(base) === null) return null
|
|
78
|
-
const gitStyle = /^(?:git\+|github:|git@)/i.test(base) || /\.git$/i.test(base) || !base.includes('://')
|
|
79
|
-
if (!gitStyle) return null
|
|
80
|
-
const ref = input.slice(hash + 1)
|
|
81
|
-
return GIT_REF_RE.test(ref) ? ref : null
|
|
82
|
-
}
|
|
83
|
-
|
|
84
60
|
/**
|
|
85
61
|
* DSH 命令行前缀(官方唯一形式 `dsh plugin --profile <name> <add|update> <target>`)—— 提取其中的安装目标;
|
|
86
62
|
* 非命令输入原样返回。兼容「直接粘贴完整命令」(防用户输错):--profile 必填且只有长选项
|
|
@@ -113,7 +89,7 @@ export function globalNpmPackagesOf(value: string): string[] | null {
|
|
|
113
89
|
return packages
|
|
114
90
|
}
|
|
115
91
|
|
|
116
|
-
/**
|
|
92
|
+
/** 从目录值或已装 Git spec 中提取 owner/repo 身份。 */
|
|
117
93
|
export function githubRepoOf(value: string): string | null {
|
|
118
94
|
if (typeof value !== 'string') return null
|
|
119
95
|
const input = value.trim()
|
|
@@ -137,25 +113,24 @@ export function githubRepoOf(value: string): string | null {
|
|
|
137
113
|
return null
|
|
138
114
|
}
|
|
139
115
|
|
|
140
|
-
/**
|
|
116
|
+
/** 校验 npm 包名(卸载目标的语法)。 */
|
|
141
117
|
export function validPackageName(name: string): boolean {
|
|
142
118
|
return typeof name === 'string' && PACKAGE_RE.test(name)
|
|
143
119
|
}
|
|
144
120
|
|
|
145
121
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* the only reliable key is the one pnpm prints verbatim. Null when absent.
|
|
122
|
+
* 取 pnpm 在 `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` 提示里原样打印的 `allowBuilds` key。
|
|
123
|
+
* Git 安装时 pnpm 按 `name@<实际 fetch URL>`(如
|
|
124
|
+
* `https://codeload.github.com/<owner>/<repo>/tar.gz/<commit>`)匹配,而不是 `git+ssh://`
|
|
125
|
+
* 或分支 spec —— 裸包名与 `git+` spec 都匹配不上,唯一可靠的 key 就是 pnpm 原样输出的那个。
|
|
126
|
+
* 无则返回 null。
|
|
152
127
|
*/
|
|
153
128
|
export function parseAllowBuildsKey(output: string): string | null {
|
|
154
129
|
const m = /^[ \t]*(\S+@\S+):[ \t]*true[ \t]*$/m.exec(output)
|
|
155
130
|
return m === null ? null : m[1].trim()
|
|
156
131
|
}
|
|
157
132
|
|
|
158
|
-
/**
|
|
133
|
+
/** 当裸标量非法时,为 YAML 块映射 key 加引号。 */
|
|
159
134
|
function quoteYamlKey(key: string): string {
|
|
160
135
|
if (/^[-?:,[\]{}#&*!|>'"%@`]/.test(key) || /:(\s|$)/.test(key)) {
|
|
161
136
|
return `'${key.replace(/'/g, "''")}'`
|
|
@@ -164,15 +139,14 @@ function quoteYamlKey(key: string): string {
|
|
|
164
139
|
}
|
|
165
140
|
|
|
166
141
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* `set this to true or false` value) are dropped on rewrite.
|
|
142
|
+
* 向 profile 的 pnpm-workspace.yaml 追加一条 `allowBuilds` 记录:与已有条目合并,
|
|
143
|
+
* 其余 yaml 保持不变;重写时丢弃损坏的占位条目(pnpm 安装失败的 bug 会写入
|
|
144
|
+
* 字面量 `set this to true or false`)。
|
|
171
145
|
*/
|
|
172
146
|
export function addAllowBuildsKey(profile: string, key: string): void {
|
|
173
147
|
const file = join(profileDirectory(profile), 'pnpm-workspace.yaml')
|
|
174
148
|
let yaml = ''
|
|
175
|
-
try { yaml = readFileSync(file, 'utf8') } catch { /*
|
|
149
|
+
try { yaml = readFileSync(file, 'utf8') } catch { /* 文件不存在,下方创建 */ }
|
|
176
150
|
const eol = /\r\n/.test(yaml) ? '\r\n' : '\n'
|
|
177
151
|
const blockRe = /allowBuilds:[ \t]*\r?\n((?:[ \t]+[^\r\n]*\r?\n?)*)/g
|
|
178
152
|
const map = new Map<string, string>()
|
|
@@ -3,24 +3,21 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 安装执行器共用的纯进度估算工具。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* line and map it onto a 0-100 estimate; keeping them free of I/O makes the
|
|
12
|
-
* estimation logic directly unit-testable (see tests/progress.test.ts).
|
|
8
|
+
* dsh CLI 包装了 pnpm,pnpm 用回车原地刷新 `Progress: …` 行,因此原始输出是一串碎片。
|
|
9
|
+
* 这些工具把单个碎片归一化成干净的终端行,并映射到 0-100 估算值;不碰 I/O
|
|
10
|
+
* 让估算逻辑可直接单测(见 tests/progress.test.ts)。
|
|
13
11
|
*/
|
|
14
12
|
|
|
15
|
-
/**
|
|
13
|
+
/** 剥掉 `[out]` / `[err]` 来源前缀与 ANSI 转义序列。 */
|
|
16
14
|
export function cleanLine(raw: string): string {
|
|
17
15
|
return raw.replace(/^\[(?:err|out)\]\s*/, '').replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').trim()
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
/**
|
|
21
|
-
*
|
|
22
|
-
* `Progress: resolved N, reused X, downloaded Y, added Z
|
|
23
|
-
* phase; resolution/install phase lines bump the estimate towards done.
|
|
19
|
+
* 从单行 CLI 输出估算 0-100 进度。pnpm 在 fetch 阶段输出
|
|
20
|
+
* `Progress: resolved N, reused X, downloaded Y, added Z`;解析/安装阶段的行把估算值推向完成。
|
|
24
21
|
*/
|
|
25
22
|
export function estimateProgress(line: string): number {
|
|
26
23
|
const progress = line.match(/Progress:\s*(.+)/)
|
|
@@ -28,7 +25,7 @@ export function estimateProgress(line: string): number {
|
|
|
28
25
|
let resolved = 0
|
|
29
26
|
let handled = 0
|
|
30
27
|
for (const part of progress[1].split(',')) {
|
|
31
|
-
// pnpm
|
|
28
|
+
// pnpm 输出 `label count` 形式的 token,如 `resolved 100, reused 40`
|
|
32
29
|
const [label, raw] = part.trim().split(/\s+/)
|
|
33
30
|
const value = Number(raw)
|
|
34
31
|
if (label === 'resolved') resolved = value
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* back to DEFAULT_SETTINGS so old files stay readable as new options land.
|
|
6
|
+
* 插件中心设置持久化:每个活跃 profile 一个 `hub-settings.json`(与 pnpm-workspace.yaml 同目录)。
|
|
7
|
+
* 所有字段可选;缺失的 key 回退到 DEFAULT_SETTINGS,新选项上线后旧文件仍可读。
|
|
9
8
|
*/
|
|
10
9
|
import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
|
|
11
10
|
import { dirname, join } from 'node:path'
|
|
@@ -20,10 +19,6 @@ export interface HubSettings {
|
|
|
20
19
|
npmRegistry: string
|
|
21
20
|
/** 启用命令行安装 NPM 包(默认开;关掉后输入 npm 包名安装被拦截) */
|
|
22
21
|
enableNpmInstall: boolean
|
|
23
|
-
/** 启用命令行安装 GitHub 源码(默认开;关掉后输入 GitHub 地址安装被拦截) */
|
|
24
|
-
enableGitInstall: boolean
|
|
25
|
-
/** 启用命令行粘贴 dsh plugin 命令安装(默认开;关掉后命令输入被禁用) */
|
|
26
|
-
enableDshInstall: boolean
|
|
27
22
|
/** 日志存放位置覆盖(空串 = 默认 ~/.dsh/profiles/<profile>/hub.log;可填目录或 .log 文件路径) */
|
|
28
23
|
logPath: string
|
|
29
24
|
}
|
|
@@ -33,8 +28,6 @@ export const DEFAULT_SETTINGS: HubSettings = {
|
|
|
33
28
|
proxy: '',
|
|
34
29
|
npmRegistry: '',
|
|
35
30
|
enableNpmInstall: true,
|
|
36
|
-
enableGitInstall: true,
|
|
37
|
-
enableDshInstall: true,
|
|
38
31
|
logPath: '',
|
|
39
32
|
}
|
|
40
33
|
|
|
@@ -1,178 +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
|
-
* 安装前预检:对 GitHub 源的插件,检查其分发(codeload tarball)里是否真的包含
|
|
7
|
-
* package.json 声明的文件(main / exports["."] / dsh.bundle.patch)。git 分发常不提交
|
|
8
|
-
* 构建产物(lib/ 等),这类残缺包装完会让宿主重启加载插件树时 ERR_MODULE_NOT_FOUND
|
|
9
|
-
* 直接崩溃 —— 预检在动手安装前就把它们拦下,避免白装一次。
|
|
10
|
-
* npm 包信任 registry 的完整性,直接放行;任何无法预检的情况也放行,交给装后
|
|
11
|
-
* 校验(verifyInstalledEntry)兜底,保证预检自身失败不会卡死安装流程。
|
|
12
|
-
*/
|
|
13
|
-
import { execFile } from 'node:child_process';
|
|
14
|
-
import { createWriteStream, mkdtempSync, rmSync } from 'node:fs';
|
|
15
|
-
import { get } from 'node:https';
|
|
16
|
-
import { tmpdir } from 'node:os';
|
|
17
|
-
import { join } from 'node:path';
|
|
18
|
-
import { pipeline } from 'node:stream/promises';
|
|
19
|
-
import { promisify } from 'node:util';
|
|
20
|
-
import { githubRefOf, githubRepoOf } from '../profile/profile.js';
|
|
21
|
-
import { githubReleaseTarget } from './release-target.js';
|
|
22
|
-
import { requiredDistributionFiles, resolveBundlePatchFiles } from './package-entry.js';
|
|
23
|
-
const execFileAsync = promisify(execFile);
|
|
24
|
-
/** 单次网络操作超时(ms):git ls-remote / codeload 下载。网络不可达时不能无限挂起,
|
|
25
|
-
* 否则 `/install` 请求永远不返回、任务不入队、前端进度卡 0%。超时后放行(交给装后校验)。 */
|
|
26
|
-
const PREFLIGHT_TIMEOUT_MS = 15_000;
|
|
27
|
-
export async function preflightTarget(target) {
|
|
28
|
-
// release 包自带包根:拿 monorepo HEAD 的源码分发去校验它没有意义,会把完整产物
|
|
29
|
-
// 误判成缺入口文件而拦下。该包的真实性交给安装本身与装后 verifyInstalledEntry 兜底。
|
|
30
|
-
if (githubReleaseTarget(target) !== null)
|
|
31
|
-
return { ok: true, missing: null, name: null };
|
|
32
|
-
const source = githubRepoOf(target);
|
|
33
|
-
if (source === null)
|
|
34
|
-
return { ok: true, missing: null, name: null };
|
|
35
|
-
const [owner, repo] = source.split('/');
|
|
36
|
-
// 显式 pin 的 Git ref 要在它自己的修订上预检:默认分支常是 monorepo 根,与 pin 的发布包
|
|
37
|
-
// 形态不同,拿 HEAD 校验会把完整产物误判成缺入口文件(或反之)。无 pin 时仍查 HEAD。
|
|
38
|
-
const commit = await remoteCommit(owner, repo, githubRefOf(target));
|
|
39
|
-
if (commit === null)
|
|
40
|
-
return { ok: true, missing: null, name: null };
|
|
41
|
-
const dir = mkdtempSync(join(tmpdir(), 'dsh-preflight-'));
|
|
42
|
-
try {
|
|
43
|
-
const tar = join(dir, 'pkg.tar.gz');
|
|
44
|
-
await download(`https://codeload.github.com/${owner}/${repo}/tar.gz/${commit}`, tar);
|
|
45
|
-
const rootPackage = await rootPackageEntry(tar);
|
|
46
|
-
if (rootPackage === null)
|
|
47
|
-
return { ok: true, missing: null, name: null };
|
|
48
|
-
const rootPrefix = rootPackage.slice(0, -'package.json'.length);
|
|
49
|
-
const meta = await readTarJson(tar, rootPackage);
|
|
50
|
-
if (meta === null)
|
|
51
|
-
return { ok: true, missing: null, name: null };
|
|
52
|
-
// 只校验作者声明过的文件:没声明入口的包(只挂技能的 bundle)没有可缺失的入口,
|
|
53
|
-
// 它的分发是否完整由 dsh.bundle.patch 是否在包里决定;补丁若按本包名 mount 自己,
|
|
54
|
-
// 入口又变成必须的(宿主会 import 本包)—— 所以判入口前先把补丁读出来。
|
|
55
|
-
const patchTexts = [];
|
|
56
|
-
for (const file of resolveBundlePatchFiles(meta)) {
|
|
57
|
-
const text = await readTarText(tar, `${rootPrefix}${file}`);
|
|
58
|
-
if (text !== null)
|
|
59
|
-
patchTexts.push(text);
|
|
60
|
-
}
|
|
61
|
-
const required = requiredDistributionFiles(meta, patchTexts).map((file) => `${rootPrefix}${file}`);
|
|
62
|
-
const missing = missingDistributionFileOfTarList(await tarListing(tar), required);
|
|
63
|
-
return { ok: missing === null, missing, name: typeof meta.name === 'string' ? meta.name : null };
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return { ok: true, missing: null, name: null };
|
|
67
|
-
}
|
|
68
|
-
finally {
|
|
69
|
-
rmSync(dir, { recursive: true, force: true });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Resolve the commit to inspect: the pinned `ref` when given, otherwise the
|
|
74
|
-
* repository default branch (`HEAD`). A full commit SHA passes through as-is;
|
|
75
|
-
* annotated tags are peeled to their commit. Any git/network failure returns
|
|
76
|
-
* null and the caller falls back to "no preflight".
|
|
77
|
-
*/
|
|
78
|
-
async function remoteCommit(owner, repo, ref) {
|
|
79
|
-
if (ref !== null && /^[0-9a-f]{40}$/i.test(ref))
|
|
80
|
-
return ref;
|
|
81
|
-
try {
|
|
82
|
-
const { stdout } = await execFileAsync('git', ['ls-remote', `https://github.com/${owner}/${repo}.git`, ref ?? 'HEAD'], {
|
|
83
|
-
timeout: PREFLIGHT_TIMEOUT_MS,
|
|
84
|
-
maxBuffer: 1024 * 1024,
|
|
85
|
-
});
|
|
86
|
-
const lines = stdout.split(/\r?\n/).filter((line) => line !== '');
|
|
87
|
-
// Annotated tags list the tag object and the peeled commit (`<ref>^{}`): prefer the peel.
|
|
88
|
-
const chosen = lines.find((line) => line.endsWith('^{}')) ?? lines[lines.length - 1];
|
|
89
|
-
return chosen?.split(/\s+/)[0] ?? null;
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
function download(url, dest) {
|
|
96
|
-
return new Promise((resolve, reject) => {
|
|
97
|
-
const req = get(url, { timeout: PREFLIGHT_TIMEOUT_MS }, (res) => {
|
|
98
|
-
if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 303) {
|
|
99
|
-
res.resume();
|
|
100
|
-
if (res.headers.location) {
|
|
101
|
-
void download(res.headers.location, dest).then(resolve, reject);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
reject(new Error('download failed: redirect without location'));
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (res.statusCode !== 200) {
|
|
108
|
-
res.resume();
|
|
109
|
-
reject(new Error(`download failed: HTTP ${res.statusCode ?? '?'}`));
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
void pipeline(res, createWriteStream(dest)).then(resolve, reject);
|
|
113
|
-
});
|
|
114
|
-
// 网络不可达时 connect/响应可能无限挂起:超时销毁,避免预检卡死(放行交给装后校验)
|
|
115
|
-
req.on('timeout', () => req.destroy(new Error(`download timeout after ${PREFLIGHT_TIMEOUT_MS}ms`)));
|
|
116
|
-
req.on('error', reject);
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
/** GitHub codeload tarballs use a dynamic top-level directory (`repo-<sha>/`),
|
|
120
|
-
* not npm's `package/` prefix. Pick only a package.json directly under that
|
|
121
|
-
* single archive root; nested monorepo package manifests are intentionally
|
|
122
|
-
* ignored. Exported for a small parser regression test. */
|
|
123
|
-
export function rootPackageEntryOfTarList(listing) {
|
|
124
|
-
const candidates = listing
|
|
125
|
-
.split(/\r?\n/)
|
|
126
|
-
.filter((line) => /^[^/]+\/package\.json$/u.test(line));
|
|
127
|
-
return candidates.length === 1 ? candidates[0] : null;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* 清单里第一个缺失的必需文件(成员名带包根前缀,由调用方拼好),全部都在则为 null。
|
|
131
|
-
* 清单不可读(tar 缺失/解压失败)时按第一个必需文件缺失处理:与逐文件查询时
|
|
132
|
-
* 「查不到就是缺」的判定一致,宁可拦下一次可疑安装,也不放一个空包过去。
|
|
133
|
-
* Exported for a small parser regression test.
|
|
134
|
-
*/
|
|
135
|
-
export function missingDistributionFileOfTarList(listing, required) {
|
|
136
|
-
if (required.length === 0)
|
|
137
|
-
return null;
|
|
138
|
-
if (listing === null)
|
|
139
|
-
return required[0];
|
|
140
|
-
const present = new Set(listing.split(/\r?\n/));
|
|
141
|
-
return required.find((file) => !present.has(file)) ?? null;
|
|
142
|
-
}
|
|
143
|
-
async function rootPackageEntry(tar) {
|
|
144
|
-
const listing = await tarListing(tar);
|
|
145
|
-
return listing === null ? null : rootPackageEntryOfTarList(listing);
|
|
146
|
-
}
|
|
147
|
-
/** tar 成员清单;`tar` 执行失败时返回 null(调用方按无法判定处理)。 */
|
|
148
|
-
async function tarListing(tar) {
|
|
149
|
-
try {
|
|
150
|
-
const { stdout } = await execFileAsync('tar', ['-tzf', tar], { maxBuffer: 16 * 1024 * 1024 });
|
|
151
|
-
return stdout;
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
/** 从 tarball 提取一个文件的内容;文件不存在或提取失败返回 null。 */
|
|
158
|
-
async function readTarText(tar, entry) {
|
|
159
|
-
try {
|
|
160
|
-
const { stdout } = await execFileAsync('tar', ['-xzOf', tar, entry], { maxBuffer: 4 * 1024 * 1024 });
|
|
161
|
-
return stdout;
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
/** 从 tarball 提取并解析一个 JSON 文件;文件不存在或解析失败返回 null。 */
|
|
168
|
-
async function readTarJson(tar, entry) {
|
|
169
|
-
const text = await readTarText(tar, entry);
|
|
170
|
-
if (text === null)
|
|
171
|
-
return null;
|
|
172
|
-
try {
|
|
173
|
-
return JSON.parse(text);
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/** 预检结果:ok=false 表示分发改 package.json 声明的文件(入口或 bundle 补丁),
|
|
2
|
-
* missing 为该文件在包内的相对路径;
|
|
3
|
-
* name 为 git 分发包 package.json 声明的包名(非 git 目标/无法确定时为 null),
|
|
4
|
-
* 供安装路由做「包名冲突」检测 —— 同名包名已被其他来源占用时,pnpm 装前必然撞车,
|
|
5
|
-
* 需要把晦涩的 CLI 报错转成明确的拦截。 */
|
|
6
|
-
export interface PreflightResult {
|
|
7
|
-
ok: boolean;
|
|
8
|
-
missing: string | null;
|
|
9
|
-
name: string | null;
|
|
10
|
-
}
|
|
11
|
-
export declare function preflightTarget(target: string): Promise<PreflightResult>;
|
|
12
|
-
/** GitHub codeload tarballs use a dynamic top-level directory (`repo-<sha>/`),
|
|
13
|
-
* not npm's `package/` prefix. Pick only a package.json directly under that
|
|
14
|
-
* single archive root; nested monorepo package manifests are intentionally
|
|
15
|
-
* ignored. Exported for a small parser regression test. */
|
|
16
|
-
export declare function rootPackageEntryOfTarList(listing: string): string | null;
|
|
17
|
-
/**
|
|
18
|
-
* 清单里第一个缺失的必需文件(成员名带包根前缀,由调用方拼好),全部都在则为 null。
|
|
19
|
-
* 清单不可读(tar 缺失/解压失败)时按第一个必需文件缺失处理:与逐文件查询时
|
|
20
|
-
* 「查不到就是缺」的判定一致,宁可拦下一次可疑安装,也不放一个空包过去。
|
|
21
|
-
* Exported for a small parser regression test.
|
|
22
|
-
*/
|
|
23
|
-
export declare function missingDistributionFileOfTarList(listing: string | null, required: string[]): string | null;
|
|
@@ -1,184 +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
|
-
* 安装前预检:对 GitHub 源的插件,检查其分发(codeload tarball)里是否真的包含
|
|
7
|
-
* package.json 声明的文件(main / exports["."] / dsh.bundle.patch)。git 分发常不提交
|
|
8
|
-
* 构建产物(lib/ 等),这类残缺包装完会让宿主重启加载插件树时 ERR_MODULE_NOT_FOUND
|
|
9
|
-
* 直接崩溃 —— 预检在动手安装前就把它们拦下,避免白装一次。
|
|
10
|
-
* npm 包信任 registry 的完整性,直接放行;任何无法预检的情况也放行,交给装后
|
|
11
|
-
* 校验(verifyInstalledEntry)兜底,保证预检自身失败不会卡死安装流程。
|
|
12
|
-
*/
|
|
13
|
-
import { execFile } from 'node:child_process'
|
|
14
|
-
import { createWriteStream, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
15
|
-
import { get } from 'node:https'
|
|
16
|
-
import { tmpdir } from 'node:os'
|
|
17
|
-
import { join } from 'node:path'
|
|
18
|
-
import { pipeline } from 'node:stream/promises'
|
|
19
|
-
import { promisify } from 'node:util'
|
|
20
|
-
import { githubRefOf, githubRepoOf } from '../profile/profile.ts'
|
|
21
|
-
import { githubReleaseTarget } from './release-target.ts'
|
|
22
|
-
import { requiredDistributionFiles, resolveBundlePatchFiles } from './package-entry.ts'
|
|
23
|
-
|
|
24
|
-
const execFileAsync = promisify(execFile)
|
|
25
|
-
|
|
26
|
-
/** 单次网络操作超时(ms):git ls-remote / codeload 下载。网络不可达时不能无限挂起,
|
|
27
|
-
* 否则 `/install` 请求永远不返回、任务不入队、前端进度卡 0%。超时后放行(交给装后校验)。 */
|
|
28
|
-
const PREFLIGHT_TIMEOUT_MS = 15_000
|
|
29
|
-
|
|
30
|
-
/** 预检结果:ok=false 表示分发改 package.json 声明的文件(入口或 bundle 补丁),
|
|
31
|
-
* missing 为该文件在包内的相对路径;
|
|
32
|
-
* name 为 git 分发包 package.json 声明的包名(非 git 目标/无法确定时为 null),
|
|
33
|
-
* 供安装路由做「包名冲突」检测 —— 同名包名已被其他来源占用时,pnpm 装前必然撞车,
|
|
34
|
-
* 需要把晦涩的 CLI 报错转成明确的拦截。 */
|
|
35
|
-
export interface PreflightResult {
|
|
36
|
-
ok: boolean
|
|
37
|
-
missing: string | null
|
|
38
|
-
name: string | null
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function preflightTarget(target: string): Promise<PreflightResult> {
|
|
42
|
-
// release 包自带包根:拿 monorepo HEAD 的源码分发去校验它没有意义,会把完整产物
|
|
43
|
-
// 误判成缺入口文件而拦下。该包的真实性交给安装本身与装后 verifyInstalledEntry 兜底。
|
|
44
|
-
if (githubReleaseTarget(target) !== null) return { ok: true, missing: null, name: null }
|
|
45
|
-
const source = githubRepoOf(target)
|
|
46
|
-
if (source === null) return { ok: true, missing: null, name: null }
|
|
47
|
-
const [owner, repo] = source.split('/')
|
|
48
|
-
// 显式 pin 的 Git ref 要在它自己的修订上预检:默认分支常是 monorepo 根,与 pin 的发布包
|
|
49
|
-
// 形态不同,拿 HEAD 校验会把完整产物误判成缺入口文件(或反之)。无 pin 时仍查 HEAD。
|
|
50
|
-
const commit = await remoteCommit(owner, repo, githubRefOf(target))
|
|
51
|
-
if (commit === null) return { ok: true, missing: null, name: null }
|
|
52
|
-
const dir = mkdtempSync(join(tmpdir(), 'dsh-preflight-'))
|
|
53
|
-
try {
|
|
54
|
-
const tar = join(dir, 'pkg.tar.gz')
|
|
55
|
-
await download(`https://codeload.github.com/${owner}/${repo}/tar.gz/${commit}`, tar)
|
|
56
|
-
const rootPackage = await rootPackageEntry(tar)
|
|
57
|
-
if (rootPackage === null) return { ok: true, missing: null, name: null }
|
|
58
|
-
const rootPrefix = rootPackage.slice(0, -'package.json'.length)
|
|
59
|
-
const meta = await readTarJson(tar, rootPackage)
|
|
60
|
-
if (meta === null) return { ok: true, missing: null, name: null }
|
|
61
|
-
// 只校验作者声明过的文件:没声明入口的包(只挂技能的 bundle)没有可缺失的入口,
|
|
62
|
-
// 它的分发是否完整由 dsh.bundle.patch 是否在包里决定;补丁若按本包名 mount 自己,
|
|
63
|
-
// 入口又变成必须的(宿主会 import 本包)—— 所以判入口前先把补丁读出来。
|
|
64
|
-
const patchTexts: string[] = []
|
|
65
|
-
for (const file of resolveBundlePatchFiles(meta)) {
|
|
66
|
-
const text = await readTarText(tar, `${rootPrefix}${file}`)
|
|
67
|
-
if (text !== null) patchTexts.push(text)
|
|
68
|
-
}
|
|
69
|
-
const required = requiredDistributionFiles(meta, patchTexts).map((file) => `${rootPrefix}${file}`)
|
|
70
|
-
const missing = missingDistributionFileOfTarList(await tarListing(tar), required)
|
|
71
|
-
return { ok: missing === null, missing, name: typeof meta.name === 'string' ? meta.name : null }
|
|
72
|
-
} catch {
|
|
73
|
-
return { ok: true, missing: null, name: null }
|
|
74
|
-
} finally {
|
|
75
|
-
rmSync(dir, { recursive: true, force: true })
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Resolve the commit to inspect: the pinned `ref` when given, otherwise the
|
|
81
|
-
* repository default branch (`HEAD`). A full commit SHA passes through as-is;
|
|
82
|
-
* annotated tags are peeled to their commit. Any git/network failure returns
|
|
83
|
-
* null and the caller falls back to "no preflight".
|
|
84
|
-
*/
|
|
85
|
-
async function remoteCommit(owner: string, repo: string, ref: string | null): Promise<string | null> {
|
|
86
|
-
if (ref !== null && /^[0-9a-f]{40}$/i.test(ref)) return ref
|
|
87
|
-
try {
|
|
88
|
-
const { stdout } = await execFileAsync('git', ['ls-remote', `https://github.com/${owner}/${repo}.git`, ref ?? 'HEAD'], {
|
|
89
|
-
timeout: PREFLIGHT_TIMEOUT_MS,
|
|
90
|
-
maxBuffer: 1024 * 1024,
|
|
91
|
-
})
|
|
92
|
-
const lines = stdout.split(/\r?\n/).filter((line) => line !== '')
|
|
93
|
-
// Annotated tags list the tag object and the peeled commit (`<ref>^{}`): prefer the peel.
|
|
94
|
-
const chosen = lines.find((line) => line.endsWith('^{}')) ?? lines[lines.length - 1]
|
|
95
|
-
return chosen?.split(/\s+/)[0] ?? null
|
|
96
|
-
} catch {
|
|
97
|
-
return null
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function download(url: string, dest: string): Promise<void> {
|
|
102
|
-
return new Promise((resolve, reject) => {
|
|
103
|
-
const req = get(url, { timeout: PREFLIGHT_TIMEOUT_MS }, (res) => {
|
|
104
|
-
if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 303) {
|
|
105
|
-
res.resume()
|
|
106
|
-
if (res.headers.location) {
|
|
107
|
-
void download(res.headers.location, dest).then(resolve, reject)
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
reject(new Error('download failed: redirect without location'))
|
|
111
|
-
return
|
|
112
|
-
}
|
|
113
|
-
if (res.statusCode !== 200) {
|
|
114
|
-
res.resume()
|
|
115
|
-
reject(new Error(`download failed: HTTP ${res.statusCode ?? '?'}`))
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
void pipeline(res, createWriteStream(dest)).then(resolve, reject)
|
|
119
|
-
})
|
|
120
|
-
// 网络不可达时 connect/响应可能无限挂起:超时销毁,避免预检卡死(放行交给装后校验)
|
|
121
|
-
req.on('timeout', () => req.destroy(new Error(`download timeout after ${PREFLIGHT_TIMEOUT_MS}ms`)))
|
|
122
|
-
req.on('error', reject)
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** GitHub codeload tarballs use a dynamic top-level directory (`repo-<sha>/`),
|
|
127
|
-
* not npm's `package/` prefix. Pick only a package.json directly under that
|
|
128
|
-
* single archive root; nested monorepo package manifests are intentionally
|
|
129
|
-
* ignored. Exported for a small parser regression test. */
|
|
130
|
-
export function rootPackageEntryOfTarList(listing: string): string | null {
|
|
131
|
-
const candidates = listing
|
|
132
|
-
.split(/\r?\n/)
|
|
133
|
-
.filter((line) => /^[^/]+\/package\.json$/u.test(line))
|
|
134
|
-
return candidates.length === 1 ? candidates[0] : null
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 清单里第一个缺失的必需文件(成员名带包根前缀,由调用方拼好),全部都在则为 null。
|
|
139
|
-
* 清单不可读(tar 缺失/解压失败)时按第一个必需文件缺失处理:与逐文件查询时
|
|
140
|
-
* 「查不到就是缺」的判定一致,宁可拦下一次可疑安装,也不放一个空包过去。
|
|
141
|
-
* Exported for a small parser regression test.
|
|
142
|
-
*/
|
|
143
|
-
export function missingDistributionFileOfTarList(listing: string | null, required: string[]): string | null {
|
|
144
|
-
if (required.length === 0) return null
|
|
145
|
-
if (listing === null) return required[0]
|
|
146
|
-
const present = new Set(listing.split(/\r?\n/))
|
|
147
|
-
return required.find((file) => !present.has(file)) ?? null
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function rootPackageEntry(tar: string): Promise<string | null> {
|
|
151
|
-
const listing = await tarListing(tar)
|
|
152
|
-
return listing === null ? null : rootPackageEntryOfTarList(listing)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** tar 成员清单;`tar` 执行失败时返回 null(调用方按无法判定处理)。 */
|
|
156
|
-
async function tarListing(tar: string): Promise<string | null> {
|
|
157
|
-
try {
|
|
158
|
-
const { stdout } = await execFileAsync('tar', ['-tzf', tar], { maxBuffer: 16 * 1024 * 1024 })
|
|
159
|
-
return stdout
|
|
160
|
-
} catch {
|
|
161
|
-
return null
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** 从 tarball 提取一个文件的内容;文件不存在或提取失败返回 null。 */
|
|
166
|
-
async function readTarText(tar: string, entry: string): Promise<string | null> {
|
|
167
|
-
try {
|
|
168
|
-
const { stdout } = await execFileAsync('tar', ['-xzOf', tar, entry], { maxBuffer: 4 * 1024 * 1024 })
|
|
169
|
-
return stdout
|
|
170
|
-
} catch {
|
|
171
|
-
return null
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** 从 tarball 提取并解析一个 JSON 文件;文件不存在或解析失败返回 null。 */
|
|
176
|
-
async function readTarJson(tar: string, entry: string): Promise<Record<string, unknown> | null> {
|
|
177
|
-
const text = await readTarText(tar, entry)
|
|
178
|
-
if (text === null) return null
|
|
179
|
-
try {
|
|
180
|
-
return JSON.parse(text) as Record<string, unknown>
|
|
181
|
-
} catch {
|
|
182
|
-
return null
|
|
183
|
-
}
|
|
184
|
-
}
|