@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.
@@ -1,119 +1,119 @@
1
- // 生态·集合站清单校验(CC1–CC5)
2
- // 依据:工作区存量盘点(2026-09-07)——dsh-plugin-certification spec v1 注册表、
3
- // adp-list 收录条目 schema、dsh-catalog 市场目录、omdsh Workshop v2、dsh-plugin-kit 三门。
4
- import path from 'node:path'
5
- import { readFileSync, existsSync, readdirSync } from 'node:fs'
6
- import { runStep, pass, fail, warn, skip, na, tail, readJson, findFiles } from './util.mjs'
7
-
8
- export const GROUP = '生态·集合站清单'
9
-
10
- const CAT_IDS = ['agi', 'ui', 'usage', 'theme', 'model', 'identity', 'session', 'memory', 'tools', 'wsl', 'browser', 'vision', 'voice', 'docs', 'skill', 'workflow', 'git', 'notify', 'dev', 'security', 'remote', 'market', 'fun']
11
- const OMSDSH_ACTIVATION = ['immediate', 'hot-reload', 'restart-plugin', 'restart-profile', 'restart-host']
12
-
13
- function githubSlug(pkg) {
14
- const repo = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url ?? ''
15
- const m = String(repo).match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
16
- return m ? { owner: m[1], repo: m[2], slug: `${m[1]}__${m[2]}` } : null
17
- }
18
-
19
- export function addChecks(doctor, ctx) {
20
- const ws = ctx.workspaceRoot
21
- const { pkg, pkgName, repoPath } = ctx
22
- const g = githubSlug(pkg)
23
-
24
- doctor.add(GROUP, 'CC1 认证注册表(dsh-plugin-certification spec v1)', () => {
25
- const p = path.join(ws, 'dsh-plugin-certification', 'data', 'certified.json')
26
- if (!existsSync(p)) return skip('认证仓本地不存在(--workspace 外运行无法核对)')
27
- if (!g) return skip('无法从 repository 字段解析 GitHub owner/repo')
28
- const data = readJson(p)
29
- const hit = (data.entries ?? []).find((e) => String(e.repo).toLowerCase() === `${g.owner}/${g.repo}`.toLowerCase())
30
- if (!hit) return skip('未收录于认证注册表(可选渠道)')
31
- const problems = []
32
- if (!hit.grade) problems.push('缺 grade')
33
- if (!hit.snapshot) problems.push('缺 snapshot')
34
- for (const d of ['manifest', 'buildHygiene', 'supplyChain', 'releaseIntegrity', 'installSmoke']) {
35
- const dim = hit.dimensions?.[d]
36
- if (!dim || !dim.evidence) problems.push(`维度 ${d} 缺 evidence`)
37
- if (dim && d === 'installSmoke' && dim.result === 'install-fail') problems.push('installSmoke=install-fail(认证硬门失败级)')
38
- }
39
- if (hit.veto) problems.push(`存在 veto: ${JSON.stringify(hit.veto)}`)
40
- if (problems.length) return warn(`已收录(grade ${hit.grade ?? '?'})但有缺口:\n${problems.join('\n')}`)
41
- const smoke = hit.dimensions.installSmoke.result
42
- return pass(`grade ${hit.grade} · snapshot ${hit.snapshot} · installSmoke=${smoke} · 五维 evidence 齐全、无 veto`)
43
- })
44
-
45
- doctor.add(GROUP, 'CC2 收录条目(adp-list awesome-dsh-plugin)', () => {
46
- if (!g) return skip('无法解析 GitHub slug')
47
- const dir = path.join(ws, 'adp-list', 'data', 'plugins')
48
- if (!existsSync(dir)) return skip('adp-list 本地不存在')
49
- const hits = readdirSync(dir).filter((f) => f === `${g.slug}.yml` || f.startsWith(`${g.slug}--`))
50
- if (!hits.length) return skip('未收录于 adp-list(可选渠道)')
51
- const problems = []
52
- for (const f of hits) {
53
- const text = readFileSync(path.join(dir, f), 'utf8')
54
- const kv = {}
55
- // 注意:本环境 `$` 不匹配行尾孤立 \r 之前,行必须按 /\r?\n/ 切分并先剥 \r
56
- for (const line0 of text.split(/\r?\n/)) {
57
- const line = line0.replace(/\r$/, '')
58
- const m = line.match(/^\s*([A-Za-z0-9_.-]+):\s*([\s\S]*)$/)
59
- if (m) kv[m[1]] = m[2].trim()
60
- }
61
- for (const k of ['url', 'name', 'category']) if (!(k in kv)) problems.push(`${f}: 缺 ${k}`)
62
- if (kv.category && !CAT_IDS.includes(kv.category)) problems.push(`${f}: category=${kv.category} 不在 23 值枚举`)
63
- if (kv.url && !kv.url.includes(`${g.owner}/${g.repo}`)) problems.push(`${f}: url 与 repository 不符(${kv.url})`)
64
- if (!/description:/.test(text) || !/^\s*en:\s*\S+/m.test(text)) problems.push(`${f}: description.en 必填单行`)
65
- if (kv.tarball) {
66
- if (!/^https:\/\/(github\.com|objects\.githubusercontent\.com|release-assets\.githubusercontent\.com)\/.*\/releases\/.+\.tgz$/.test(kv.tarball)) problems.push(`${f}: tarball 必须 GitHub Release 托管 https .tgz`)
67
- }
68
- }
69
- if (problems.length) return fail(problems.join('\n'))
70
- return pass(`已收录 ${hits.length} 条(${hits.join(', ')}),字段/枚举/描述校验通过`)
71
- })
72
-
73
- doctor.add(GROUP, 'CC3 市场目录条目(dsh-catalog)', () => {
74
- const p = path.join(ws, 'dsh-catalog', 'data', 'packages.json')
75
- if (!existsSync(p)) return skip('dsh-catalog 本地不存在')
76
- const list = readJson(p)
77
- const hit = (Array.isArray(list) ? list : []).find((e) => e.npm === pkgName)
78
- if (!hit) return skip('未收录于 DSH Desktop Market 目录(可选渠道)')
79
- const problems = []
80
- for (const k of ['npm', 'repo', 'displayName', 'categories', 'summary']) if (!(k in hit)) problems.push(`缺 ${k}`)
81
- if (hit.repo && g && !String(hit.repo).includes(`${g.owner}/${g.repo}`)) problems.push('repo 字段与 repository 不符')
82
- if (!Array.isArray(hit.categories) || !hit.categories.length) problems.push('categories 必须为非空数组')
83
- if (typeof hit.summary === 'string' && hit.summary.length > 1000) problems.push('summary 超 v1 schema 1000 字符上限')
84
- if (typeof hit.summary === 'string' && /dsh\s+plugin\s+--profile|pnpm\s+add|npm\s+i(nstall)?\s/.test(hit.summary)) problems.push('summary 含安装命令文本(v1 schema 禁止)')
85
- if (typeof hit.summary === 'string' && !/[.!?)—–"」』]$/.test(hit.summary.trim())) problems.push('summary 疑被截断(未以句读符结尾)')
86
- if (problems.length) return fail(problems.join('\n'))
87
- return pass(`目录第 ${list.indexOf(hit) + 1} 条(displayName: ${hit.displayName})`)
88
- })
89
-
90
- doctor.add(GROUP, 'CC4 omdsh Workshop 清单(dshWorkshop)', () => {
91
- const w = pkg.dshWorkshop
92
- if (!w) return skip('未声明 dshWorkshop(omdsh 可选渠道;harness 不读此字段,激活只看 dsh.bundle)')
93
- const problems = []
94
- const act = w.lifecycle?.activation
95
- if (!act) problems.push('缺 lifecycle.activation')
96
- else if (!OMSDSH_ACTIVATION.includes(act)) problems.push(`activation=${act} 不在 5 值枚举(build-omdsh-submission.mjs 会 throw)`)
97
- if (problems.length) return fail(problems.join('\n'))
98
- return pass(`activation=${act} · restartRequired=${/^restart-/.test(act)}(hub intake 推导规则一致)`)
99
- })
100
-
101
- doctor.add(GROUP, 'CC5 插件三门(license/五语 README/seam 三角色)', () => {
102
- // R-fix 2:这三门(Apache-2.0 + 五语 README + seam 三角色 marker)是 PerryLink 家族私有标准,
103
- // 不是生态标准。旧实现在没有家族 workspace 时改跑「本地降级检查」并把它施加到目标仓上 →
104
- // 任何外部作者跑 --only CC 都会被判 fail(不是环境假红,是真判据),且该 fail 会被误读为工具故障。
105
- // 现在:家族私有标准只在家族工作区内核对(kit CLI 存在),其余显式 not-applicable。
106
- const kitCli = path.join(ws, 'dsh-plugin-kit', 'lib', 'verify', 'cli.js')
107
- if (!existsSync(kitCli)) {
108
- return na('家族私有标准(Apache-2.0 / 五语 README / seam 三角色)不适用于非家族仓;'
109
- + '如需核对请在家族工作区内运行(--workspace 指向含 dsh-plugin-kit 的目录)')
110
- }
111
- const r = runStep('cc5-kit', process.execPath, [kitCli, 'all', repoPath], { cwd: ws, logDir: ctx.logDir, timeout: 120_000, shell: false })
112
- if (r.ok) return pass('dsh-plugin-kit 三门 CLI 全过')
113
- const out = `${r.out}\n${r.err}`
114
- const note = /no source files found under src/.test(out)
115
- ? '\n注:该仓无 src/ 目录,kit verify-seam 结构不适用(kit 已支持根层 JS 回退,此处以 kit 输出为准)' : ''
116
- if (r.code === 1) return fail(`dsh-plugin-kit 三门失败:\n${tail(out, 12)}${note}`)
117
- return fail(`dsh-plugin-kit 三门执行异常(exit ${r.code}):\n${tail(r.err)}${note}`)
118
- })
119
- }
1
+ // 生态·集合站清单校验(CC1–CC5)
2
+ // 依据:工作区存量盘点(2026-09-07)——dsh-plugin-certification spec v1 注册表、
3
+ // adp-list 收录条目 schema、dsh-catalog 市场目录、omdsh Workshop v2、dsh-plugin-kit 三门。
4
+ import path from 'node:path'
5
+ import { readFileSync, existsSync, readdirSync } from 'node:fs'
6
+ import { runStep, pass, fail, warn, skip, na, tail, readJson, findFiles } from './util.mjs'
7
+
8
+ export const GROUP = '生态·集合站清单'
9
+
10
+ const CAT_IDS = ['agi', 'ui', 'usage', 'theme', 'model', 'identity', 'session', 'memory', 'tools', 'wsl', 'browser', 'vision', 'voice', 'docs', 'skill', 'workflow', 'git', 'notify', 'dev', 'security', 'remote', 'market', 'fun']
11
+ const OMSDSH_ACTIVATION = ['immediate', 'hot-reload', 'restart-plugin', 'restart-profile', 'restart-host']
12
+
13
+ function githubSlug(pkg) {
14
+ const repo = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url ?? ''
15
+ const m = String(repo).match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
16
+ return m ? { owner: m[1], repo: m[2], slug: `${m[1]}__${m[2]}` } : null
17
+ }
18
+
19
+ export function addChecks(doctor, ctx) {
20
+ const ws = ctx.workspaceRoot
21
+ const { pkg, pkgName, repoPath } = ctx
22
+ const g = githubSlug(pkg)
23
+
24
+ doctor.add(GROUP, 'CC1 认证注册表(dsh-plugin-certification spec v1)', () => {
25
+ const p = path.join(ws, 'dsh-plugin-certification', 'data', 'certified.json')
26
+ if (!existsSync(p)) return skip('认证仓本地不存在(--workspace 外运行无法核对)')
27
+ if (!g) return skip('无法从 repository 字段解析 GitHub owner/repo')
28
+ const data = readJson(p)
29
+ const hit = (data.entries ?? []).find((e) => String(e.repo).toLowerCase() === `${g.owner}/${g.repo}`.toLowerCase())
30
+ if (!hit) return skip('未收录于认证注册表(可选渠道)')
31
+ const problems = []
32
+ if (!hit.grade) problems.push('缺 grade')
33
+ if (!hit.snapshot) problems.push('缺 snapshot')
34
+ for (const d of ['manifest', 'buildHygiene', 'supplyChain', 'releaseIntegrity', 'installSmoke']) {
35
+ const dim = hit.dimensions?.[d]
36
+ if (!dim || !dim.evidence) problems.push(`维度 ${d} 缺 evidence`)
37
+ if (dim && d === 'installSmoke' && dim.result === 'install-fail') problems.push('installSmoke=install-fail(认证硬门失败级)')
38
+ }
39
+ if (hit.veto) problems.push(`存在 veto: ${JSON.stringify(hit.veto)}`)
40
+ if (problems.length) return warn(`已收录(grade ${hit.grade ?? '?'})但有缺口:\n${problems.join('\n')}`)
41
+ const smoke = hit.dimensions.installSmoke.result
42
+ return pass(`grade ${hit.grade} · snapshot ${hit.snapshot} · installSmoke=${smoke} · 五维 evidence 齐全、无 veto`)
43
+ })
44
+
45
+ doctor.add(GROUP, 'CC2 收录条目(adp-list awesome-dsh-plugin)', () => {
46
+ if (!g) return skip('无法解析 GitHub slug')
47
+ const dir = path.join(ws, 'adp-list', 'data', 'plugins')
48
+ if (!existsSync(dir)) return skip('adp-list 本地不存在')
49
+ const hits = readdirSync(dir).filter((f) => f === `${g.slug}.yml` || f.startsWith(`${g.slug}--`))
50
+ if (!hits.length) return skip('未收录于 adp-list(可选渠道)')
51
+ const problems = []
52
+ for (const f of hits) {
53
+ const text = readFileSync(path.join(dir, f), 'utf8')
54
+ const kv = {}
55
+ // 注意:本环境 `$` 不匹配行尾孤立 \r 之前,行必须按 /\r?\n/ 切分并先剥 \r
56
+ for (const line0 of text.split(/\r?\n/)) {
57
+ const line = line0.replace(/\r$/, '')
58
+ const m = line.match(/^\s*([A-Za-z0-9_.-]+):\s*([\s\S]*)$/)
59
+ if (m) kv[m[1]] = m[2].trim()
60
+ }
61
+ for (const k of ['url', 'name', 'category']) if (!(k in kv)) problems.push(`${f}: 缺 ${k}`)
62
+ if (kv.category && !CAT_IDS.includes(kv.category)) problems.push(`${f}: category=${kv.category} 不在 23 值枚举`)
63
+ if (kv.url && !kv.url.includes(`${g.owner}/${g.repo}`)) problems.push(`${f}: url 与 repository 不符(${kv.url})`)
64
+ if (!/description:/.test(text) || !/^\s*en:\s*\S+/m.test(text)) problems.push(`${f}: description.en 必填单行`)
65
+ if (kv.tarball) {
66
+ if (!/^https:\/\/(github\.com|objects\.githubusercontent\.com|release-assets\.githubusercontent\.com)\/.*\/releases\/.+\.tgz$/.test(kv.tarball)) problems.push(`${f}: tarball 必须 GitHub Release 托管 https .tgz`)
67
+ }
68
+ }
69
+ if (problems.length) return fail(problems.join('\n'))
70
+ return pass(`已收录 ${hits.length} 条(${hits.join(', ')}),字段/枚举/描述校验通过`)
71
+ })
72
+
73
+ doctor.add(GROUP, 'CC3 市场目录条目(dsh-catalog)', () => {
74
+ const p = path.join(ws, 'dsh-catalog', 'data', 'packages.json')
75
+ if (!existsSync(p)) return skip('dsh-catalog 本地不存在')
76
+ const list = readJson(p)
77
+ const hit = (Array.isArray(list) ? list : []).find((e) => e.npm === pkgName)
78
+ if (!hit) return skip('未收录于 DSH Desktop Market 目录(可选渠道)')
79
+ const problems = []
80
+ for (const k of ['npm', 'repo', 'displayName', 'categories', 'summary']) if (!(k in hit)) problems.push(`缺 ${k}`)
81
+ if (hit.repo && g && !String(hit.repo).includes(`${g.owner}/${g.repo}`)) problems.push('repo 字段与 repository 不符')
82
+ if (!Array.isArray(hit.categories) || !hit.categories.length) problems.push('categories 必须为非空数组')
83
+ if (typeof hit.summary === 'string' && hit.summary.length > 1000) problems.push('summary 超 v1 schema 1000 字符上限')
84
+ if (typeof hit.summary === 'string' && /dsh\s+plugin\s+--profile|pnpm\s+add|npm\s+i(nstall)?\s/.test(hit.summary)) problems.push('summary 含安装命令文本(v1 schema 禁止)')
85
+ if (typeof hit.summary === 'string' && !/[.!?)—–"」』]$/.test(hit.summary.trim())) problems.push('summary 疑被截断(未以句读符结尾)')
86
+ if (problems.length) return fail(problems.join('\n'))
87
+ return pass(`目录第 ${list.indexOf(hit) + 1} 条(displayName: ${hit.displayName})`)
88
+ })
89
+
90
+ doctor.add(GROUP, 'CC4 omdsh Workshop 清单(dshWorkshop)', () => {
91
+ const w = pkg.dshWorkshop
92
+ if (!w) return skip('未声明 dshWorkshop(omdsh 可选渠道;harness 不读此字段,激活只看 dsh.bundle)')
93
+ const problems = []
94
+ const act = w.lifecycle?.activation
95
+ if (!act) problems.push('缺 lifecycle.activation')
96
+ else if (!OMSDSH_ACTIVATION.includes(act)) problems.push(`activation=${act} 不在 5 值枚举(build-omdsh-submission.mjs 会 throw)`)
97
+ if (problems.length) return fail(problems.join('\n'))
98
+ return pass(`activation=${act} · restartRequired=${/^restart-/.test(act)}(hub intake 推导规则一致)`)
99
+ })
100
+
101
+ doctor.add(GROUP, 'CC5 插件三门(license/五语 README/seam 三角色)', () => {
102
+ // R-fix 2:这三门(Apache-2.0 + 五语 README + seam 三角色 marker)是 PerryLink 家族私有标准,
103
+ // 不是生态标准。旧实现在没有家族 workspace 时改跑「本地降级检查」并把它施加到目标仓上 →
104
+ // 任何外部作者跑 --only CC 都会被判 fail(不是环境假红,是真判据),且该 fail 会被误读为工具故障。
105
+ // 现在:家族私有标准只在家族工作区内核对(kit CLI 存在),其余显式 not-applicable。
106
+ const kitCli = path.join(ws, 'dsh-plugin-kit', 'lib', 'verify', 'cli.js')
107
+ if (!existsSync(kitCli)) {
108
+ return na('家族私有标准(Apache-2.0 / 五语 README / seam 三角色)不适用于非家族仓;'
109
+ + '如需核对请在家族工作区内运行(--workspace 指向含 dsh-plugin-kit 的目录)')
110
+ }
111
+ const r = runStep('cc5-kit', process.execPath, [kitCli, 'all', repoPath], { cwd: ws, logDir: ctx.logDir, timeout: 120_000, shell: false })
112
+ if (r.ok) return pass('dsh-plugin-kit 三门 CLI 全过')
113
+ const out = `${r.out}\n${r.err}`
114
+ const note = /no source files found under src/.test(out)
115
+ ? '\n注:该仓无 src/ 目录,kit verify-seam 结构不适用(kit 已支持根层 JS 回退,此处以 kit 输出为准)' : ''
116
+ if (r.code === 1) return fail(`dsh-plugin-kit 三门失败:\n${tail(out, 12)}${note}`)
117
+ return fail(`dsh-plugin-kit 三门执行异常(exit ${r.code}):\n${tail(r.err)}${note}`)
118
+ })
119
+ }