@tokensapi/dsh-plugin-check 0.2.0 → 0.3.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/cordis.patch.yml +3 -0
- package/lib/cowork-plugin.mjs +78 -0
- package/lib/registry-check.mjs +158 -0
- package/package.json +17 -2
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* Cowork 插件入口:会话内插件体检工具
|
|
3
|
+
* ============================================================
|
|
4
|
+
* 装进 TokensCowork 后注册 plugin_check 工具:对任意 npm 插件包
|
|
5
|
+
* 跑清单规则(与 CLI 的清单阶段同源),模型或用户在会话里即可
|
|
6
|
+
* 判断"这个插件装进来会不会有问题"。含隔离启动冒烟的完整体检
|
|
7
|
+
* 仍走 CLI:npx @tokensapi/dsh-plugin-check --package <name@ver>。
|
|
8
|
+
* ============================================================ */
|
|
9
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
10
|
+
import { checkPublishedManifest } from './registry-check.mjs'
|
|
11
|
+
|
|
12
|
+
/** 缺省比对的运行时版本;宿主升级后可通过插件配置覆盖。 */
|
|
13
|
+
const DEFAULT_RUNTIME = '0.1.3-alpha.1'
|
|
14
|
+
const REGISTRY = 'https://registry.npmjs.org'
|
|
15
|
+
const FETCH_TIMEOUT_MS = 15_000
|
|
16
|
+
|
|
17
|
+
export const name = 'tokens-plugin-check'
|
|
18
|
+
export const inject = ['tools']
|
|
19
|
+
|
|
20
|
+
async function fetchManifest(packageName, version) {
|
|
21
|
+
const controller = new AbortController()
|
|
22
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(
|
|
25
|
+
`${REGISTRY}/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}`,
|
|
26
|
+
{ signal: controller.signal, headers: { accept: 'application/json' } },
|
|
27
|
+
)
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(response.status === 404 ? `npm 上未找到 ${packageName}@${version}` : `npm 查询失败(HTTP ${response.status})`)
|
|
30
|
+
}
|
|
31
|
+
return await response.json()
|
|
32
|
+
} finally {
|
|
33
|
+
clearTimeout(timer)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
39
|
+
* @param {{ runtime?: string }} [config]
|
|
40
|
+
*/
|
|
41
|
+
export function apply(ctx, config = {}) {
|
|
42
|
+
const runtime = typeof config.runtime === 'string' && config.runtime !== '' ? config.runtime : DEFAULT_RUNTIME
|
|
43
|
+
ctx.tools.register(defineTool({
|
|
44
|
+
name: 'plugin_check',
|
|
45
|
+
description: 'Check whether a published DSH/TokensCowork plugin npm package is safe to install: '
|
|
46
|
+
+ 'forbidden lifecycle scripts, dual-kernel core dependencies, missing dsh bundle patch, '
|
|
47
|
+
+ 'and runtime/peer compatibility against the current DSH runtime. '
|
|
48
|
+
+ 'Returns ok=false with per-rule findings when the plugin would break the host.',
|
|
49
|
+
parameters: {
|
|
50
|
+
package: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
required: true,
|
|
53
|
+
description: 'npm package name, e.g. "@tokensapi/dsh-progressive-tools".',
|
|
54
|
+
},
|
|
55
|
+
version: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
description: 'Exact version to check. Defaults to the latest dist-tag.',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
output: {
|
|
61
|
+
schema: { type: 'json' },
|
|
62
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
|
|
63
|
+
},
|
|
64
|
+
isConcurrencySafe: () => true,
|
|
65
|
+
async execute(args) {
|
|
66
|
+
const manifest = await fetchManifest(args.package, args.version ?? 'latest')
|
|
67
|
+
const findings = checkPublishedManifest(manifest, runtime)
|
|
68
|
+
return {
|
|
69
|
+
package: manifest.name,
|
|
70
|
+
version: manifest.version,
|
|
71
|
+
runtime,
|
|
72
|
+
ok: !findings.some(finding => finding.level === 'error'),
|
|
73
|
+
findings,
|
|
74
|
+
note: '以上为清单规则;含隔离启动冒烟的完整体检请运行 npx @tokensapi/dsh-plugin-check --package <name@version>',
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
}))
|
|
78
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokensapi/dsh-plugin-check",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "TokensCowork/DSH \u63d2\u4ef6\u5408\u683c\u6027\u4f53\u68c0
|
|
3
|
+
"version": "0.3.0",
|
|
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
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dsh-plugin-check": "bin/dsh-plugin-check.mjs"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
11
|
"lib",
|
|
12
|
+
"cordis.patch.yml",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
@@ -26,5 +27,19 @@
|
|
|
26
27
|
"type": "git",
|
|
27
28
|
"url": "https://github.com/TokensAPI/TokensCowork.git",
|
|
28
29
|
"directory": "plugin-check"
|
|
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
|
+
}
|
|
29
44
|
}
|
|
30
45
|
}
|