@tokensapi/dsh-plugin-check 0.3.2 → 0.3.4

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.
@@ -1,83 +1,118 @@
1
- /* ============================================================
2
- * Cowork 插件入口:会话内插件体检工具
3
- * ============================================================
4
- * 装进 TokensCowork 后注册 plugin_check 工具:对任意 npm 插件包
5
- * 跑清单规则 + 包内补丁行检查(与 CLI 的清单阶段同源),模型或
6
- * 用户在会话里即可判断"这个插件装进来会不会有问题"。补丁行会
7
- * 从 tarball 里真读出来 —— 只看 registry 清单会漏掉"补丁行 name
8
- * 写成插件名"这类装上即崩的缺陷。含隔离启动冒烟的完整体检仍走
9
- * CLI:npx @tokensapi/dsh-plugin-check --package <name@ver>。
10
- * ============================================================ */
11
- import { defineTool } from '@deepseek-ai/dsh-tools'
12
- import { checkPublishedPackage } from './registry-check.mjs'
13
-
14
- /** 缺省比对的运行时版本;宿主升级后可通过插件配置覆盖。 */
15
- const DEFAULT_RUNTIME = '0.1.3-alpha.1'
16
- const REGISTRY = 'https://registry.npmjs.org'
17
- const FETCH_TIMEOUT_MS = 15_000
18
-
19
- export const name = 'tokens-plugin-check'
20
- export const inject = ['tools']
21
-
22
- async function fetchManifest(packageName, version) {
23
- const controller = new AbortController()
24
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
25
- try {
26
- const response = await fetch(
27
- `${REGISTRY}/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}`,
28
- { signal: controller.signal, headers: { accept: 'application/json' } },
29
- )
30
- if (!response.ok) {
31
- throw new Error(response.status === 404 ? `npm 上未找到 ${packageName}@${version}` : `npm 查询失败(HTTP ${response.status})`)
32
- }
33
- return await response.json()
34
- } finally {
35
- clearTimeout(timer)
36
- }
37
- }
38
-
39
- /**
40
- * @param {import('@deepseek-ai/cordis').Context} ctx
41
- * @param {{ runtime?: string }} [config]
42
- */
43
- export function apply(ctx, config = {}) {
44
- const runtime = typeof config.runtime === 'string' && config.runtime !== '' ? config.runtime : DEFAULT_RUNTIME
45
- ctx.tools.register(defineTool({
46
- name: 'plugin_check',
47
- description: 'Check whether a published DSH/TokensCowork plugin npm package is safe to install: '
48
- + 'forbidden lifecycle scripts, dual-kernel core dependencies, missing dsh bundle patch, '
49
- + 'and runtime/peer compatibility against the current DSH runtime. '
50
- + 'Returns ok=false with per-rule findings when the plugin would break the host.',
51
- parameters: {
52
- package: {
53
- type: 'string',
54
- required: true,
55
- description: 'npm package name, e.g. "@tokensapi/dsh-progressive-tools".',
56
- },
57
- version: {
58
- type: 'string',
59
- description: 'Exact version to check. Defaults to the latest dist-tag.',
60
- },
61
- },
62
- output: {
63
- schema: { type: 'json' },
64
- render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
65
- },
66
- isConcurrencySafe: () => true,
67
- async execute(args) {
68
- const manifest = await fetchManifest(args.package, args.version ?? 'latest')
69
- const { findings, inspectedPatch } = await checkPublishedPackage(manifest, runtime)
70
- return {
71
- package: manifest.name,
72
- version: manifest.version,
73
- runtime,
74
- ok: !findings.some(finding => finding.level === 'error'),
75
- inspectedPatch,
76
- findings,
77
- note: inspectedPatch
78
- ? '以上为清单规则 + 包内补丁行检查;含隔离启动冒烟的完整体检请运行 npx @tokensapi/dsh-plugin-check --package <name@version>'
79
- : '未读到包内补丁文件,补丁行未检查;请运行 npx @tokensapi/dsh-plugin-check --package <name@version> 做完整体检',
80
- }
81
- },
82
- }))
83
- }
1
+ /* ============================================================
2
+ * Cowork 插件入口:会话内插件体检工具
3
+ * ============================================================
4
+ * 装进 TokensCowork 后注册 plugin_check 工具:对本地插件目录或
5
+ * 任意 npm 插件包
6
+ * 跑清单规则 + 包内补丁行检查(与 CLI 的清单阶段同源),模型或
7
+ * 用户在会话里即可判断"这个插件装进来会不会有问题"。补丁行会
8
+ * 从 tarball 里真读出来 —— 只看 registry 清单会漏掉"补丁行 name
9
+ * 写成插件名"这类装上即崩的缺陷。含隔离启动冒烟的完整体检仍走
10
+ * CLI:npx @tokensapi/dsh-plugin-check --package <name@ver>。
11
+ * ============================================================ */
12
+ import { resolve } from 'node:path'
13
+ import { defineTool } from '@deepseek-ai/dsh-tools'
14
+ import { checkManifest } from './manifest-check.mjs'
15
+ import { checkPublishedPackage } from './registry-check.mjs'
16
+ import { CONTRACT, checkContractBaseline } from './contract.mjs'
17
+
18
+ /** 缺省比对的运行时版本;宿主升级后可通过插件配置覆盖。 */
19
+ const DEFAULT_RUNTIME = '0.1.3-alpha.1'
20
+ const REGISTRY = 'https://registry.npmjs.org'
21
+ const FETCH_TIMEOUT_MS = 15_000
22
+
23
+ export const name = 'tokens-plugin-check'
24
+ export const inject = ['tools']
25
+
26
+ async function fetchManifest(packageName, version) {
27
+ const controller = new AbortController()
28
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
29
+ try {
30
+ const response = await fetch(
31
+ `${REGISTRY}/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}`,
32
+ { signal: controller.signal, headers: { accept: 'application/json' } },
33
+ )
34
+ if (!response.ok) {
35
+ throw new Error(response.status === 404 ? `npm 上未找到 ${packageName}@${version}` : `npm 查询失败(HTTP ${response.status})`)
36
+ }
37
+ return await response.json()
38
+ } finally {
39
+ clearTimeout(timer)
40
+ }
41
+ }
42
+
43
+ /**
44
+ * @param {import('@deepseek-ai/cordis').Context} ctx
45
+ * @param {{ runtime?: string }} [config]
46
+ */
47
+ export function apply(ctx, config = {}) {
48
+ const runtime = typeof config.runtime === 'string' && config.runtime !== '' ? config.runtime : DEFAULT_RUNTIME
49
+ ctx.tools.register(defineTool({
50
+ name: 'plugin_check',
51
+ description: 'Check whether a DSH/TokensCowork plugin is safe to install: forbidden lifecycle '
52
+ + 'scripts, dual-kernel core dependencies, missing or out-of-scope cordis patch rows, and '
53
+ + 'runtime/peer compatibility against the current DSH runtime. Returns ok=false with per-rule '
54
+ + 'findings when the plugin would break the host. '
55
+ + 'Pass "path" for a plugin the user is developing locally (checks the working tree, no publish '
56
+ + 'needed) — prefer this whenever the user asks about their own plugin before releasing it. '
57
+ + 'Pass "package" for an already published npm package. Exactly one of the two is required.',
58
+ parameters: {
59
+ path: {
60
+ type: 'string',
61
+ description: 'Local plugin directory containing package.json. Use for unpublished/in-development plugins.',
62
+ },
63
+ package: {
64
+ type: 'string',
65
+ description: 'Published npm package name, e.g. "@tokensapi/dsh-progressive-tools".',
66
+ },
67
+ version: {
68
+ type: 'string',
69
+ description: 'Exact version to check with "package". Defaults to the latest dist-tag.',
70
+ },
71
+ },
72
+ output: {
73
+ schema: { type: 'json' },
74
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
75
+ },
76
+ isConcurrencySafe: () => true,
77
+ async execute(args) {
78
+ const hasPath = typeof args.path === 'string' && args.path !== ''
79
+ const hasPackage = typeof args.package === 'string' && args.package !== ''
80
+ if (hasPath === hasPackage) {
81
+ throw new Error('plugin_check 需要且只需要一个目标:本地目录传 path,已发布包传 package')
82
+ }
83
+ if (hasPath) {
84
+ // 本地工作区体检:发版前就能判,不需要先 publish。隔离冒烟要装
85
+ // 依赖起子进程,宿主进程里不做,如实告知走 CLI。
86
+ const local = checkManifest(resolve(args.path), { runtime })
87
+ const localFindings = [...checkContractBaseline(runtime), ...local.findings]
88
+ return {
89
+ path: resolve(args.path),
90
+ package: local.manifest?.name,
91
+ version: local.manifest?.version,
92
+ runtime,
93
+ contract: CONTRACT,
94
+ ok: !localFindings.some(finding => finding.level === 'error'),
95
+ inspectedPatch: true,
96
+ findings: localFindings,
97
+ note: '以上为清单规则 + 补丁行检查(读的是本地工作区);隔离启动冒烟请在插件目录运行 '
98
+ + 'npx @tokensapi/dsh-plugin-check --runtime ' + runtime,
99
+ }
100
+ }
101
+ const manifest = await fetchManifest(args.package, args.version ?? 'latest')
102
+ const { findings, inspectedPatch } = await checkPublishedPackage(manifest, runtime)
103
+ const published = [...checkContractBaseline(runtime), ...findings]
104
+ return {
105
+ package: manifest.name,
106
+ version: manifest.version,
107
+ runtime,
108
+ contract: CONTRACT,
109
+ ok: !published.some(finding => finding.level === 'error'),
110
+ inspectedPatch,
111
+ findings: published,
112
+ note: inspectedPatch
113
+ ? '以上为清单规则 + 包内补丁行检查;含隔离启动冒烟的完整体检请运行 npx @tokensapi/dsh-plugin-check --package <name@version>'
114
+ : '未读到包内补丁文件,补丁行未检查;请运行 npx @tokensapi/dsh-plugin-check --package <name@version> 做完整体检',
115
+ }
116
+ },
117
+ }))
118
+ }
@@ -1,139 +1,138 @@
1
- /* ============================================================
2
- * 阶段①:清单体检(纯静态,秒级,离线)
3
- * ============================================================
4
- * 每一条规则都对应一种已经真实发生过的"把宿主搞崩/搞坏"路径,
5
- * 规则说明写在 README;error 挡上架,warning 提示即将收紧或
6
- * 潜在错配。判定为纯函数,便于单测与市场门禁复用。
7
- * ============================================================ */
8
- import { existsSync, readFileSync, statSync } from 'node:fs'
9
- import { join, resolve } from 'node:path'
10
- import semver from 'semver'
11
- import { checkPatchContent } from './patch-rows.mjs'
12
-
13
- /** 安装即执行任意代码的脚本;市场受控安装的既有红线。 */
14
- const FORBIDDEN_LIFECYCLE = ['preinstall', 'install', 'postinstall', 'prepare']
15
-
16
- /**
17
- * 携带身份语义的核心包:cordis 上下文与 dsh 服务在宿主/插件各持一份时
18
- * Symbol 不等,注册对不上。schemastery、cosmokit 等纯工具库不在此列,
19
- * 插件可以正常依赖。
20
- */
21
- const CORE_SCOPE = '@deepseek-ai/'
22
- const isIdentityCore = (name) => name === '@deepseek-ai/cordis'
23
- || name.startsWith('@deepseek-ai/cordis-')
24
- || name.startsWith('@deepseek-ai/dsh')
25
-
26
- const SUSPICIOUS_FILE = /(\.env(\.|$)|id_rsa|\.pem$|\.p12$|\.key$)/u
27
-
28
- /**
29
- * 对一个插件目录执行全部清单规则。
30
- * @param {string} pluginDir - 插件根目录(含 package.json)。
31
- * @param {{ runtime?: string }} [options] - runtime 为目标 DSH 运行时
32
- * 版本(如 0.1.3-alpha.1);给出时才执行 engine/peer 与其的交集判定。
33
- * @returns {{ findings: Array<{level: 'error'|'warning', rule: string, message: string}>, manifest?: object, rows?: object[] }}
34
- */
35
- export function checkManifest(pluginDir, options = {}) {
36
- const findings = []
37
- const error = (rule, message) => findings.push({ level: 'error', rule, message })
38
- const warning = (rule, message) => findings.push({ level: 'warning', rule, message })
39
-
40
- const manifestPath = resolve(pluginDir, 'package.json')
41
- if (!existsSync(manifestPath)) {
42
- error('M1-manifest', 'package.json 不存在')
43
- return { findings }
44
- }
45
- let manifest
46
- try {
47
- manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
48
- } catch (cause) {
49
- error('M1-manifest', `package.json 不是合法 JSON: ${cause instanceof Error ? cause.message : String(cause)}`)
50
- return { findings }
51
- }
52
-
53
- if (typeof manifest.name !== 'string' || manifest.name === '') {
54
- error('M1-manifest', 'name 缺失')
55
- }
56
- if (typeof manifest.version !== 'string' || semver.valid(manifest.version) === null) {
57
- error('M1-manifest', 'version 不是合法 SemVer')
58
- } else if (semver.prerelease(manifest.version) !== null) {
59
- warning('M1-manifest', `version ${manifest.version} 是预发布版;市场上架要求精确稳定版`)
60
- }
61
-
62
- const scripts = manifest.scripts ?? {}
63
- for (const script of FORBIDDEN_LIFECYCLE) {
64
- if (typeof scripts[script] === 'string') {
65
- error('M2-lifecycle', `禁止 ${script} 生命周期脚本(安装即执行任意代码,市场受控安装会拒绝);构建请改用 prepack`)
66
- }
67
- }
68
-
69
- const dependencies = manifest.dependencies ?? {}
70
- for (const name of Object.keys(dependencies)) {
71
- if (isIdentityCore(name)) {
72
- error('M3-core-peer', `${name} 出现在 dependencies;核心运行时必须声明为 peerDependencies,否则宿主与插件各持一份实例,服务注册无法对上,典型症状是整个 Cowork 启动失败`)
73
- }
74
- }
75
-
76
- const dsh = manifest.dsh ?? {}
77
- const bundle = dsh.bundle ?? {}
78
- let rows
79
- if (typeof bundle.patch !== 'string' || bundle.patch === '') {
80
- error('M4-bundle', 'dsh.bundle.patch 缺失;宿主靠它把插件挂进加载树')
81
- } else {
82
- const patchPath = resolve(pluginDir, bundle.patch)
83
- if (!existsSync(patchPath)) {
84
- error('M4-bundle', `dsh.bundle.patch 指向的文件不存在: ${bundle.patch}`)
85
- } else {
86
- // M4/M5 与会话内体检共用同一份实现,避免两条路径判定漂移。
87
- const checked = checkPatchContent(
88
- typeof manifest.name === 'string' ? manifest.name : '',
89
- readFileSync(patchPath, 'utf8'),
90
- )
91
- findings.push(...checked.findings)
92
- rows = checked.rows
93
- }
94
- }
95
-
96
- const engine = typeof dsh.engine === 'string' ? dsh.engine : undefined
97
- if (engine === undefined) {
98
- warning('M6-engine', 'dsh.engine 未声明(目标 DSH 运行时 SemVer 范围,如 ">=0.1.3-alpha.1 <0.2");该字段将成为上架必填')
99
- } else if (semver.validRange(engine, { includePrerelease: true }) === null) {
100
- error('M6-engine', `dsh.engine 不是合法 SemVer 范围: ${engine}`)
101
- } else if (options.runtime !== undefined
102
- && !semver.satisfies(options.runtime, engine, { includePrerelease: true })) {
103
- error('M6-engine', `dsh.engine "${engine}" 不覆盖目标运行时 ${options.runtime}`)
104
- }
105
-
106
- const peers = manifest.peerDependencies ?? {}
107
- if (options.runtime !== undefined) {
108
- for (const [name, range] of Object.entries(peers)) {
109
- if (!name.startsWith(`${CORE_SCOPE}dsh`)) continue
110
- if (semver.validRange(range, { includePrerelease: true }) === null) {
111
- error('M7-peer-range', `peer ${name} 的范围不合法: ${range}`)
112
- } else if (!semver.satisfies(options.runtime, range, { includePrerelease: true })) {
113
- warning('M7-peer-range', `peer ${name}@"${range}" 不含目标运行时 ${options.runtime};宿主升级后此插件可能在运行时断裂(接口错配类故障)`)
114
- }
115
- }
116
- }
117
-
118
- if (manifest.engines?.node === undefined) {
119
- warning('M8-node-engines', 'engines.node 未声明;宿主运行在 Node 22+')
120
- }
121
- if (manifest.license === undefined) {
122
- warning('M9-license', 'license 未声明')
123
- }
124
-
125
- const filesList = Array.isArray(manifest.files) ? manifest.files : []
126
- for (const entry of filesList) {
127
- if (typeof entry === 'string' && SUSPICIOUS_FILE.test(entry)) {
128
- error('M10-secrets', `files 白名单包含疑似凭据文件: ${entry}`)
129
- }
130
- }
131
- for (const candidate of ['.env', 'id_rsa']) {
132
- const candidatePath = join(pluginDir, candidate)
133
- if (existsSync(candidatePath) && statSync(candidatePath).isFile() && filesList.length === 0) {
134
- warning('M10-secrets', `目录含 ${candidate} 且未用 files 白名单限定发布内容,npm pack 可能把它带上`)
135
- }
136
- }
137
-
138
- return { findings, manifest, rows }
139
- }
1
+ /* ============================================================
2
+ * 阶段①:清单体检(纯静态,秒级,离线)
3
+ * ============================================================
4
+ * 每一条规则都对应一种已经真实发生过的"把宿主搞崩/搞坏"路径,
5
+ * 规则说明写在 README;error 挡上架,warning 提示即将收紧或
6
+ * 潜在错配。判定为纯函数,便于单测与市场门禁复用。
7
+ * ============================================================ */
8
+ import { existsSync, readFileSync, statSync } from 'node:fs'
9
+ import { join, resolve } from 'node:path'
10
+ import semver from 'semver'
11
+ import { checkPatchContent } from './patch-rows.mjs'
12
+ import { LIFECYCLE_SCRIPTS, lifecycleMessage } from './contract.mjs'
13
+
14
+ /**
15
+ * 携带身份语义的核心包:cordis 上下文与 dsh 服务在宿主/插件各持一份时
16
+ * Symbol 不等,注册对不上。schemastery、cosmokit 等纯工具库不在此列,
17
+ * 插件可以正常依赖。
18
+ */
19
+ const CORE_SCOPE = '@deepseek-ai/'
20
+ const isIdentityCore = (name) => name === '@deepseek-ai/cordis'
21
+ || name.startsWith('@deepseek-ai/cordis-')
22
+ || name.startsWith('@deepseek-ai/dsh')
23
+
24
+ const SUSPICIOUS_FILE = /(\.env(\.|$)|id_rsa|\.pem$|\.p12$|\.key$)/u
25
+
26
+ /**
27
+ * 对一个插件目录执行全部清单规则。
28
+ * @param {string} pluginDir - 插件根目录(含 package.json)。
29
+ * @param {{ runtime?: string }} [options] - runtime 为目标 DSH 运行时
30
+ * 版本(如 0.1.3-alpha.1);给出时才执行 engine/peer 与其的交集判定。
31
+ * @returns {{ findings: Array<{level: 'error'|'warning', rule: string, message: string}>, manifest?: object, rows?: object[] }}
32
+ */
33
+ export function checkManifest(pluginDir, options = {}) {
34
+ const findings = []
35
+ const error = (rule, message) => findings.push({ level: 'error', rule, message })
36
+ const warning = (rule, message) => findings.push({ level: 'warning', rule, message })
37
+
38
+ const manifestPath = resolve(pluginDir, 'package.json')
39
+ if (!existsSync(manifestPath)) {
40
+ error('M1-manifest', 'package.json 不存在')
41
+ return { findings }
42
+ }
43
+ let manifest
44
+ try {
45
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
46
+ } catch (cause) {
47
+ error('M1-manifest', `package.json 不是合法 JSON: ${cause instanceof Error ? cause.message : String(cause)}`)
48
+ return { findings }
49
+ }
50
+
51
+ if (typeof manifest.name !== 'string' || manifest.name === '') {
52
+ error('M1-manifest', 'name 缺失')
53
+ }
54
+ if (typeof manifest.version !== 'string' || semver.valid(manifest.version) === null) {
55
+ error('M1-manifest', 'version 不是合法 SemVer')
56
+ } else if (semver.prerelease(manifest.version) !== null) {
57
+ warning('M1-manifest', `version ${manifest.version} 是预发布版;`
58
+ + `市场受控安装只认稳定的精确版(上游 stableExactVersion),npm latest 若停在预发布版,`
59
+ + `用户端拿不到一键安装按钮,只会看到手动命令。预发布请发在 next 之类的 dist-tag 上`)
60
+ }
61
+
62
+ const scripts = manifest.scripts ?? {}
63
+ for (const script of LIFECYCLE_SCRIPTS) {
64
+ if (typeof scripts[script] === 'string') warning('M2-lifecycle', lifecycleMessage(script))
65
+ }
66
+
67
+ const dependencies = manifest.dependencies ?? {}
68
+ for (const name of Object.keys(dependencies)) {
69
+ if (isIdentityCore(name)) {
70
+ error('M3-core-peer', `${name} 出现在 dependencies;核心运行时必须声明为 peerDependencies,否则宿主与插件各持一份实例,服务注册无法对上,典型症状是整个 Cowork 启动失败`)
71
+ }
72
+ }
73
+
74
+ const dsh = manifest.dsh ?? {}
75
+ const bundle = dsh.bundle ?? {}
76
+ let rows
77
+ if (typeof bundle.patch !== 'string' || bundle.patch === '') {
78
+ error('M4-bundle', 'dsh.bundle.patch 缺失;宿主靠它把插件挂进加载树')
79
+ } else {
80
+ const patchPath = resolve(pluginDir, bundle.patch)
81
+ if (!existsSync(patchPath)) {
82
+ error('M4-bundle', `dsh.bundle.patch 指向的文件不存在: ${bundle.patch}`)
83
+ } else {
84
+ // M4/M5 与会话内体检共用同一份实现,避免两条路径判定漂移。
85
+ const checked = checkPatchContent(
86
+ typeof manifest.name === 'string' ? manifest.name : '',
87
+ readFileSync(patchPath, 'utf8'),
88
+ )
89
+ findings.push(...checked.findings)
90
+ rows = checked.rows
91
+ }
92
+ }
93
+
94
+ const engine = typeof dsh.engine === 'string' ? dsh.engine : undefined
95
+ if (engine === undefined) {
96
+ warning('M6-engine', 'dsh.engine 未声明(目标 DSH 运行时 SemVer 范围,如 ">=0.1.3-alpha.1 <0.2");'
97
+ + '上游目前不强制读取该字段,但声明之后体检才能判断你与目标运行时是否相容')
98
+ } else if (semver.validRange(engine, { includePrerelease: true }) === null) {
99
+ error('M6-engine', `dsh.engine 不是合法 SemVer 范围: ${engine}`)
100
+ } else if (options.runtime !== undefined
101
+ && !semver.satisfies(options.runtime, engine, { includePrerelease: true })) {
102
+ error('M6-engine', `dsh.engine "${engine}" 不覆盖目标运行时 ${options.runtime}`)
103
+ }
104
+
105
+ const peers = manifest.peerDependencies ?? {}
106
+ if (options.runtime !== undefined) {
107
+ for (const [name, range] of Object.entries(peers)) {
108
+ if (!name.startsWith(`${CORE_SCOPE}dsh`)) continue
109
+ if (semver.validRange(range, { includePrerelease: true }) === null) {
110
+ error('M7-peer-range', `peer ${name} 的范围不合法: ${range}`)
111
+ } else if (!semver.satisfies(options.runtime, range, { includePrerelease: true })) {
112
+ warning('M7-peer-range', `peer ${name}@"${range}" 不含目标运行时 ${options.runtime};宿主升级后此插件可能在运行时断裂(接口错配类故障)`)
113
+ }
114
+ }
115
+ }
116
+
117
+ if (manifest.engines?.node === undefined) {
118
+ warning('M8-node-engines', 'engines.node 未声明;宿主运行在 Node 22+')
119
+ }
120
+ if (manifest.license === undefined) {
121
+ warning('M9-license', 'license 未声明')
122
+ }
123
+
124
+ const filesList = Array.isArray(manifest.files) ? manifest.files : []
125
+ for (const entry of filesList) {
126
+ if (typeof entry === 'string' && SUSPICIOUS_FILE.test(entry)) {
127
+ error('M10-secrets', `files 白名单包含疑似凭据文件: ${entry}`)
128
+ }
129
+ }
130
+ for (const candidate of ['.env', 'id_rsa']) {
131
+ const candidatePath = join(pluginDir, candidate)
132
+ if (existsSync(candidatePath) && statSync(candidatePath).isFile() && filesList.length === 0) {
133
+ warning('M10-secrets', `目录含 ${candidate} 且未用 files 白名单限定发布内容,npm pack 可能把它带上`)
134
+ }
135
+ }
136
+
137
+ return { findings, manifest, rows }
138
+ }