@tokensapi/dsh-plugin-check 0.3.3 → 0.4.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.md +116 -95
- package/bin/dsh-plugin-check.mjs +26 -12
- package/docs/CHECKS.md +335 -0
- package/lib/client-check.mjs +98 -0
- package/lib/contract.mjs +210 -0
- package/lib/cowork-plugin.mjs +118 -113
- package/lib/manifest-check.mjs +105 -69
- package/lib/patch-rows.mjs +126 -31
- package/lib/registry-check.mjs +19 -146
- package/lib/semver-rules.mjs +129 -0
- package/lib/smoke-check.mjs +13 -1
- package/lib/tarball.mjs +5 -1
- package/package.json +45 -44
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* 版本相关规则(M1 / M6 / M7)的唯一实现
|
|
3
|
+
* ============================================================
|
|
4
|
+
* 这三条规则以前在两处各写一遍:CLI 走 manifest-check.mjs(用真
|
|
5
|
+
* semver),会话内 package= 走 registry-check.mjs(用一套手写的迷你
|
|
6
|
+
* SemVer)。迷你版只认完整三段版本号,把 ">=0.1 <0.2"、"1.x"、
|
|
7
|
+
* "1.2.0 - 2.0.0" 这些完全合法的 npm 范围判成"不可识别"→ error,
|
|
8
|
+
* 于是同一个包在两条路径上会拿到相反的结论,而且合格插件被判不合格。
|
|
9
|
+
*
|
|
10
|
+
* 现在统一到真 semver,并且只此一份:两条路径调同一个函数,再想漂
|
|
11
|
+
* 也漂不了(tests/cross-path.test.mjs 是这条约束的闸门)。
|
|
12
|
+
*
|
|
13
|
+
* 判定口径:一律带 includePrerelease —— 目标运行时本身就长期是
|
|
14
|
+
* 0.1.3-alpha.1 这种预发布版,不带这个选项 semver 会把它排除在
|
|
15
|
+
* 任何范围之外,得出"谁都不兼容"的荒谬结论。
|
|
16
|
+
* ============================================================ */
|
|
17
|
+
import semver from 'semver'
|
|
18
|
+
import { isIdentityCore, isRuntimeVersioned } from './contract.mjs'
|
|
19
|
+
|
|
20
|
+
const OPTIONS = { includePrerelease: true }
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 版本是否落在范围内。
|
|
24
|
+
* @param {string} version - 具体版本。
|
|
25
|
+
* @param {string} range - SemVer 范围。
|
|
26
|
+
* @returns {boolean | null} 范围不合法时返回 null(保持既有对外契约)。
|
|
27
|
+
*/
|
|
28
|
+
export function versionSatisfies(version, range) {
|
|
29
|
+
if (semver.valid(version) === null) return null
|
|
30
|
+
if (semver.validRange(range, OPTIONS) === null) return null
|
|
31
|
+
return semver.satisfies(version, range, OPTIONS)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* M1:name 与 version 的基本合法性。
|
|
36
|
+
* @param {object} manifest - package.json / registry 版本文档。
|
|
37
|
+
* @returns {Array<{level: string, rule: string, message: string}>}
|
|
38
|
+
*/
|
|
39
|
+
export function checkIdentity(manifest) {
|
|
40
|
+
const findings = []
|
|
41
|
+
if (typeof manifest.name !== 'string' || manifest.name === '') {
|
|
42
|
+
findings.push({ level: 'error', rule: 'M1-manifest', message: 'name 缺失' })
|
|
43
|
+
}
|
|
44
|
+
if (typeof manifest.version !== 'string' || semver.valid(manifest.version) === null) {
|
|
45
|
+
findings.push({ level: 'error', rule: 'M1-manifest', message: 'version 不是合法 SemVer' })
|
|
46
|
+
} else if (semver.prerelease(manifest.version) !== null) {
|
|
47
|
+
findings.push({
|
|
48
|
+
level: 'warning',
|
|
49
|
+
rule: 'M1-manifest',
|
|
50
|
+
message: `version ${manifest.version} 是预发布版;`
|
|
51
|
+
+ `市场受控安装只认稳定的精确版(上游 stableExactVersion),npm latest 若停在预发布版,`
|
|
52
|
+
+ `用户端拿不到一键安装按钮,只会看到手动命令。预发布请发在 next 之类的 dist-tag 上`,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
return findings
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* M6:dsh.engine 声明的目标运行时范围。
|
|
60
|
+
* @param {object} manifest
|
|
61
|
+
* @param {string | undefined} runtime - 目标 DSH 运行时;未给时只判范围合法性。
|
|
62
|
+
* @returns {Array<{level: string, rule: string, message: string}>}
|
|
63
|
+
*/
|
|
64
|
+
export function checkEngine(manifest, runtime) {
|
|
65
|
+
const engine = typeof manifest.dsh?.engine === 'string' ? manifest.dsh.engine : undefined
|
|
66
|
+
if (engine === undefined) {
|
|
67
|
+
return [{
|
|
68
|
+
level: 'warning',
|
|
69
|
+
rule: 'M6-engine',
|
|
70
|
+
message: 'dsh.engine 未声明(目标 DSH 运行时 SemVer 范围,如 ">=0.1.3-alpha.1 <0.2");'
|
|
71
|
+
+ '上游目前不强制读取该字段,但声明之后体检才能判断你与目标运行时是否相容',
|
|
72
|
+
}]
|
|
73
|
+
}
|
|
74
|
+
if (semver.validRange(engine, OPTIONS) === null) {
|
|
75
|
+
return [{ level: 'error', rule: 'M6-engine', message: `dsh.engine 不是合法 SemVer 范围: ${engine}` }]
|
|
76
|
+
}
|
|
77
|
+
if (runtime !== undefined && runtime !== '' && !semver.satisfies(runtime, engine, OPTIONS)) {
|
|
78
|
+
return [{ level: 'error', rule: 'M6-engine', message: `dsh.engine "${engine}" 不覆盖目标运行时 ${runtime}` }]
|
|
79
|
+
}
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* M7:核心 peer 的范围。
|
|
85
|
+
*
|
|
86
|
+
* 两层判定的口径**故意不同**:
|
|
87
|
+
* - 范围合法性:全部 isIdentityCore 的包。以前只看 @deepseek-ai/dsh
|
|
88
|
+
* 前缀,@deepseek-ai/cordis 的范围写坏了完全不报,偏偏它正是 S2 冒烟
|
|
89
|
+
* 真正要装的那个包,范围写坏下一步必然装不出来。
|
|
90
|
+
* - 是否含目标运行时:只对 isRuntimeVersioned 的包。cordis 自成 4.x
|
|
91
|
+
* 版本线,拿它跟运行时 0.1.3-alpha.1 比是范畴错误(详见 contract.mjs)。
|
|
92
|
+
* @param {object} manifest
|
|
93
|
+
* @param {string | undefined} runtime
|
|
94
|
+
* @returns {Array<{level: string, rule: string, message: string}>}
|
|
95
|
+
*/
|
|
96
|
+
export function checkPeerRanges(manifest, runtime) {
|
|
97
|
+
const findings = []
|
|
98
|
+
for (const [name, range] of Object.entries(manifest.peerDependencies ?? {})) {
|
|
99
|
+
if (!isIdentityCore(name)) continue
|
|
100
|
+
if (semver.validRange(String(range), OPTIONS) === null) {
|
|
101
|
+
findings.push({ level: 'error', rule: 'M7-peer-range', message: `peer ${name} 的范围不合法: ${range}` })
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
if (!isRuntimeVersioned(name)) continue
|
|
105
|
+
if (runtime === undefined || runtime === '') continue
|
|
106
|
+
if (!semver.satisfies(runtime, String(range), OPTIONS)) {
|
|
107
|
+
findings.push({
|
|
108
|
+
level: 'warning',
|
|
109
|
+
rule: 'M7-peer-range',
|
|
110
|
+
message: `peer ${name}@"${range}" 不含目标运行时 ${runtime};宿主升级后此插件可能在运行时断裂(接口错配类故障)`,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return findings
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 三条一起跑,供两条执行路径直接复用。
|
|
119
|
+
* @param {object} manifest
|
|
120
|
+
* @param {string | undefined} runtime
|
|
121
|
+
* @returns {Array<{level: string, rule: string, message: string}>}
|
|
122
|
+
*/
|
|
123
|
+
export function checkVersionRules(manifest, runtime) {
|
|
124
|
+
return [
|
|
125
|
+
...checkIdentity(manifest),
|
|
126
|
+
...checkEngine(manifest, runtime),
|
|
127
|
+
...checkPeerRanges(manifest, runtime),
|
|
128
|
+
]
|
|
129
|
+
}
|
package/lib/smoke-check.mjs
CHANGED
|
@@ -193,7 +193,19 @@ export function checkSmoke(pluginDir, manifest, rows, options = {}) {
|
|
|
193
193
|
error('S4-apply', `冒烟子进程异常: ${reason}`)
|
|
194
194
|
return { findings, workspace: options.keepWorkspace ? workspace : undefined }
|
|
195
195
|
}
|
|
196
|
-
|
|
196
|
+
// 子进程写出的结果文件理论上总是合法 JSON,但它可能被入口代码
|
|
197
|
+
// 的 stdout 污染、被磁盘写满截断、或者被入口自己覆盖掉。那种时候
|
|
198
|
+
// 崩掉整个 CLI 等于把"这个插件冒烟失败"变成"体检工具坏了",
|
|
199
|
+
// 作者拿不到任何可用结论 —— 按 S4-apply error 如实报出来。
|
|
200
|
+
let results
|
|
201
|
+
try {
|
|
202
|
+
results = JSON.parse(readFileSync(resultPath, 'utf8'))
|
|
203
|
+
if (!Array.isArray(results)) throw new TypeError('结果不是数组')
|
|
204
|
+
} catch (cause) {
|
|
205
|
+
const reason = cause instanceof Error ? cause.message : String(cause)
|
|
206
|
+
error('S4-apply', `冒烟结果文件不可解析: ${reason}(多半是插件入口往 stdout/结果文件里写了东西)`)
|
|
207
|
+
return { findings, workspace: options.keepWorkspace ? workspace : undefined }
|
|
208
|
+
}
|
|
197
209
|
const passed = results.filter(result => result.status === 'ok').length
|
|
198
210
|
findings.push({ level: 'info', rule: 'S4-apply', message: `隔离冒烟: ${passed}/${results.length} 行通过(解析→import→应用)` })
|
|
199
211
|
for (const result of results) {
|
package/lib/tarball.mjs
CHANGED
|
@@ -88,7 +88,11 @@ export async function fetchTarEntry(url, match, options = {}) {
|
|
|
88
88
|
if (declared > maxBytes) return { error: `tarball 超过 ${Math.round(maxBytes / 1024 / 1024)}MB,跳过包内检查` }
|
|
89
89
|
const raw = Buffer.from(await response.arrayBuffer())
|
|
90
90
|
if (raw.length > maxBytes) return { error: `tarball 超过 ${Math.round(maxBytes / 1024 / 1024)}MB,跳过包内检查` }
|
|
91
|
-
|
|
91
|
+
// 上面两道闸只管住了**压缩后**的字节数,解压后的体积不受它约束
|
|
92
|
+
// —— 一个几百 KB 的 gzip 可以解成几 GB。maxOutputLength 让 zlib
|
|
93
|
+
// 在超限时抛错(ERR_BUFFER_TOO_LARGE)而不是把内存吃干。
|
|
94
|
+
const tar = gunzipSync(raw, { maxOutputLength: maxBytes })
|
|
95
|
+
return { content: readTarEntry(tar, match) }
|
|
92
96
|
} catch (cause) {
|
|
93
97
|
const reason = cause instanceof Error ? cause.message : String(cause)
|
|
94
98
|
return { error: cause?.name === 'AbortError' ? '下载 tarball 超时' : `读取 tarball 失败: ${reason}` }
|
package/package.json
CHANGED
|
@@ -1,44 +1,45 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@tokensapi/dsh-plugin-check",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "TokensCowork/DSH
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"dsh-plugin-check": "bin/dsh-plugin-check.mjs"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"bin",
|
|
11
|
-
"lib",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"@deepseek-ai/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@tokensapi/dsh-plugin-check",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "TokensCowork/DSH 插件合格性体检:CLI 一条命令判定插件能否安全装入宿主;装入 Cowork 后提供会话内 plugin_check 工具。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dsh-plugin-check": "bin/dsh-plugin-check.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib",
|
|
12
|
+
"docs",
|
|
13
|
+
"cordis.patch.yml",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test \"tests/*.test.mjs\""
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=22.0.0"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"js-yaml": "^4.1.0",
|
|
24
|
+
"semver": "^7.6.0"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/TokensAPI/tokens_DshPluginCheck_code.git"
|
|
30
|
+
},
|
|
31
|
+
"main": "lib/cowork-plugin.mjs",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": "./lib/cowork-plugin.mjs"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@deepseek-ai/cordis": "4.0.1 || 4.0.2",
|
|
37
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.8 || 0.1.3-alpha.1"
|
|
38
|
+
},
|
|
39
|
+
"dsh": {
|
|
40
|
+
"engine": ">=0.1.0-rc.8 <0.2.0",
|
|
41
|
+
"bundle": {
|
|
42
|
+
"patch": "./cordis.patch.yml"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|