@perrylink/dsh-plugin-doctor 0.1.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,71 @@
1
+ // dsh-plugin-doctor 检测框架:零依赖,检查注册 / 运行 / 判定 / 渲染
2
+ export class Doctor {
3
+ constructor() {
4
+ this.checks = []
5
+ }
6
+
7
+ add(group, name, fn, opts = {}) {
8
+ this.checks.push({ group, name, fn, opts })
9
+ }
10
+
11
+ async run(ctx, { groups } = {}) {
12
+ const results = []
13
+ for (const c of this.checks) {
14
+ if (groups && !groups.includes(c.group)) continue
15
+ let res
16
+ try {
17
+ res = await c.fn(ctx)
18
+ if (typeof res === 'string') res = { status: 'pass', message: res }
19
+ if (!res || typeof res !== 'object') res = { status: 'pass', message: String(res) }
20
+ } catch (err) {
21
+ res = { status: 'error', message: err && err.stack ? err.stack : String(err) }
22
+ }
23
+ res.group = c.group
24
+ res.name = c.name
25
+ res.critical = !!c.opts.critical
26
+ results.push(res)
27
+ }
28
+ return results
29
+ }
30
+ }
31
+
32
+ // 最坏状态排序:error/fail > warn > skip > pass
33
+ export function verdict(results) {
34
+ let worst = 'pass'
35
+ for (const r of results) {
36
+ if (r.status === 'error' && worst !== 'error') worst = 'error'
37
+ else if (r.status === 'fail' && worst !== 'error') worst = 'fail'
38
+ else if (r.status === 'warn' && worst === 'pass') worst = 'warn'
39
+ else if (r.status === 'skip' && worst === 'pass') worst = 'skip'
40
+ }
41
+ const criticalFail = results.some((r) => r.critical && (r.status === 'fail' || r.status === 'error'))
42
+ return { worst, criticalFail, ok: worst === 'pass' }
43
+ }
44
+
45
+ const ICONS = { pass: '[PASS]', warn: '[WARN]', fail: '[FAIL]', error: '[ERROR]', skip: '[SKIP]' }
46
+
47
+ export function render(results) {
48
+ const lines = []
49
+ let lastGroup = null
50
+ for (const r of results) {
51
+ if (r.group !== lastGroup) {
52
+ lines.push('', `== ${r.group} ==`)
53
+ lastGroup = r.group
54
+ }
55
+ const tag = r.critical ? ' [关键]' : ''
56
+ lines.push(`${ICONS[r.status] ?? '[?]'} ${r.name}${tag}`)
57
+ const msg = String(r.message ?? '').trim()
58
+ if (msg) {
59
+ for (const line of msg.split('\n')) lines.push(` ${line}`)
60
+ }
61
+ if (r.evidence) lines.push(` 证据: ${r.evidence}`)
62
+ }
63
+ const v = verdict(results)
64
+ const counts = {}
65
+ for (const r of results) counts[r.status] = (counts[r.status] ?? 0) + 1
66
+ lines.push(
67
+ '',
68
+ `=== 汇总: ${Object.entries(counts).map(([k, n]) => `${k}=${n}`).join(' ')} | 总判定: ${v.worst.toUpperCase()}${v.criticalFail ? '(含关键项失败)' : ''} ===`,
69
+ )
70
+ return lines.join('\n')
71
+ }
package/lib/util.mjs ADDED
@@ -0,0 +1,90 @@
1
+ // 工具:临时 DSH_HOME 沙箱 + 子进程执行(输出落盘,规避管道捕获限制)
2
+ import { spawnSync } from 'node:child_process'
3
+ import {
4
+ mkdtempSync, mkdirSync, rmSync, openSync, closeSync, readFileSync, writeFileSync, existsSync, readdirSync,
5
+ } from 'node:fs'
6
+ import { tmpdir } from 'node:os'
7
+ import path from 'node:path'
8
+
9
+ // 沙箱目录一律建在 %TEMP%,绝不触碰真实 ~/.dsh(红线 3)
10
+ export function makeSandbox(label) {
11
+ const root = mkdtempSync(path.join(tmpdir(), `dsh-doctor-${label}-`))
12
+ const home = path.join(root, 'home')
13
+ const logs = path.join(root, 'logs')
14
+ mkdirSync(home, { recursive: true })
15
+ mkdirSync(logs, { recursive: true })
16
+ return { root, home, logs }
17
+ }
18
+
19
+ export function cleanSandbox(sb) {
20
+ if (sb && sb.root) {
21
+ try { rmSync(sb.root, { recursive: true, force: true }) } catch {}
22
+ }
23
+ }
24
+
25
+ // 执行命令:stdout/stderr 分别写入日志文件(不建管道),返回退出码与全文
26
+ export function runStep(label, cmd, args, { env = {}, timeout = 120_000, cwd, logDir, shell } = {}) {
27
+ const dir = logDir ?? process.cwd()
28
+ const outPath = path.join(dir, `${label}.out.log`)
29
+ const errPath = path.join(dir, `${label}.err.log`)
30
+ const fdOut = openSync(outPath, 'w')
31
+ const fdErr = openSync(errPath, 'w')
32
+ const res = spawnSync(cmd, args, {
33
+ cwd,
34
+ env: { ...process.env, ...env },
35
+ stdio: ['ignore', fdOut, fdErr],
36
+ timeout,
37
+ windowsHide: true,
38
+ // Windows 下解析 npm.cmd/pnpm.cmd 等 shim 需要 shell;直接调用 node.exe 等真实可执行文件时禁用
39
+ shell: shell ?? process.platform === 'win32',
40
+ })
41
+ closeSync(fdOut)
42
+ closeSync(fdErr)
43
+ const read = (p) => { try { return readFileSync(p, 'utf8') } catch { return '' } }
44
+ return {
45
+ code: res.status,
46
+ signal: res.signal,
47
+ spawnError: res.error ? String(res.error) : null,
48
+ out: read(outPath),
49
+ err: read(errPath),
50
+ outPath,
51
+ errPath,
52
+ ok: res.status === 0 && !res.error,
53
+ }
54
+ }
55
+
56
+ export function tail(text, n = 8) {
57
+ if (!text) return '(无输出)'
58
+ const lines = text.trim().split('\n')
59
+ return lines.slice(-n).join('\n')
60
+ }
61
+
62
+ export const pass = (message) => ({ status: 'pass', message })
63
+ export const fail = (message) => ({ status: 'fail', message })
64
+ export const warn = (message) => ({ status: 'warn', message })
65
+ export const skip = (reason) => ({ status: 'skip', message: reason })
66
+
67
+ export function readJson(p) {
68
+ return JSON.parse(readFileSync(p, 'utf8'))
69
+ }
70
+
71
+ export function exists(p) {
72
+ return existsSync(p)
73
+ }
74
+
75
+ export function writeJson(p, obj) {
76
+ writeFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8')
77
+ }
78
+
79
+ // 递归收集目录下匹配文件(跳过 node_modules/lib/dist 等构建产物)
80
+ export function findFiles(dir, sub, re, out = []) {
81
+ const base = path.join(dir, sub)
82
+ if (!existsSync(base)) return out
83
+ for (const entry of readdirSync(base, { withFileTypes: true })) {
84
+ if (['node_modules', 'lib', 'dist', '.git'].includes(entry.name)) continue
85
+ const p = path.join(base, entry.name)
86
+ if (entry.isDirectory()) findFiles(dir, path.join(sub, entry.name), re, out)
87
+ else if (re.test(entry.name)) out.push(p)
88
+ }
89
+ return out
90
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@perrylink/dsh-plugin-doctor",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency static + sandbox smoke detector for DeepSeek Harness (dsh) plugins: package-structure gates (R), cordis contract scans (K), keyless-headless sandbox smoke (D), and ecosystem-listing checks (CC).",
5
+ "type": "module",
6
+ "main": "doctor.mjs",
7
+ "bin": {
8
+ "dsh-plugin-doctor": "./doctor.mjs"
9
+ },
10
+ "files": [
11
+ "doctor.mjs",
12
+ "lib/",
13
+ "README.md",
14
+ "SURVEY.md"
15
+ ],
16
+ "engines": {
17
+ "node": "^22.19.0 || >=24.0.0"
18
+ },
19
+ "keywords": [
20
+ "dsh",
21
+ "deepseek-harness",
22
+ "cordis",
23
+ "plugin",
24
+ "doctor",
25
+ "lint",
26
+ "smoke-test",
27
+ "verification"
28
+ ],
29
+ "license": "Apache-2.0",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/PerryLink/dsh-plugin-doctor.git"
33
+ },
34
+ "scripts": {
35
+ "test": "node --check doctor.mjs && node --check lib/framework.mjs && node --check lib/util.mjs && node --check lib/checks-package.mjs && node --check lib/checks-cordis.mjs && node --check lib/checks-smoke.mjs && node --check lib/checks-collections.mjs"
36
+ }
37
+ }