@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.
@@ -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
+ }
@@ -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
- const results = JSON.parse(readFileSync(resultPath, 'utf8'))
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
- return { content: readTarEntry(gunzipSync(raw), match) }
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.3.3",
4
- "description": "TokensCowork/DSH \u63d2\u4ef6\u5408\u683c\u6027\u4f53\u68c0:CLI \u4e00\u6761\u547d\u4ee4\u5224\u5b9a\u63d2\u4ef6\u80fd\u5426\u5b89\u5168\u88c5\u5165\u5bbf\u4e3b;\u88c5\u5165 Cowork \u540e\u63d0\u4f9b\u4f1a\u8bdd\u5185 plugin_check \u5de5\u5177\u3002",
5
- "type": "module",
6
- "bin": {
7
- "dsh-plugin-check": "bin/dsh-plugin-check.mjs"
8
- },
9
- "files": [
10
- "bin",
11
- "lib",
12
- "cordis.patch.yml",
13
- "README.md"
14
- ],
15
- "scripts": {
16
- "test": "node --test \"tests/*.test.mjs\""
17
- },
18
- "engines": {
19
- "node": ">=22.0.0"
20
- },
21
- "dependencies": {
22
- "js-yaml": "^4.1.0",
23
- "semver": "^7.6.0"
24
- },
25
- "license": "MIT",
26
- "repository": {
27
- "type": "git",
28
- "url": "https://github.com/TokensAPI/tokens_DshPluginCheck_code.git"
29
- },
30
- "main": "lib/cowork-plugin.mjs",
31
- "exports": {
32
- ".": "./lib/cowork-plugin.mjs"
33
- },
34
- "peerDependencies": {
35
- "@deepseek-ai/cordis": "4.0.1 || 4.0.2",
36
- "@deepseek-ai/dsh-tools": "0.1.0-rc.8 || 0.1.3-alpha.1"
37
- },
38
- "dsh": {
39
- "engine": ">=0.1.0-rc.8 <0.2.0",
40
- "bundle": {
41
- "patch": "./cordis.patch.yml"
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
+ }