@tokensapi/dsh-plugin-check 0.2.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.
@@ -1,174 +1,174 @@
1
- /* ============================================================
2
- * 阶段②:隔离启动冒烟(联网,分钟级)
3
- * ============================================================
4
- * 在临时工作区里按插件自己声明的 peer 范围装出一套真实运行时,
5
- * 然后在子进程里逐行做真实网关会做的事:import 补丁行入口、
6
- * new Context()、ctx.plugin() 应用。这里能过,插件就不会以
7
- * "装上即崩"的方式杀死用户的 Cowork:
8
- * - 装不上(依赖解析失败)在 install 步暴露;
9
- * - import 期崩溃(顶层异常/缺模块/ESM 形状)在 import 步暴露;
10
- * - 应用期崩溃(apply 抛出/插件形状不合法)在 plugin 步暴露;
11
- * - 声明了 inject 而服务未注入属正常停车,不算失败。
12
- * 全程 --ignore-scripts,被测代码没有机会在检查器进程里执行
13
- * 安装钩子;插件代码只在一次性子进程里运行,超时即杀。
14
- * ============================================================ */
15
- import { spawnSync } from 'node:child_process'
16
- import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync, readdirSync } from 'node:fs'
17
- import { tmpdir } from 'node:os'
18
- import { join, resolve } from 'node:path'
19
- import { runNpm } from './npm.mjs'
20
-
21
- const RUNNER_TIMEOUT_MS = 90_000
22
- const INSTALL_TIMEOUT_MS = 300_000
23
-
24
- /** 子进程执行的行级冒烟脚本;结果写文件,避开插件对 stdout 的污染。 */
25
- const RUNNER_SOURCE = `
26
- import { pathToFileURL } from 'node:url'
27
- import { writeFileSync } from 'node:fs'
28
- const [workspace, resultPath, rowsJson] = process.argv.slice(2)
29
- const rows = JSON.parse(rowsJson)
30
- const results = []
31
- let current = null
32
- process.on('uncaughtException', (cause) => finish(cause))
33
- process.on('unhandledRejection', (cause) => finish(cause))
34
- function finish(cause) {
35
- if (current !== null) {
36
- results.push({ ...current, status: 'failed', stage: current.stage,
37
- error: cause instanceof Error ? cause.message : String(cause) })
38
- current = null
39
- }
40
- writeFileSync(resultPath, JSON.stringify(results))
41
- process.exit(0)
42
- }
43
- const { createRequire } = await import('node:module')
44
- const workspaceRequire = createRequire(pathToFileURL(workspace + '/x.js'))
45
- let Context
46
- try {
47
- const cordis = await import(pathToFileURL(workspaceRequire.resolve('@deepseek-ai/cordis')))
48
- Context = cordis.Context
49
- } catch (cause) {
50
- writeFileSync(resultPath, JSON.stringify([{ name: '@deepseek-ai/cordis', stage: 'import',
51
- status: 'failed', error: '无法载入 cordis 运行时: ' + (cause instanceof Error ? cause.message : String(cause)) }]))
52
- process.exit(0)
53
- }
54
- for (const row of rows) {
55
- current = { name: row.name, stage: 'resolve' }
56
- try {
57
- const entryPath = workspaceRequire.resolve(row.name)
58
- current.stage = 'import'
59
- const mod = await import(pathToFileURL(entryPath))
60
- const plugin = mod.default ?? mod
61
- current.stage = 'apply'
62
- const ctx = new Context()
63
- if (row.dynamicConfig) ctx.plugin(plugin)
64
- else if (row.config === undefined) ctx.plugin(plugin)
65
- else ctx.plugin(plugin, row.config)
66
- await new Promise((r) => setTimeout(r, 750))
67
- results.push({ name: row.name, status: 'ok',
68
- note: row.dynamicConfig ? '配置含动态表达式,以空配置应用' : undefined })
69
- current = null
70
- } catch (cause) {
71
- results.push({ name: row.name, status: 'failed', stage: current.stage,
72
- error: cause instanceof Error ? cause.message : String(cause) })
73
- current = null
74
- }
75
- }
76
- writeFileSync(resultPath, JSON.stringify(results))
77
- process.exit(0)
78
- `
79
-
80
-
81
- /**
82
- * 对插件目录执行隔离冒烟。
83
- * @param {string} pluginDir - 插件根目录。
84
- * @param {object} manifest - 已解析的 package.json。
85
- * @param {Array<{name: string, config?: unknown, disabled?: boolean, dynamicConfig: boolean}>} rows - 补丁行。
86
- * @param {{ keepWorkspace?: boolean }} [options]
87
- * @returns {{ findings: Array<{level: 'error'|'warning', rule: string, message: string}>, workspace?: string }}
88
- */
89
- export function checkSmoke(pluginDir, manifest, rows, options = {}) {
90
- const findings = []
91
- const error = (rule, message) => findings.push({ level: 'error', rule, message })
92
- const warning = (rule, message) => findings.push({ level: 'warning', rule, message })
93
-
94
- const workspace = mkdtempSync(join(tmpdir(), 'dsh-plugin-check-'))
95
- const cleanup = () => {
96
- if (options.keepWorkspace) return
97
- try { rmSync(workspace, { recursive: true, force: true }) } catch { /* 临时目录残留不影响判定 */ }
98
- }
99
-
100
- try {
101
- // S1 打包:--ignore-scripts 跳过 prepack 等脚本,只验证发布内容本身。
102
- const pack = runNpm(['pack', resolve(pluginDir), '--ignore-scripts', '--pack-destination', workspace], workspace, INSTALL_TIMEOUT_MS)
103
- if (pack.status !== 0) {
104
- error('S1-pack', `npm pack 失败: ${(pack.stderr || pack.stdout || '').trim().split('\n').slice(-3).join(' ')}`)
105
- return { findings, workspace: options.keepWorkspace ? workspace : undefined }
106
- }
107
- const tarball = readdirSync(workspace).find(name => name.endsWith('.tgz'))
108
- if (tarball === undefined) {
109
- error('S1-pack', 'npm pack 未产出 tgz')
110
- return { findings, workspace: options.keepWorkspace ? workspace : undefined }
111
- }
112
-
113
- // S2 安装:插件走 tgz(等价用户侧的受控安装),运行时按插件自己
114
- // 声明的 peer 范围解析;cordis 是冒烟宿主的最低要求,未声明则补上。
115
- const dependencies = { [manifest.name]: `file:./${tarball}` }
116
- const peers = manifest.peerDependencies ?? {}
117
- for (const [name, range] of Object.entries(peers)) dependencies[name] = range
118
- if (dependencies['@deepseek-ai/cordis'] === undefined) {
119
- dependencies['@deepseek-ai/cordis'] = '*'
120
- warning('S2-install', 'peerDependencies 未声明 @deepseek-ai/cordis,冒烟以最新版补齐;建议显式声明')
121
- }
122
- writeFileSync(join(workspace, 'package.json'), JSON.stringify({
123
- name: 'dsh-plugin-check-workspace',
124
- private: true,
125
- dependencies,
126
- }, undefined, 2))
127
- const install = runNpm(
128
- ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--loglevel=error'],
129
- workspace,
130
- INSTALL_TIMEOUT_MS,
131
- )
132
- if (install.status !== 0) {
133
- error('S2-install', `依赖装不出来(用户侧受控安装会同样失败): ${(install.stderr || '').trim().split('\n').slice(-4).join(' ')}`)
134
- return { findings, workspace: options.keepWorkspace ? workspace : undefined }
135
- }
136
-
137
- // S3+S4 行级冒烟:解析/import/apply 三段判定在子进程完成。
138
- const activeRows = rows.filter(row => !row.disabled)
139
- if (activeRows.length === 0) {
140
- warning('S3-rows', '补丁没有启用状态的行,冒烟无事可做')
141
- return { findings, workspace: options.keepWorkspace ? workspace : undefined }
142
- }
143
- const runnerPath = join(workspace, '__runner.mjs')
144
- const resultPath = join(workspace, '__result.json')
145
- writeFileSync(runnerPath, RUNNER_SOURCE)
146
- const run = spawnSync(process.execPath, [
147
- runnerPath,
148
- workspace,
149
- resultPath,
150
- JSON.stringify(activeRows.map(row => ({ name: row.name, config: row.config, dynamicConfig: row.dynamicConfig }))),
151
- ], { cwd: workspace, encoding: 'utf8', timeout: RUNNER_TIMEOUT_MS })
152
- if (!existsSync(resultPath)) {
153
- const reason = run.signal === 'SIGTERM'
154
- ? `超时(${RUNNER_TIMEOUT_MS / 1000}s):入口疑似在顶层做阻塞等待`
155
- : (run.stderr || '子进程未产出结果').trim().split('\n').slice(-3).join(' ')
156
- error('S4-apply', `冒烟子进程异常: ${reason}`)
157
- return { findings, workspace: options.keepWorkspace ? workspace : undefined }
158
- }
159
- const results = JSON.parse(readFileSync(resultPath, 'utf8'))
160
- const passed = results.filter(result => result.status === 'ok').length
161
- findings.push({ level: 'info', rule: 'S4-apply', message: `隔离冒烟: ${passed}/${results.length} 行通过(解析→import→应用)` })
162
- for (const result of results) {
163
- if (result.status === 'ok') {
164
- if (result.note !== undefined) warning('S4-apply', `${result.name}: ${result.note}`)
165
- continue
166
- }
167
- const stageLabel = { resolve: '入口解析', import: 'import 载入', apply: '插件应用' }[result.stage] ?? result.stage
168
- error('S4-apply', `${result.name} 在 ${stageLabel} 阶段失败: ${result.error}(装入用户 Cowork 会在启动链同点崩溃)`)
169
- }
170
- return { findings, workspace: options.keepWorkspace ? workspace : undefined }
171
- } finally {
172
- cleanup()
173
- }
174
- }
1
+ /* ============================================================
2
+ * 阶段②:隔离启动冒烟(联网,分钟级)
3
+ * ============================================================
4
+ * 在临时工作区里按插件自己声明的 peer 范围装出一套真实运行时,
5
+ * 然后在子进程里逐行做真实网关会做的事:import 补丁行入口、
6
+ * new Context()、ctx.plugin() 应用。这里能过,插件就不会以
7
+ * "装上即崩"的方式杀死用户的 Cowork:
8
+ * - 装不上(依赖解析失败)在 install 步暴露;
9
+ * - import 期崩溃(顶层异常/缺模块/ESM 形状)在 import 步暴露;
10
+ * - 应用期崩溃(apply 抛出/插件形状不合法)在 plugin 步暴露;
11
+ * - 声明了 inject 而服务未注入属正常停车,不算失败。
12
+ * 全程 --ignore-scripts,被测代码没有机会在检查器进程里执行
13
+ * 安装钩子;插件代码只在一次性子进程里运行,超时即杀。
14
+ * ============================================================ */
15
+ import { spawnSync } from 'node:child_process'
16
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync, readdirSync } from 'node:fs'
17
+ import { tmpdir } from 'node:os'
18
+ import { join, resolve } from 'node:path'
19
+ import { runNpm } from './npm.mjs'
20
+
21
+ const RUNNER_TIMEOUT_MS = 90_000
22
+ const INSTALL_TIMEOUT_MS = 300_000
23
+
24
+ /** 子进程执行的行级冒烟脚本;结果写文件,避开插件对 stdout 的污染。 */
25
+ const RUNNER_SOURCE = `
26
+ import { pathToFileURL } from 'node:url'
27
+ import { writeFileSync } from 'node:fs'
28
+ const [workspace, resultPath, rowsJson] = process.argv.slice(2)
29
+ const rows = JSON.parse(rowsJson)
30
+ const results = []
31
+ let current = null
32
+ process.on('uncaughtException', (cause) => finish(cause))
33
+ process.on('unhandledRejection', (cause) => finish(cause))
34
+ function finish(cause) {
35
+ if (current !== null) {
36
+ results.push({ ...current, status: 'failed', stage: current.stage,
37
+ error: cause instanceof Error ? cause.message : String(cause) })
38
+ current = null
39
+ }
40
+ writeFileSync(resultPath, JSON.stringify(results))
41
+ process.exit(0)
42
+ }
43
+ const { createRequire } = await import('node:module')
44
+ const workspaceRequire = createRequire(pathToFileURL(workspace + '/x.js'))
45
+ let Context
46
+ try {
47
+ const cordis = await import(pathToFileURL(workspaceRequire.resolve('@deepseek-ai/cordis')))
48
+ Context = cordis.Context
49
+ } catch (cause) {
50
+ writeFileSync(resultPath, JSON.stringify([{ name: '@deepseek-ai/cordis', stage: 'import',
51
+ status: 'failed', error: '无法载入 cordis 运行时: ' + (cause instanceof Error ? cause.message : String(cause)) }]))
52
+ process.exit(0)
53
+ }
54
+ for (const row of rows) {
55
+ current = { name: row.name, stage: 'resolve' }
56
+ try {
57
+ const entryPath = workspaceRequire.resolve(row.name)
58
+ current.stage = 'import'
59
+ const mod = await import(pathToFileURL(entryPath))
60
+ const plugin = mod.default ?? mod
61
+ current.stage = 'apply'
62
+ const ctx = new Context()
63
+ if (row.dynamicConfig) ctx.plugin(plugin)
64
+ else if (row.config === undefined) ctx.plugin(plugin)
65
+ else ctx.plugin(plugin, row.config)
66
+ await new Promise((r) => setTimeout(r, 750))
67
+ results.push({ name: row.name, status: 'ok',
68
+ note: row.dynamicConfig ? '配置含动态表达式,以空配置应用' : undefined })
69
+ current = null
70
+ } catch (cause) {
71
+ results.push({ name: row.name, status: 'failed', stage: current.stage,
72
+ error: cause instanceof Error ? cause.message : String(cause) })
73
+ current = null
74
+ }
75
+ }
76
+ writeFileSync(resultPath, JSON.stringify(results))
77
+ process.exit(0)
78
+ `
79
+
80
+
81
+ /**
82
+ * 对插件目录执行隔离冒烟。
83
+ * @param {string} pluginDir - 插件根目录。
84
+ * @param {object} manifest - 已解析的 package.json。
85
+ * @param {Array<{name: string, config?: unknown, disabled?: boolean, dynamicConfig: boolean}>} rows - 补丁行。
86
+ * @param {{ keepWorkspace?: boolean }} [options]
87
+ * @returns {{ findings: Array<{level: 'error'|'warning', rule: string, message: string}>, workspace?: string }}
88
+ */
89
+ export function checkSmoke(pluginDir, manifest, rows, options = {}) {
90
+ const findings = []
91
+ const error = (rule, message) => findings.push({ level: 'error', rule, message })
92
+ const warning = (rule, message) => findings.push({ level: 'warning', rule, message })
93
+
94
+ const workspace = mkdtempSync(join(tmpdir(), 'dsh-plugin-check-'))
95
+ const cleanup = () => {
96
+ if (options.keepWorkspace) return
97
+ try { rmSync(workspace, { recursive: true, force: true }) } catch { /* 临时目录残留不影响判定 */ }
98
+ }
99
+
100
+ try {
101
+ // S1 打包:--ignore-scripts 跳过 prepack 等脚本,只验证发布内容本身。
102
+ const pack = runNpm(['pack', resolve(pluginDir), '--ignore-scripts', '--pack-destination', workspace], workspace, INSTALL_TIMEOUT_MS)
103
+ if (pack.status !== 0) {
104
+ error('S1-pack', `npm pack 失败: ${(pack.stderr || pack.stdout || '').trim().split('\n').slice(-3).join(' ')}`)
105
+ return { findings, workspace: options.keepWorkspace ? workspace : undefined }
106
+ }
107
+ const tarball = readdirSync(workspace).find(name => name.endsWith('.tgz'))
108
+ if (tarball === undefined) {
109
+ error('S1-pack', 'npm pack 未产出 tgz')
110
+ return { findings, workspace: options.keepWorkspace ? workspace : undefined }
111
+ }
112
+
113
+ // S2 安装:插件走 tgz(等价用户侧的受控安装),运行时按插件自己
114
+ // 声明的 peer 范围解析;cordis 是冒烟宿主的最低要求,未声明则补上。
115
+ const dependencies = { [manifest.name]: `file:./${tarball}` }
116
+ const peers = manifest.peerDependencies ?? {}
117
+ for (const [name, range] of Object.entries(peers)) dependencies[name] = range
118
+ if (dependencies['@deepseek-ai/cordis'] === undefined) {
119
+ dependencies['@deepseek-ai/cordis'] = '*'
120
+ warning('S2-install', 'peerDependencies 未声明 @deepseek-ai/cordis,冒烟以最新版补齐;建议显式声明')
121
+ }
122
+ writeFileSync(join(workspace, 'package.json'), JSON.stringify({
123
+ name: 'dsh-plugin-check-workspace',
124
+ private: true,
125
+ dependencies,
126
+ }, undefined, 2))
127
+ const install = runNpm(
128
+ ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--loglevel=error'],
129
+ workspace,
130
+ INSTALL_TIMEOUT_MS,
131
+ )
132
+ if (install.status !== 0) {
133
+ error('S2-install', `依赖装不出来(用户侧受控安装会同样失败): ${(install.stderr || '').trim().split('\n').slice(-4).join(' ')}`)
134
+ return { findings, workspace: options.keepWorkspace ? workspace : undefined }
135
+ }
136
+
137
+ // S3+S4 行级冒烟:解析/import/apply 三段判定在子进程完成。
138
+ const activeRows = rows.filter(row => !row.disabled)
139
+ if (activeRows.length === 0) {
140
+ warning('S3-rows', '补丁没有启用状态的行,冒烟无事可做')
141
+ return { findings, workspace: options.keepWorkspace ? workspace : undefined }
142
+ }
143
+ const runnerPath = join(workspace, '__runner.mjs')
144
+ const resultPath = join(workspace, '__result.json')
145
+ writeFileSync(runnerPath, RUNNER_SOURCE)
146
+ const run = spawnSync(process.execPath, [
147
+ runnerPath,
148
+ workspace,
149
+ resultPath,
150
+ JSON.stringify(activeRows.map(row => ({ name: row.name, config: row.config, dynamicConfig: row.dynamicConfig }))),
151
+ ], { cwd: workspace, encoding: 'utf8', timeout: RUNNER_TIMEOUT_MS })
152
+ if (!existsSync(resultPath)) {
153
+ const reason = run.signal === 'SIGTERM'
154
+ ? `超时(${RUNNER_TIMEOUT_MS / 1000}s):入口疑似在顶层做阻塞等待`
155
+ : (run.stderr || '子进程未产出结果').trim().split('\n').slice(-3).join(' ')
156
+ error('S4-apply', `冒烟子进程异常: ${reason}`)
157
+ return { findings, workspace: options.keepWorkspace ? workspace : undefined }
158
+ }
159
+ const results = JSON.parse(readFileSync(resultPath, 'utf8'))
160
+ const passed = results.filter(result => result.status === 'ok').length
161
+ findings.push({ level: 'info', rule: 'S4-apply', message: `隔离冒烟: ${passed}/${results.length} 行通过(解析→import→应用)` })
162
+ for (const result of results) {
163
+ if (result.status === 'ok') {
164
+ if (result.note !== undefined) warning('S4-apply', `${result.name}: ${result.note}`)
165
+ continue
166
+ }
167
+ const stageLabel = { resolve: '入口解析', import: 'import 载入', apply: '插件应用' }[result.stage] ?? result.stage
168
+ error('S4-apply', `${result.name} 在 ${stageLabel} 阶段失败: ${result.error}(装入用户 Cowork 会在启动链同点崩溃)`)
169
+ }
170
+ return { findings, workspace: options.keepWorkspace ? workspace : undefined }
171
+ } finally {
172
+ cleanup()
173
+ }
174
+ }
package/package.json CHANGED
@@ -1,30 +1,44 @@
1
- {
2
- "name": "@tokensapi/dsh-plugin-check",
3
- "version": "0.2.0",
4
- "description": "TokensCowork/DSH \u63d2\u4ef6\u5408\u683c\u6027\u4f53\u68c0:\u4e00\u6761\u547d\u4ee4\u5224\u65ad\u63d2\u4ef6\u80fd\u5426\u5b89\u5168\u88c5\u5165\u5bbf\u4e3b,\u4e0d\u628a Cowork \u641e\u5d29\u3002",
5
- "type": "module",
6
- "bin": {
7
- "dsh-plugin-check": "bin/dsh-plugin-check.mjs"
8
- },
9
- "files": [
10
- "bin",
11
- "lib",
12
- "README.md"
13
- ],
14
- "scripts": {
15
- "test": "node --test \"tests/*.test.mjs\""
16
- },
17
- "engines": {
18
- "node": ">=22.0.0"
19
- },
20
- "dependencies": {
21
- "js-yaml": "^4.1.0",
22
- "semver": "^7.6.0"
23
- },
24
- "license": "MIT",
25
- "repository": {
26
- "type": "git",
27
- "url": "https://github.com/TokensAPI/TokensCowork.git",
28
- "directory": "plugin-check"
29
- }
30
- }
1
+ {
2
+ "name": "@tokensapi/dsh-plugin-check",
3
+ "version": "0.3.1",
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
+ }