@perrylink/dsh-plugin-doctor 0.2.2 → 0.2.3

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/lib/framework.mjs CHANGED
@@ -1,110 +1,110 @@
1
- // 检测框架:零依赖,检查注册 / 运行 / 判定 / 渲染
2
- //
3
- // 不可变契约(R-fix 0B 冻结;家族 37 仓的 CI 依赖它们,任何改动都会造成硬红):
4
- // 1. 结果项 res.name 保留 `^R[0-8] / ^K[1-9] / ^D\d / ^CC\d ` 前缀 —— ID 只新增字段,绝不从 name 里拆走
5
- // 2. 结果项仍是扁平数组(JSON 的 .results),按注册顺序稳定输出
6
- // 3. 渲染行的形态不变(`[STATUS] <name>`),stdout 必须仍含 "R0 " 与 "K1 "
7
- // 4. skip 不算 fail(verdict.ok 只看 fail/error)
8
- export const GROUP_IDS = {
9
- '静态·包结构': 'R',
10
- '静态·cordis 契约扫描': 'K',
11
- '动态·沙箱冒烟': 'D',
12
- '生态·集合站清单': 'CC',
13
- }
14
-
15
- export class Doctor {
16
- constructor() {
17
- this.checks = []
18
- }
19
-
20
- add(group, name, fn, opts = {}) {
21
- this.checks.push({ group, name, fn, opts })
22
- }
23
-
24
- async run(ctx, { groups } = {}) {
25
- const results = []
26
- for (const c of this.checks) {
27
- if (groups && !groups.includes(c.group)) continue
28
- let res
29
- try {
30
- res = await c.fn(ctx)
31
- if (typeof res === 'string') res = { status: 'pass', message: res }
32
- if (!res || typeof res !== 'object') res = { status: 'pass', message: String(res) }
33
- } catch (err) {
34
- res = { status: 'error', message: err && err.stack ? err.stack : String(err), category: 'doctor-internal' }
35
- }
36
- res.group = c.group
37
- res.name = c.name
38
- res.critical = !!c.opts.critical
39
- // 稳定 ID 从 name 前缀派生(只读,不改写 name)
40
- const m = /^([A-Z]{1,3})(\d+)\s/.exec(c.name)
41
- res.id = m ? `${m[1]}${m[2]}` : null
42
- res.groupId = GROUP_IDS[c.group] ?? null
43
- if (!res.category) res.category = res.status === 'skip' ? 'not-applicable' : 'plugin-defect'
44
- results.push(res)
45
- }
46
- return results
47
- }
48
- }
49
-
50
- /** 按组统计:total / ran(非 skip)/ skipped / 各状态计数 / 累计 filesInspected */
51
- export function summarizeGroups(results) {
52
- const out = {}
53
- for (const r of results) {
54
- const g = r.groupId ?? '?'
55
- const s = (out[g] ??= { total: 0, ran: 0, skipped: 0, pass: 0, warn: 0, fail: 0, error: 0, skip: 0, filesInspected: 0 })
56
- s.total += 1
57
- s[r.status] = (s[r.status] ?? 0) + 1
58
- if (r.status === 'skip') s.skipped += 1
59
- else s.ran += 1
60
- if (typeof r.filesInspected === 'number') s.filesInspected += r.filesInspected
61
- }
62
- return out
63
- }
64
-
65
- /** 被请求、但一条都没真跑的组(全部 skip)→ 该次运行「降级」,不得裸报成功 */
66
- export function degradedGroups(groups) {
67
- return Object.entries(groups).filter(([, v]) => v.total > 0 && v.ran === 0).map(([k]) => k)
68
- }
69
-
70
- // 最坏状态排序:error/fail > warn > skip > pass
71
- export function verdict(results, { degraded = [] } = {}) {
72
- let worst = 'pass'
73
- for (const r of results) {
74
- if (r.status === 'error' && worst !== 'error') worst = 'error'
75
- else if (r.status === 'fail' && worst !== 'error') worst = 'fail'
76
- else if (r.status === 'warn' && worst === 'pass') worst = 'warn'
77
- else if (r.status === 'skip' && worst === 'pass') worst = 'skip'
78
- }
79
- const criticalFail = results.some((r) => r.critical && (r.status === 'fail' || r.status === 'error'))
80
- // 退出码契约:0 = 无 fail/error(可含 warn/skip);1 = 存在 fail/error
81
- return { worst, criticalFail, ok: worst !== 'fail' && worst !== 'error', degraded }
82
- }
83
-
84
- const ICONS = { pass: '[PASS]', warn: '[WARN]', fail: '[FAIL]', error: '[ERROR]', skip: '[SKIP]' }
85
-
86
- export function render(results) {
87
- const lines = []
88
- let lastGroup = null
89
- for (const r of results) {
90
- if (r.group !== lastGroup) {
91
- lines.push('', `== ${r.group} ==`)
92
- lastGroup = r.group
93
- }
94
- const tag = r.critical ? ' [关键]' : ''
95
- lines.push(`${ICONS[r.status] ?? '[?]'} ${r.name}${tag}`)
96
- const msg = String(r.message ?? '').trim()
97
- if (msg) {
98
- for (const line of msg.split('\n')) lines.push(` ${line}`)
99
- }
100
- if (r.evidence) lines.push(` 证据: ${r.evidence}`)
101
- }
102
- const v = verdict(results)
103
- const counts = {}
104
- for (const r of results) counts[r.status] = (counts[r.status] ?? 0) + 1
105
- lines.push(
106
- '',
107
- `=== 汇总: ${Object.entries(counts).map(([k, n]) => `${k}=${n}`).join(' ')} | 总判定: ${v.worst.toUpperCase()}${v.criticalFail ? '(含关键项失败)' : ''} ===`,
108
- )
109
- return lines.join('\n')
110
- }
1
+ // 检测框架:零依赖,检查注册 / 运行 / 判定 / 渲染
2
+ //
3
+ // 不可变契约(R-fix 0B 冻结;家族 37 仓的 CI 依赖它们,任何改动都会造成硬红):
4
+ // 1. 结果项 res.name 保留 `^R[0-8] / ^K[1-9] / ^D\d / ^CC\d ` 前缀 —— ID 只新增字段,绝不从 name 里拆走
5
+ // 2. 结果项仍是扁平数组(JSON 的 .results),按注册顺序稳定输出
6
+ // 3. 渲染行的形态不变(`[STATUS] <name>`),stdout 必须仍含 "R0 " 与 "K1 "
7
+ // 4. skip 不算 fail(verdict.ok 只看 fail/error)
8
+ export const GROUP_IDS = {
9
+ '静态·包结构': 'R',
10
+ '静态·cordis 契约扫描': 'K',
11
+ '动态·沙箱冒烟': 'D',
12
+ '生态·集合站清单': 'CC',
13
+ }
14
+
15
+ export class Doctor {
16
+ constructor() {
17
+ this.checks = []
18
+ }
19
+
20
+ add(group, name, fn, opts = {}) {
21
+ this.checks.push({ group, name, fn, opts })
22
+ }
23
+
24
+ async run(ctx, { groups } = {}) {
25
+ const results = []
26
+ for (const c of this.checks) {
27
+ if (groups && !groups.includes(c.group)) continue
28
+ let res
29
+ try {
30
+ res = await c.fn(ctx)
31
+ if (typeof res === 'string') res = { status: 'pass', message: res }
32
+ if (!res || typeof res !== 'object') res = { status: 'pass', message: String(res) }
33
+ } catch (err) {
34
+ res = { status: 'error', message: err && err.stack ? err.stack : String(err), category: 'doctor-internal' }
35
+ }
36
+ res.group = c.group
37
+ res.name = c.name
38
+ res.critical = !!c.opts.critical
39
+ // 稳定 ID 从 name 前缀派生(只读,不改写 name)
40
+ const m = /^([A-Z]{1,3})(\d+)\s/.exec(c.name)
41
+ res.id = m ? `${m[1]}${m[2]}` : null
42
+ res.groupId = GROUP_IDS[c.group] ?? null
43
+ if (!res.category) res.category = res.status === 'skip' ? 'not-applicable' : 'plugin-defect'
44
+ results.push(res)
45
+ }
46
+ return results
47
+ }
48
+ }
49
+
50
+ /** 按组统计:total / ran(非 skip)/ skipped / 各状态计数 / 累计 filesInspected */
51
+ export function summarizeGroups(results) {
52
+ const out = {}
53
+ for (const r of results) {
54
+ const g = r.groupId ?? '?'
55
+ const s = (out[g] ??= { total: 0, ran: 0, skipped: 0, pass: 0, warn: 0, fail: 0, error: 0, skip: 0, filesInspected: 0 })
56
+ s.total += 1
57
+ s[r.status] = (s[r.status] ?? 0) + 1
58
+ if (r.status === 'skip') s.skipped += 1
59
+ else s.ran += 1
60
+ if (typeof r.filesInspected === 'number') s.filesInspected += r.filesInspected
61
+ }
62
+ return out
63
+ }
64
+
65
+ /** 被请求、但一条都没真跑的组(全部 skip)→ 该次运行「降级」,不得裸报成功 */
66
+ export function degradedGroups(groups) {
67
+ return Object.entries(groups).filter(([, v]) => v.total > 0 && v.ran === 0).map(([k]) => k)
68
+ }
69
+
70
+ // 最坏状态排序:error/fail > warn > skip > pass
71
+ export function verdict(results, { degraded = [] } = {}) {
72
+ let worst = 'pass'
73
+ for (const r of results) {
74
+ if (r.status === 'error' && worst !== 'error') worst = 'error'
75
+ else if (r.status === 'fail' && worst !== 'error') worst = 'fail'
76
+ else if (r.status === 'warn' && worst === 'pass') worst = 'warn'
77
+ else if (r.status === 'skip' && worst === 'pass') worst = 'skip'
78
+ }
79
+ const criticalFail = results.some((r) => r.critical && (r.status === 'fail' || r.status === 'error'))
80
+ // 退出码契约:0 = 无 fail/error(可含 warn/skip);1 = 存在 fail/error
81
+ return { worst, criticalFail, ok: worst !== 'fail' && worst !== 'error', degraded }
82
+ }
83
+
84
+ const ICONS = { pass: '[PASS]', warn: '[WARN]', fail: '[FAIL]', error: '[ERROR]', skip: '[SKIP]' }
85
+
86
+ export function render(results) {
87
+ const lines = []
88
+ let lastGroup = null
89
+ for (const r of results) {
90
+ if (r.group !== lastGroup) {
91
+ lines.push('', `== ${r.group} ==`)
92
+ lastGroup = r.group
93
+ }
94
+ const tag = r.critical ? ' [关键]' : ''
95
+ lines.push(`${ICONS[r.status] ?? '[?]'} ${r.name}${tag}`)
96
+ const msg = String(r.message ?? '').trim()
97
+ if (msg) {
98
+ for (const line of msg.split('\n')) lines.push(` ${line}`)
99
+ }
100
+ if (r.evidence) lines.push(` 证据: ${r.evidence}`)
101
+ }
102
+ const v = verdict(results)
103
+ const counts = {}
104
+ for (const r of results) counts[r.status] = (counts[r.status] ?? 0) + 1
105
+ lines.push(
106
+ '',
107
+ `=== 汇总: ${Object.entries(counts).map(([k, n]) => `${k}=${n}`).join(' ')} | 总判定: ${v.worst.toUpperCase()}${v.criticalFail ? '(含关键项失败)' : ''} ===`,
108
+ )
109
+ return lines.join('\n')
110
+ }
package/lib/util.mjs CHANGED
@@ -1,125 +1,125 @@
1
- // 工具:临时沙箱 + 子进程执行(输出落盘,规避管道捕获限制)
2
- import { spawnSync } from 'node:child_process'
3
- import {
4
- mkdtempSync, mkdirSync, renameSync, 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
- // 前缀用 `doctor-` 而非 `dsh-`:宿主运行目录的保护模板是 %TEMP%\dsh-*(红线 1),
11
- // 用 `dsh-doctor-*` 会与该模板相撞;改名后本工具的自建临时目录不再落在保护模板内。
12
- const SANDBOX_PREFIX = 'doctor-sbx-'
13
- const QUARANTINE_PREFIX = 'doctor-quarantine-'
14
-
15
- export function makeSandbox(label) {
16
- const root = mkdtempSync(path.join(tmpdir(), `${SANDBOX_PREFIX}${label}-`))
17
- const home = path.join(root, 'home')
18
- const logs = path.join(root, 'logs')
19
- mkdirSync(home, { recursive: true })
20
- mkdirSync(logs, { recursive: true })
21
- return { root, home, logs }
22
- }
23
-
24
- // 清理改为「隔离不删除」(红线 4 三段式的前两段:dry-run 打印 → rename 到隔离目录)。
25
- // 返回隔离后的绝对路径;调用方必须打印它,人工确认后再自行清理。
26
- // 本工具不再有任何 rmSync/rm -rf 路径 —— 旧的 cleanSandbox 已移除。
27
- export function quarantineSandbox(sb) {
28
- if (!sb || !sb.root || !existsSync(sb.root)) return null
29
- const dest = path.join(tmpdir(), `${QUARANTINE_PREFIX}${Date.now()}-${path.basename(sb.root)}`)
30
- try {
31
- renameSync(sb.root, dest)
32
- return dest
33
- } catch {
34
- return null
35
- }
36
- }
37
-
38
- // 把运行期绝对路径替换为占位符,使 message 可对外复用(R-fix 0C / L1)。
39
- // 只处理"本次运行自己注入的路径",不做通用脱敏。
40
- export function redact(text, paths = []) {
41
- let s = String(text ?? '')
42
- const variants = []
43
- for (const p of paths) {
44
- if (!p || typeof p !== 'string') continue
45
- variants.push(p, p.replace(/\\/g, '/'), p.replace(/\//g, '\\'))
46
- }
47
- const tmp = tmpdir()
48
- variants.push(tmp, tmp.replace(/\\/g, '/'), tmp.replace(/\//g, '\\'))
49
- for (const v of [...new Set(variants)].sort((a, b) => b.length - a.length)) {
50
- if (v.length < 4) continue
51
- s = s.split(v).join('<path>')
52
- }
53
- return s
54
- }
55
-
56
- // 执行命令:stdout/stderr 分别写入日志文件(不建管道),返回退出码与全文
57
- export function runStep(label, cmd, args, { env = {}, timeout = 120_000, cwd, logDir, shell } = {}) {
58
- const dir = logDir ?? process.cwd()
59
- const outPath = path.join(dir, `${label}.out.log`)
60
- const errPath = path.join(dir, `${label}.err.log`)
61
- const fdOut = openSync(outPath, 'w')
62
- const fdErr = openSync(errPath, 'w')
63
- const res = spawnSync(cmd, args, {
64
- cwd,
65
- env: { ...process.env, ...env },
66
- stdio: ['ignore', fdOut, fdErr],
67
- timeout,
68
- windowsHide: true,
69
- // Windows 下解析 npm.cmd/pnpm.cmd 等 shim 需要 shell;直接调用 node.exe 等真实可执行文件时禁用
70
- shell: shell ?? process.platform === 'win32',
71
- })
72
- closeSync(fdOut)
73
- closeSync(fdErr)
74
- const read = (p) => { try { return readFileSync(p, 'utf8') } catch { return '' } }
75
- return {
76
- code: res.status,
77
- signal: res.signal,
78
- spawnError: res.error ? String(res.error) : null,
79
- out: read(outPath),
80
- err: read(errPath),
81
- outPath,
82
- errPath,
83
- ok: res.status === 0 && !res.error,
84
- }
85
- }
86
-
87
- export function tail(text, n = 8) {
88
- if (!text) return '(无输出)'
89
- const lines = text.trim().split('\n')
90
- return lines.slice(-n).join('\n')
91
- }
92
-
93
- export const pass = (message) => ({ status: 'pass', message })
94
- export const fail = (message) => ({ status: 'fail', message })
95
- export const warn = (message) => ({ status: 'warn', message })
96
- export const skip = (reason) => ({ status: 'skip', message: reason })
97
- /** 显式不可判:与 skip 同判定,但带 category 以便上游区分「环境不适用」与「确实没跑」 */
98
- export const na = (reason, category = 'not-applicable') => ({ status: 'skip', message: reason, category })
99
- /** 环境类降级:不计 pass、不计插件缺陷,走 category=environment */
100
- export const envskip = (reason) => ({ status: 'skip', message: reason, category: 'environment' })
101
-
102
- export function readJson(p) {
103
- return JSON.parse(readFileSync(p, 'utf8'))
104
- }
105
-
106
- export function exists(p) {
107
- return existsSync(p)
108
- }
109
-
110
- export function writeJson(p, obj) {
111
- writeFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8')
112
- }
113
-
114
- // 递归收集目录下匹配文件(跳过 node_modules/lib/dist 等构建产物)
115
- export function findFiles(dir, sub, re, out = []) {
116
- const base = path.join(dir, sub)
117
- if (!existsSync(base)) return out
118
- for (const entry of readdirSync(base, { withFileTypes: true })) {
119
- if (['node_modules', 'lib', 'dist', '.git'].includes(entry.name)) continue
120
- const p = path.join(base, entry.name)
121
- if (entry.isDirectory()) findFiles(dir, path.join(sub, entry.name), re, out)
122
- else if (re.test(entry.name)) out.push(p)
123
- }
124
- return out
125
- }
1
+ // 工具:临时沙箱 + 子进程执行(输出落盘,规避管道捕获限制)
2
+ import { spawnSync } from 'node:child_process'
3
+ import {
4
+ mkdtempSync, mkdirSync, renameSync, 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
+ // 前缀用 `doctor-` 而非 `dsh-`:宿主运行目录的保护模板是 %TEMP%\dsh-*(红线 1),
11
+ // 用 `dsh-doctor-*` 会与该模板相撞;改名后本工具的自建临时目录不再落在保护模板内。
12
+ const SANDBOX_PREFIX = 'doctor-sbx-'
13
+ const QUARANTINE_PREFIX = 'doctor-quarantine-'
14
+
15
+ export function makeSandbox(label) {
16
+ const root = mkdtempSync(path.join(tmpdir(), `${SANDBOX_PREFIX}${label}-`))
17
+ const home = path.join(root, 'home')
18
+ const logs = path.join(root, 'logs')
19
+ mkdirSync(home, { recursive: true })
20
+ mkdirSync(logs, { recursive: true })
21
+ return { root, home, logs }
22
+ }
23
+
24
+ // 清理改为「隔离不删除」(红线 4 三段式的前两段:dry-run 打印 → rename 到隔离目录)。
25
+ // 返回隔离后的绝对路径;调用方必须打印它,人工确认后再自行清理。
26
+ // 本工具不再有任何 rmSync/rm -rf 路径 —— 旧的 cleanSandbox 已移除。
27
+ export function quarantineSandbox(sb) {
28
+ if (!sb || !sb.root || !existsSync(sb.root)) return null
29
+ const dest = path.join(tmpdir(), `${QUARANTINE_PREFIX}${Date.now()}-${path.basename(sb.root)}`)
30
+ try {
31
+ renameSync(sb.root, dest)
32
+ return dest
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+
38
+ // 把运行期绝对路径替换为占位符,使 message 可对外复用(R-fix 0C / L1)。
39
+ // 只处理"本次运行自己注入的路径",不做通用脱敏。
40
+ export function redact(text, paths = []) {
41
+ let s = String(text ?? '')
42
+ const variants = []
43
+ for (const p of paths) {
44
+ if (!p || typeof p !== 'string') continue
45
+ variants.push(p, p.replace(/\\/g, '/'), p.replace(/\//g, '\\'))
46
+ }
47
+ const tmp = tmpdir()
48
+ variants.push(tmp, tmp.replace(/\\/g, '/'), tmp.replace(/\//g, '\\'))
49
+ for (const v of [...new Set(variants)].sort((a, b) => b.length - a.length)) {
50
+ if (v.length < 4) continue
51
+ s = s.split(v).join('<path>')
52
+ }
53
+ return s
54
+ }
55
+
56
+ // 执行命令:stdout/stderr 分别写入日志文件(不建管道),返回退出码与全文
57
+ export function runStep(label, cmd, args, { env = {}, timeout = 120_000, cwd, logDir, shell } = {}) {
58
+ const dir = logDir ?? process.cwd()
59
+ const outPath = path.join(dir, `${label}.out.log`)
60
+ const errPath = path.join(dir, `${label}.err.log`)
61
+ const fdOut = openSync(outPath, 'w')
62
+ const fdErr = openSync(errPath, 'w')
63
+ const res = spawnSync(cmd, args, {
64
+ cwd,
65
+ env: { ...process.env, ...env },
66
+ stdio: ['ignore', fdOut, fdErr],
67
+ timeout,
68
+ windowsHide: true,
69
+ // Windows 下解析 npm.cmd/pnpm.cmd 等 shim 需要 shell;直接调用 node.exe 等真实可执行文件时禁用
70
+ shell: shell ?? process.platform === 'win32',
71
+ })
72
+ closeSync(fdOut)
73
+ closeSync(fdErr)
74
+ const read = (p) => { try { return readFileSync(p, 'utf8') } catch { return '' } }
75
+ return {
76
+ code: res.status,
77
+ signal: res.signal,
78
+ spawnError: res.error ? String(res.error) : null,
79
+ out: read(outPath),
80
+ err: read(errPath),
81
+ outPath,
82
+ errPath,
83
+ ok: res.status === 0 && !res.error,
84
+ }
85
+ }
86
+
87
+ export function tail(text, n = 8) {
88
+ if (!text) return '(无输出)'
89
+ const lines = text.trim().split('\n')
90
+ return lines.slice(-n).join('\n')
91
+ }
92
+
93
+ export const pass = (message) => ({ status: 'pass', message })
94
+ export const fail = (message) => ({ status: 'fail', message })
95
+ export const warn = (message) => ({ status: 'warn', message })
96
+ export const skip = (reason) => ({ status: 'skip', message: reason })
97
+ /** 显式不可判:与 skip 同判定,但带 category 以便上游区分「环境不适用」与「确实没跑」 */
98
+ export const na = (reason, category = 'not-applicable') => ({ status: 'skip', message: reason, category })
99
+ /** 环境类降级:不计 pass、不计插件缺陷,走 category=environment */
100
+ export const envskip = (reason) => ({ status: 'skip', message: reason, category: 'environment' })
101
+
102
+ export function readJson(p) {
103
+ return JSON.parse(readFileSync(p, 'utf8'))
104
+ }
105
+
106
+ export function exists(p) {
107
+ return existsSync(p)
108
+ }
109
+
110
+ export function writeJson(p, obj) {
111
+ writeFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8')
112
+ }
113
+
114
+ // 递归收集目录下匹配文件(跳过 node_modules/lib/dist 等构建产物)
115
+ export function findFiles(dir, sub, re, out = []) {
116
+ const base = path.join(dir, sub)
117
+ if (!existsSync(base)) return out
118
+ for (const entry of readdirSync(base, { withFileTypes: true })) {
119
+ if (['node_modules', 'lib', 'dist', '.git'].includes(entry.name)) continue
120
+ const p = path.join(base, entry.name)
121
+ if (entry.isDirectory()) findFiles(dir, path.join(sub, entry.name), re, out)
122
+ else if (re.test(entry.name)) out.push(p)
123
+ }
124
+ return out
125
+ }
package/package.json CHANGED
@@ -1,50 +1,61 @@
1
- {
2
- "name": "@perrylink/dsh-plugin-doctor",
3
- "version": "0.2.2",
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
- "CHANGELOG.md",
15
- "SURVEY.md"
16
- ],
17
- "engines": {
18
- "node": "^22.19.0 || >=24.0.0"
19
- },
20
- "keywords": [
21
- "dsh",
22
- "dsh-plugin",
23
- "deepseek-harness",
24
- "cordis",
25
- "plugin",
26
- "doctor",
27
- "lint",
28
- "smoke-test",
29
- "verification"
30
- ],
31
- "license": "Apache-2.0",
32
- "repository": {
33
- "type": "git",
34
- "url": "git+https://github.com/PerryLink/dsh-plugin-doctor.git"
35
- },
36
- "homepage": "https://www.npmjs.com/package/@perrylink/dsh-plugin-doctor",
37
- "bugs": {
38
- "url": "https://github.com/PerryLink/dsh-plugin-doctor/issues"
39
- },
40
- "funding": {
41
- "type": "github",
42
- "url": "https://github.com/sponsors/PerryLink"
43
- },
44
- "scripts": {
45
- "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 && node --check scripts/badge.mjs && node --check scripts/verify.mjs && node --check tests/selftest.mjs && node --check tests/contract.mjs && node tests/selftest.mjs && node tests/contract.mjs",
46
- "prepublishOnly": "npm test",
47
- "verify:registry": "node scripts/verify.mjs",
48
- "badge": "node scripts/badge.mjs"
49
- }
50
- }
1
+ {
2
+ "name": "@perrylink/dsh-plugin-doctor",
3
+ "version": "0.2.3",
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": "./plugin.mjs",
7
+ "exports": {
8
+ ".": "./plugin.mjs",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "bin": {
12
+ "dsh-plugin-doctor": "./doctor.mjs"
13
+ },
14
+ "files": [
15
+ "doctor.mjs",
16
+ "plugin.mjs",
17
+ "lib/",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "CHANGELOG.md",
21
+ "SURVEY.md"
22
+ ],
23
+ "engines": {
24
+ "node": "^22.19.0 || >=24.0.0"
25
+ },
26
+ "keywords": [
27
+ "dsh",
28
+ "dsh-plugin",
29
+ "deepseek-harness",
30
+ "cordis",
31
+ "plugin",
32
+ "doctor",
33
+ "lint",
34
+ "smoke-test",
35
+ "verification"
36
+ ],
37
+ "license": "Apache-2.0",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/PerryLink/dsh-plugin-doctor.git"
41
+ },
42
+ "homepage": "https://www.npmjs.com/package/@perrylink/dsh-plugin-doctor",
43
+ "bugs": {
44
+ "url": "https://github.com/PerryLink/dsh-plugin-doctor/issues"
45
+ },
46
+ "funding": {
47
+ "type": "github",
48
+ "url": "https://github.com/sponsors/PerryLink"
49
+ },
50
+ "scripts": {
51
+ "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 && node --check scripts/badge.mjs && node --check scripts/verify.mjs && node --check tests/selftest.mjs && node --check tests/contract.mjs && node tests/selftest.mjs && node tests/contract.mjs",
52
+ "prepublishOnly": "npm test",
53
+ "verify:registry": "node scripts/verify.mjs",
54
+ "badge": "node scripts/badge.mjs"
55
+ },
56
+ "dsh": {
57
+ "bundle": {
58
+ "patch": "./cordis.patch.yml"
59
+ }
60
+ }
61
+ }