@tokensapi/dsh-plugin-check 0.3.0 → 0.3.1
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 +79 -79
- package/bin/dsh-plugin-check.mjs +113 -113
- package/cordis.patch.yml +3 -3
- package/lib/cowork-plugin.mjs +78 -78
- package/lib/fetch-package.mjs +44 -44
- package/lib/manifest-check.mjs +144 -144
- package/lib/npm.mjs +13 -13
- package/lib/patch-rows.mjs +86 -86
- package/lib/registry-check.mjs +158 -158
- package/lib/smoke-check.mjs +174 -174
- package/package.json +44 -45
package/lib/registry-check.mjs
CHANGED
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
/* ============================================================
|
|
2
|
-
* 已发布包体检(注册表版,零依赖纯函数)
|
|
3
|
-
* ============================================================
|
|
4
|
-
* 对 npm registry 的版本文档跑清单规则,与目录体检(manifest-check)
|
|
5
|
-
* 同一套语义,但不需要文件系统 —— 供 Cowork 插件进程内使用。
|
|
6
|
-
* 冒烟阶段无法在宿主进程执行,完整体检走 CLI 或 Plugin Check
|
|
7
|
-
* workflow。
|
|
8
|
-
* ============================================================ */
|
|
9
|
-
|
|
10
|
-
const FORBIDDEN_LIFECYCLE = ['preinstall', 'install', 'postinstall', 'prepare']
|
|
11
|
-
const isIdentityCore = (name) =>
|
|
12
|
-
name === '@deepseek-ai/cordis' || name.startsWith('@deepseek-ai/cordis-') || name.startsWith('@deepseek-ai/dsh')
|
|
13
|
-
|
|
14
|
-
/* ------------------------- 迷你 SemVer ------------------------- */
|
|
15
|
-
// 只实现体检需要的子集:比较 + 范围判定(空格=且、||=或、^、~、
|
|
16
|
-
// >=、>、<=、<、=、裸版本),预发布参与比较;识别不了的范围返回
|
|
17
|
-
// null 由调用方按"范围不合法"处理。
|
|
18
|
-
|
|
19
|
-
function parseVersion(input) {
|
|
20
|
-
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(String(input).trim())
|
|
21
|
-
if (!match) return null
|
|
22
|
-
return {
|
|
23
|
-
parts: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
24
|
-
prerelease: match[4] === undefined ? [] : match[4].split('.'),
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function compareVersions(a, b) {
|
|
29
|
-
for (let index = 0; index < 3; index += 1) {
|
|
30
|
-
if (a.parts[index] !== b.parts[index]) return a.parts[index] < b.parts[index] ? -1 : 1
|
|
31
|
-
}
|
|
32
|
-
if (a.prerelease.length === 0 && b.prerelease.length === 0) return 0
|
|
33
|
-
if (a.prerelease.length === 0) return 1
|
|
34
|
-
if (b.prerelease.length === 0) return -1
|
|
35
|
-
const length = Math.max(a.prerelease.length, b.prerelease.length)
|
|
36
|
-
for (let index = 0; index < length; index += 1) {
|
|
37
|
-
const left = a.prerelease[index]
|
|
38
|
-
const right = b.prerelease[index]
|
|
39
|
-
if (left === undefined) return -1
|
|
40
|
-
if (right === undefined) return 1
|
|
41
|
-
const leftNumeric = /^\d+$/u.test(left)
|
|
42
|
-
const rightNumeric = /^\d+$/u.test(right)
|
|
43
|
-
if (leftNumeric && rightNumeric) {
|
|
44
|
-
if (Number(left) !== Number(right)) return Number(left) < Number(right) ? -1 : 1
|
|
45
|
-
} else if (leftNumeric !== rightNumeric) {
|
|
46
|
-
return leftNumeric ? -1 : 1
|
|
47
|
-
} else if (left !== right) {
|
|
48
|
-
return left < right ? -1 : 1
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return 0
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function comparatorHolds(version, operator, boundary) {
|
|
55
|
-
const cmp = compareVersions(version, boundary)
|
|
56
|
-
if (operator === '>') return cmp > 0
|
|
57
|
-
if (operator === '>=') return cmp >= 0
|
|
58
|
-
if (operator === '<') return cmp < 0
|
|
59
|
-
if (operator === '<=') return cmp <= 0
|
|
60
|
-
return cmp === 0
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function versionSatisfies(versionInput, rangeInput) {
|
|
64
|
-
const version = parseVersion(versionInput)
|
|
65
|
-
if (version === null) return null
|
|
66
|
-
const alternatives = String(rangeInput).split('||')
|
|
67
|
-
let recognized = false
|
|
68
|
-
for (const alternative of alternatives) {
|
|
69
|
-
const comparators = alternative.trim().split(/\s+/u).filter(Boolean)
|
|
70
|
-
if (comparators.length === 0) continue
|
|
71
|
-
let holds = true
|
|
72
|
-
let valid = true
|
|
73
|
-
for (const comparator of comparators) {
|
|
74
|
-
if (comparator === '*' || comparator === 'x') continue
|
|
75
|
-
const match = /^(>=|<=|>|<|=|\^|~)?(.+)$/u.exec(comparator)
|
|
76
|
-
const boundary = parseVersion(match[2])
|
|
77
|
-
if (boundary === null) { valid = false; break }
|
|
78
|
-
const operator = match[1] ?? '='
|
|
79
|
-
if (operator === '^') {
|
|
80
|
-
const upper = boundary.parts[0] > 0
|
|
81
|
-
? { parts: [boundary.parts[0] + 1, 0, 0], prerelease: ['0'] }
|
|
82
|
-
: boundary.parts[1] > 0
|
|
83
|
-
? { parts: [0, boundary.parts[1] + 1, 0], prerelease: ['0'] }
|
|
84
|
-
: { parts: [0, 0, boundary.parts[2] + 1], prerelease: ['0'] }
|
|
85
|
-
holds = holds && comparatorHolds(version, '>=', boundary) && comparatorHolds(version, '<', upper)
|
|
86
|
-
} else if (operator === '~') {
|
|
87
|
-
const upper = { parts: [boundary.parts[0], boundary.parts[1] + 1, 0], prerelease: ['0'] }
|
|
88
|
-
holds = holds && comparatorHolds(version, '>=', boundary) && comparatorHolds(version, '<', upper)
|
|
89
|
-
} else {
|
|
90
|
-
holds = holds && comparatorHolds(version, operator, boundary)
|
|
91
|
-
}
|
|
92
|
-
if (!holds) break
|
|
93
|
-
}
|
|
94
|
-
if (!valid) continue
|
|
95
|
-
recognized = true
|
|
96
|
-
if (holds) return true
|
|
97
|
-
}
|
|
98
|
-
return recognized ? false : null
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/* --------------------------- 规则 --------------------------- */
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 对一份 npm 版本清单跑体检规则。
|
|
105
|
-
* @param {object} manifest - registry 版本文档(scripts/dependencies/
|
|
106
|
-
* peerDependencies/dsh/engines/license/version)。
|
|
107
|
-
* @param {string} runtime - 目标 DSH 运行时版本。
|
|
108
|
-
* @returns {Array<{level: 'error'|'warning', rule: string, message: string}>}
|
|
109
|
-
*/
|
|
110
|
-
export function checkPublishedManifest(manifest, runtime) {
|
|
111
|
-
const findings = []
|
|
112
|
-
const error = (rule, message) => findings.push({ level: 'error', rule, message })
|
|
113
|
-
const warning = (rule, message) => findings.push({ level: 'warning', rule, message })
|
|
114
|
-
|
|
115
|
-
const scripts = manifest.scripts ?? {}
|
|
116
|
-
for (const script of FORBIDDEN_LIFECYCLE) {
|
|
117
|
-
if (typeof scripts[script] === 'string') {
|
|
118
|
-
error('M2-lifecycle', `禁止 ${script} 生命周期脚本(安装即执行任意代码);构建请改用 prepack`)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
for (const name of Object.keys(manifest.dependencies ?? {})) {
|
|
123
|
-
if (isIdentityCore(name)) {
|
|
124
|
-
error('M3-core-peer', `${name} 出现在 dependencies;核心运行时必须是 peerDependencies,否则双内核实例会让 Cowork 启动失败`)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const bundle = manifest.dsh?.bundle ?? {}
|
|
129
|
-
if (typeof bundle.patch !== 'string' || bundle.patch === '') {
|
|
130
|
-
error('M4-bundle', 'dsh.bundle.patch 缺失;宿主靠它把插件挂进加载树')
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const engine = typeof manifest.dsh?.engine === 'string' ? manifest.dsh.engine : undefined
|
|
134
|
-
if (engine === undefined) {
|
|
135
|
-
warning('M6-engine', 'dsh.engine 未声明(目标运行时 SemVer 范围);该字段将成为上架必填')
|
|
136
|
-
} else {
|
|
137
|
-
const satisfied = versionSatisfies(runtime, engine)
|
|
138
|
-
if (satisfied === null) error('M6-engine', `dsh.engine 不是可识别的 SemVer 范围: ${engine}`)
|
|
139
|
-
else if (!satisfied) error('M6-engine', `dsh.engine "${engine}" 不覆盖当前运行时 ${runtime}`)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
for (const [name, range] of Object.entries(manifest.peerDependencies ?? {})) {
|
|
143
|
-
if (!name.startsWith('@deepseek-ai/dsh')) continue
|
|
144
|
-
const satisfied = versionSatisfies(runtime, String(range))
|
|
145
|
-
if (satisfied === null) error('M7-peer-range', `peer ${name} 的范围不可识别: ${range}`)
|
|
146
|
-
else if (!satisfied) warning('M7-peer-range', `peer ${name}@"${range}" 不含当前运行时 ${runtime};宿主升级后可能运行时断裂`)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (manifest.engines?.node === undefined) warning('M8-node-engines', 'engines.node 未声明;宿主运行在 Node 22+')
|
|
150
|
-
if (manifest.license === undefined) warning('M9-license', 'license 未声明')
|
|
151
|
-
|
|
152
|
-
const parsed = parseVersion(typeof manifest.version === 'string' ? manifest.version : '')
|
|
153
|
-
if (parsed !== null && parsed.prerelease.length > 0) {
|
|
154
|
-
warning('M1-manifest', `version ${manifest.version} 是预发布版;市场上架要求精确稳定版`)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return findings
|
|
158
|
-
}
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* 已发布包体检(注册表版,零依赖纯函数)
|
|
3
|
+
* ============================================================
|
|
4
|
+
* 对 npm registry 的版本文档跑清单规则,与目录体检(manifest-check)
|
|
5
|
+
* 同一套语义,但不需要文件系统 —— 供 Cowork 插件进程内使用。
|
|
6
|
+
* 冒烟阶段无法在宿主进程执行,完整体检走 CLI 或 Plugin Check
|
|
7
|
+
* workflow。
|
|
8
|
+
* ============================================================ */
|
|
9
|
+
|
|
10
|
+
const FORBIDDEN_LIFECYCLE = ['preinstall', 'install', 'postinstall', 'prepare']
|
|
11
|
+
const isIdentityCore = (name) =>
|
|
12
|
+
name === '@deepseek-ai/cordis' || name.startsWith('@deepseek-ai/cordis-') || name.startsWith('@deepseek-ai/dsh')
|
|
13
|
+
|
|
14
|
+
/* ------------------------- 迷你 SemVer ------------------------- */
|
|
15
|
+
// 只实现体检需要的子集:比较 + 范围判定(空格=且、||=或、^、~、
|
|
16
|
+
// >=、>、<=、<、=、裸版本),预发布参与比较;识别不了的范围返回
|
|
17
|
+
// null 由调用方按"范围不合法"处理。
|
|
18
|
+
|
|
19
|
+
function parseVersion(input) {
|
|
20
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(String(input).trim())
|
|
21
|
+
if (!match) return null
|
|
22
|
+
return {
|
|
23
|
+
parts: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
24
|
+
prerelease: match[4] === undefined ? [] : match[4].split('.'),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compareVersions(a, b) {
|
|
29
|
+
for (let index = 0; index < 3; index += 1) {
|
|
30
|
+
if (a.parts[index] !== b.parts[index]) return a.parts[index] < b.parts[index] ? -1 : 1
|
|
31
|
+
}
|
|
32
|
+
if (a.prerelease.length === 0 && b.prerelease.length === 0) return 0
|
|
33
|
+
if (a.prerelease.length === 0) return 1
|
|
34
|
+
if (b.prerelease.length === 0) return -1
|
|
35
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length)
|
|
36
|
+
for (let index = 0; index < length; index += 1) {
|
|
37
|
+
const left = a.prerelease[index]
|
|
38
|
+
const right = b.prerelease[index]
|
|
39
|
+
if (left === undefined) return -1
|
|
40
|
+
if (right === undefined) return 1
|
|
41
|
+
const leftNumeric = /^\d+$/u.test(left)
|
|
42
|
+
const rightNumeric = /^\d+$/u.test(right)
|
|
43
|
+
if (leftNumeric && rightNumeric) {
|
|
44
|
+
if (Number(left) !== Number(right)) return Number(left) < Number(right) ? -1 : 1
|
|
45
|
+
} else if (leftNumeric !== rightNumeric) {
|
|
46
|
+
return leftNumeric ? -1 : 1
|
|
47
|
+
} else if (left !== right) {
|
|
48
|
+
return left < right ? -1 : 1
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function comparatorHolds(version, operator, boundary) {
|
|
55
|
+
const cmp = compareVersions(version, boundary)
|
|
56
|
+
if (operator === '>') return cmp > 0
|
|
57
|
+
if (operator === '>=') return cmp >= 0
|
|
58
|
+
if (operator === '<') return cmp < 0
|
|
59
|
+
if (operator === '<=') return cmp <= 0
|
|
60
|
+
return cmp === 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function versionSatisfies(versionInput, rangeInput) {
|
|
64
|
+
const version = parseVersion(versionInput)
|
|
65
|
+
if (version === null) return null
|
|
66
|
+
const alternatives = String(rangeInput).split('||')
|
|
67
|
+
let recognized = false
|
|
68
|
+
for (const alternative of alternatives) {
|
|
69
|
+
const comparators = alternative.trim().split(/\s+/u).filter(Boolean)
|
|
70
|
+
if (comparators.length === 0) continue
|
|
71
|
+
let holds = true
|
|
72
|
+
let valid = true
|
|
73
|
+
for (const comparator of comparators) {
|
|
74
|
+
if (comparator === '*' || comparator === 'x') continue
|
|
75
|
+
const match = /^(>=|<=|>|<|=|\^|~)?(.+)$/u.exec(comparator)
|
|
76
|
+
const boundary = parseVersion(match[2])
|
|
77
|
+
if (boundary === null) { valid = false; break }
|
|
78
|
+
const operator = match[1] ?? '='
|
|
79
|
+
if (operator === '^') {
|
|
80
|
+
const upper = boundary.parts[0] > 0
|
|
81
|
+
? { parts: [boundary.parts[0] + 1, 0, 0], prerelease: ['0'] }
|
|
82
|
+
: boundary.parts[1] > 0
|
|
83
|
+
? { parts: [0, boundary.parts[1] + 1, 0], prerelease: ['0'] }
|
|
84
|
+
: { parts: [0, 0, boundary.parts[2] + 1], prerelease: ['0'] }
|
|
85
|
+
holds = holds && comparatorHolds(version, '>=', boundary) && comparatorHolds(version, '<', upper)
|
|
86
|
+
} else if (operator === '~') {
|
|
87
|
+
const upper = { parts: [boundary.parts[0], boundary.parts[1] + 1, 0], prerelease: ['0'] }
|
|
88
|
+
holds = holds && comparatorHolds(version, '>=', boundary) && comparatorHolds(version, '<', upper)
|
|
89
|
+
} else {
|
|
90
|
+
holds = holds && comparatorHolds(version, operator, boundary)
|
|
91
|
+
}
|
|
92
|
+
if (!holds) break
|
|
93
|
+
}
|
|
94
|
+
if (!valid) continue
|
|
95
|
+
recognized = true
|
|
96
|
+
if (holds) return true
|
|
97
|
+
}
|
|
98
|
+
return recognized ? false : null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* --------------------------- 规则 --------------------------- */
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 对一份 npm 版本清单跑体检规则。
|
|
105
|
+
* @param {object} manifest - registry 版本文档(scripts/dependencies/
|
|
106
|
+
* peerDependencies/dsh/engines/license/version)。
|
|
107
|
+
* @param {string} runtime - 目标 DSH 运行时版本。
|
|
108
|
+
* @returns {Array<{level: 'error'|'warning', rule: string, message: string}>}
|
|
109
|
+
*/
|
|
110
|
+
export function checkPublishedManifest(manifest, runtime) {
|
|
111
|
+
const findings = []
|
|
112
|
+
const error = (rule, message) => findings.push({ level: 'error', rule, message })
|
|
113
|
+
const warning = (rule, message) => findings.push({ level: 'warning', rule, message })
|
|
114
|
+
|
|
115
|
+
const scripts = manifest.scripts ?? {}
|
|
116
|
+
for (const script of FORBIDDEN_LIFECYCLE) {
|
|
117
|
+
if (typeof scripts[script] === 'string') {
|
|
118
|
+
error('M2-lifecycle', `禁止 ${script} 生命周期脚本(安装即执行任意代码);构建请改用 prepack`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const name of Object.keys(manifest.dependencies ?? {})) {
|
|
123
|
+
if (isIdentityCore(name)) {
|
|
124
|
+
error('M3-core-peer', `${name} 出现在 dependencies;核心运行时必须是 peerDependencies,否则双内核实例会让 Cowork 启动失败`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const bundle = manifest.dsh?.bundle ?? {}
|
|
129
|
+
if (typeof bundle.patch !== 'string' || bundle.patch === '') {
|
|
130
|
+
error('M4-bundle', 'dsh.bundle.patch 缺失;宿主靠它把插件挂进加载树')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const engine = typeof manifest.dsh?.engine === 'string' ? manifest.dsh.engine : undefined
|
|
134
|
+
if (engine === undefined) {
|
|
135
|
+
warning('M6-engine', 'dsh.engine 未声明(目标运行时 SemVer 范围);该字段将成为上架必填')
|
|
136
|
+
} else {
|
|
137
|
+
const satisfied = versionSatisfies(runtime, engine)
|
|
138
|
+
if (satisfied === null) error('M6-engine', `dsh.engine 不是可识别的 SemVer 范围: ${engine}`)
|
|
139
|
+
else if (!satisfied) error('M6-engine', `dsh.engine "${engine}" 不覆盖当前运行时 ${runtime}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const [name, range] of Object.entries(manifest.peerDependencies ?? {})) {
|
|
143
|
+
if (!name.startsWith('@deepseek-ai/dsh')) continue
|
|
144
|
+
const satisfied = versionSatisfies(runtime, String(range))
|
|
145
|
+
if (satisfied === null) error('M7-peer-range', `peer ${name} 的范围不可识别: ${range}`)
|
|
146
|
+
else if (!satisfied) warning('M7-peer-range', `peer ${name}@"${range}" 不含当前运行时 ${runtime};宿主升级后可能运行时断裂`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (manifest.engines?.node === undefined) warning('M8-node-engines', 'engines.node 未声明;宿主运行在 Node 22+')
|
|
150
|
+
if (manifest.license === undefined) warning('M9-license', 'license 未声明')
|
|
151
|
+
|
|
152
|
+
const parsed = parseVersion(typeof manifest.version === 'string' ? manifest.version : '')
|
|
153
|
+
if (parsed !== null && parsed.prerelease.length > 0) {
|
|
154
|
+
warning('M1-manifest', `version ${manifest.version} 是预发布版;市场上架要求精确稳定版`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return findings
|
|
158
|
+
}
|