@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/CHANGELOG.md +69 -69
- package/LICENSE +201 -201
- package/README.md +266 -243
- package/SURVEY.md +265 -265
- package/cordis.patch.yml +8 -0
- package/doctor.mjs +255 -255
- package/lib/checks-collections.mjs +119 -119
- package/lib/checks-cordis.mjs +230 -230
- package/lib/checks-package.mjs +188 -188
- package/lib/checks-smoke.mjs +117 -117
- package/lib/framework.mjs +110 -110
- package/lib/util.mjs +125 -125
- package/package.json +61 -50
- package/plugin.mjs +119 -0
package/plugin.mjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// plugin.mjs —— dsh-plugin-doctor 的 DSH bundle 宿主半区
|
|
2
|
+
//
|
|
3
|
+
// 通过 cordis.patch.yml 行加载(package.json 的 `dsh.bundle.patch`):
|
|
4
|
+
//
|
|
5
|
+
// dsh plugin --profile web add @perrylink/dsh-plugin-doctor
|
|
6
|
+
//
|
|
7
|
+
// 注册一个只读的 plugin_doctor 工具与 /doctor 命令,1:1 复用 doctor.mjs
|
|
8
|
+
// 的静态检查层(R 包结构 / K cordis 契约 / CC 集合站清单;D 沙箱冒烟为
|
|
9
|
+
// 工具参数显式开启)。CLI(bin: doctor.mjs)不受影响。
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { readFileSync, existsSync, mkdtempSync, mkdirSync } from 'node:fs'
|
|
12
|
+
import { tmpdir } from 'node:os'
|
|
13
|
+
import { Doctor, render, verdict, summarizeGroups, degradedGroups } from './lib/framework.mjs'
|
|
14
|
+
import { addChecks as addPackageChecks } from './lib/checks-package.mjs'
|
|
15
|
+
import { addChecks as addCordisChecks } from './lib/checks-cordis.mjs'
|
|
16
|
+
import { addSmokeChecks } from './lib/checks-smoke.mjs'
|
|
17
|
+
import { addChecks as addCollectionChecks } from './lib/checks-collections.mjs'
|
|
18
|
+
import { quarantineSandbox, redact } from './lib/util.mjs'
|
|
19
|
+
|
|
20
|
+
export const name = '@perrylink/dsh-plugin-doctor'
|
|
21
|
+
|
|
22
|
+
/** 静态 R/K/CC 组(默认)+ 可选 D 组冒烟,返回脱敏后的结构化报告。 */
|
|
23
|
+
async function runDoctor(repoPath, { smoke = false } = {}) {
|
|
24
|
+
const abs = path.resolve(repoPath)
|
|
25
|
+
const pkgPath = path.join(abs, 'package.json')
|
|
26
|
+
if (!existsSync(pkgPath)) return { ok: false, error: `未找到 ${pkgPath}` }
|
|
27
|
+
let pkg
|
|
28
|
+
try {
|
|
29
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return { ok: false, error: `package.json 解析失败: ${e.message}` }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const runRoot = mkdtempSync(path.join(tmpdir(), 'doctor-run-'))
|
|
35
|
+
const logDir = path.join(runRoot, 'logs')
|
|
36
|
+
mkdirSync(logDir, { recursive: true })
|
|
37
|
+
const ctx = {
|
|
38
|
+
repoPath: abs,
|
|
39
|
+
pkg,
|
|
40
|
+
pkgName: pkg.name,
|
|
41
|
+
logDir,
|
|
42
|
+
workspaceRoot: path.resolve(import.meta.dirname, '..'),
|
|
43
|
+
sandboxRoots: [runRoot],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const doctor = new Doctor()
|
|
47
|
+
addPackageChecks(doctor, ctx)
|
|
48
|
+
addCordisChecks(doctor, ctx)
|
|
49
|
+
addCollectionChecks(doctor, ctx)
|
|
50
|
+
if (smoke) addSmokeChecks(doctor, ctx, { dshVersion: '0.1.2-rc.1' })
|
|
51
|
+
|
|
52
|
+
const started = Date.now()
|
|
53
|
+
const results = await doctor.run(ctx)
|
|
54
|
+
const redactPaths = [abs, logDir, runRoot, ...(ctx.sandboxRoots ?? [])]
|
|
55
|
+
for (const r of results) {
|
|
56
|
+
if (typeof r.message === 'string') r.message = redact(r.message, redactPaths)
|
|
57
|
+
if (typeof r.evidence === 'string') r.evidence = redact(r.evidence, redactPaths)
|
|
58
|
+
}
|
|
59
|
+
const groups = summarizeGroups(results)
|
|
60
|
+
const v = verdict(results, { degraded: degradedGroups(groups) })
|
|
61
|
+
return {
|
|
62
|
+
ok: v.ok,
|
|
63
|
+
verdict: v,
|
|
64
|
+
report: render(results),
|
|
65
|
+
durationMs: Date.now() - started,
|
|
66
|
+
quarantine: quarantineSandbox({ root: runRoot }),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function apply(ctx) {
|
|
71
|
+
const tools = ctx.get('tools')
|
|
72
|
+
if (tools) {
|
|
73
|
+
ctx.effect(() => tools.register({
|
|
74
|
+
name: 'plugin_doctor',
|
|
75
|
+
description: '只读静态检查一个 DSH 插件仓库(R 包结构 / K cordis 契约 / CC 集合站清单;可选 D 沙箱冒烟)',
|
|
76
|
+
parameters: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
repo: { type: 'string', description: '被检插件仓库路径(含 package.json)' },
|
|
80
|
+
smoke: { type: 'boolean', description: '是否额外执行 D 组沙箱冒烟(默认 false;需网络 + pnpm)' },
|
|
81
|
+
},
|
|
82
|
+
required: ['repo'],
|
|
83
|
+
},
|
|
84
|
+
output: {
|
|
85
|
+
schema: { type: 'string' },
|
|
86
|
+
render(args, value) {
|
|
87
|
+
return [{ type: 'text', text: String(value) }]
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
async execute(args) {
|
|
91
|
+
const r = await runDoctor(args.repo, { smoke: !!args.smoke })
|
|
92
|
+
if (!r.ok && r.error) return `检查失败: ${r.error}`
|
|
93
|
+
return r.report
|
|
94
|
+
},
|
|
95
|
+
}))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const commands = ctx.get('commands')
|
|
99
|
+
if (commands) {
|
|
100
|
+
ctx.effect(() => commands.register({
|
|
101
|
+
name: 'doctor',
|
|
102
|
+
description: '对插件仓运行 dsh-plugin-doctor 静态检查(R/K/CC 组)',
|
|
103
|
+
input: { hint: '<插件仓路径>' },
|
|
104
|
+
handler: async (invocation) => {
|
|
105
|
+
const repo = (invocation.rawInput ?? '').trim()
|
|
106
|
+
if (!repo) return { kind: 'error', text: '用法: /doctor <插件仓路径>' }
|
|
107
|
+
try {
|
|
108
|
+
const r = await runDoctor(repo)
|
|
109
|
+
if (!r.ok && r.error) return { kind: 'error', text: `检查失败: ${r.error}` }
|
|
110
|
+
return { kind: 'success', text: r.report }
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return { kind: 'error', text: `doctor 运行失败: ${err.message}` }
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
}))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default { name, apply }
|