opencode-skills-doctor 0.6.0 → 0.6.2
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/README.md +87 -495
- package/docs/README.zh-CN.md +23 -11
- package/docs/REFERENCE.md +541 -0
- package/docs/assets/banner.svg +49 -0
- package/docs/assets/report.png +0 -0
- package/docs/assets/social-preview.png +0 -0
- package/package.json +1 -1
- package/src/dsh.mjs +110 -47
- package/src/i18n.mjs +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-skills-doctor",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Audit, fix and update agent skills: token cost, broken references, shadowed duplicates, HTML reports, catalog sync. CLI, OpenCode plugin, MCP server; works with OpenCode, Claude Code, Codex, Grok CLI, Cursor and DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/dsh.mjs
CHANGED
|
@@ -7,13 +7,16 @@ export const ENTRY_ID = "mcp-skills-doctor"
|
|
|
7
7
|
export const SERVER_NAME = "skills_doctor"
|
|
8
8
|
export const SKILL_NAME = "skills-doctor-audit"
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
// DSH 的 cordis.patch.yml 是 patch 列表:新增条目必须放在 insert 下,
|
|
11
|
+
// 裸条目会被当成「覆盖不存在的 id」而跳过。
|
|
12
|
+
const INSERT_BLOCK = `- insert:
|
|
13
|
+
- id: ${ENTRY_ID}
|
|
14
|
+
name: "@deepseek-ai/dsh-mcp-client"
|
|
15
|
+
config:
|
|
16
|
+
serverName: ${SERVER_NAME}
|
|
17
|
+
transport: stdio
|
|
18
|
+
command: npx
|
|
19
|
+
args: ["-y", "opencode-skills-doctor", "mcp"]
|
|
17
20
|
`
|
|
18
21
|
|
|
19
22
|
export function dshHome(env = process.env) {
|
|
@@ -56,76 +59,136 @@ function skillSourceDir() {
|
|
|
56
59
|
return fileURLToPath(new URL("../skills/dsh-skills-doctor", import.meta.url))
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
function indentOf(line) {
|
|
63
|
+
return (line.match(/^[ \t]*/) ?? [""])[0].length
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function blockEnd(lines, start) {
|
|
67
|
+
let end = start + 1
|
|
68
|
+
while (end < lines.length) {
|
|
69
|
+
const line = lines[end]
|
|
70
|
+
if (line.trim() === "" || indentOf(line) > 0) end++
|
|
71
|
+
else break
|
|
67
72
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
return end
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 找到包含我们 id 的 insert 块,或旧版裸条目。 */
|
|
77
|
+
function findEntry(lines, id) {
|
|
78
|
+
for (let i = 0; i < lines.length; i++) {
|
|
79
|
+
const line = lines[i]
|
|
80
|
+
if (indentOf(line) === 0 && /^-\s*insert:\s*$/.test(line)) {
|
|
81
|
+
const end = blockEnd(lines, i)
|
|
82
|
+
const block = lines.slice(i, end).join("\n")
|
|
83
|
+
if (block.includes(id)) return { type: "insert", start: i, end }
|
|
84
|
+
i = end - 1
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
if (indentOf(line) === 0 && line.trim() === `- id: ${id}`) {
|
|
88
|
+
return { type: "legacy", start: i, end: blockEnd(lines, i) }
|
|
89
|
+
}
|
|
71
90
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function removeEntryBlock(text, id) {
|
|
95
|
+
const lines = text.split(/\r?\n/)
|
|
96
|
+
const found = findEntry(lines, id)
|
|
97
|
+
if (!found) return { text, changed: false, type: null }
|
|
98
|
+
|
|
99
|
+
if (found.type === "insert") {
|
|
100
|
+
const block = lines.slice(found.start, found.end)
|
|
101
|
+
const rows = block.filter((line) => /^\s+- id:/.test(line))
|
|
102
|
+
if (rows.length > 1) {
|
|
103
|
+
// insert 块里还有别人的条目:只摘掉我们这一行及其子行
|
|
104
|
+
const rowIndex = block.findIndex((line) => line.trim() === `- id: ${id}`)
|
|
105
|
+
let rowEnd = rowIndex + 1
|
|
106
|
+
while (
|
|
107
|
+
rowEnd < block.length &&
|
|
108
|
+
(block[rowEnd].trim() === "" || indentOf(block[rowEnd]) > indentOf(block[rowIndex]))
|
|
109
|
+
) {
|
|
110
|
+
rowEnd++
|
|
111
|
+
}
|
|
112
|
+
const kept = [...lines.slice(0, found.start), ...block.slice(0, rowIndex), ...block.slice(rowEnd), ...lines.slice(found.end)]
|
|
113
|
+
return { text: kept.join("\n"), changed: true, type: "insert-row" }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const kept = [...lines.slice(0, found.start), ...lines.slice(found.end)]
|
|
118
|
+
return { text: kept.join("\n"), changed: true, type: found.type }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function appendInsert(text) {
|
|
122
|
+
const header = text.trim() ? (text.endsWith("\n") ? "\n" : "\n\n") : ""
|
|
123
|
+
return `${text}${header}${INSERT_BLOCK}`
|
|
76
124
|
}
|
|
77
125
|
|
|
78
|
-
|
|
79
|
-
function appendEntry(patchFile, dryRun, act, dry) {
|
|
126
|
+
function ensureEntry(patchFile, dryRun, act, dry) {
|
|
80
127
|
const existing = fs.existsSync(patchFile) ? fs.readFileSync(patchFile, "utf8") : ""
|
|
81
|
-
|
|
82
|
-
|
|
128
|
+
const lines = existing.split(/\r?\n/)
|
|
129
|
+
const found = findEntry(lines, ENTRY_ID)
|
|
130
|
+
|
|
131
|
+
if (found && found.type === "insert") {
|
|
132
|
+
act(`MCP insert entry already present in ${patchFile}`)
|
|
83
133
|
return false
|
|
84
134
|
}
|
|
85
|
-
|
|
135
|
+
|
|
136
|
+
let next = existing
|
|
137
|
+
if (found && found.type === "legacy") {
|
|
138
|
+
next = removeEntryBlock(existing, ENTRY_ID).text
|
|
139
|
+
act(`migrating legacy patch row in ${patchFile}`)
|
|
140
|
+
}
|
|
141
|
+
next = appendInsert(next)
|
|
142
|
+
|
|
86
143
|
if (dryRun) {
|
|
87
|
-
dry(`
|
|
144
|
+
dry(`write ${ENTRY_ID} insert block to ${patchFile}`)
|
|
88
145
|
return true
|
|
89
146
|
}
|
|
147
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
90
148
|
if (existing) fs.copyFileSync(patchFile, `${patchFile}.bak-${stamp}`)
|
|
91
|
-
const header = existing
|
|
92
|
-
? (existing.endsWith("\n") ? "" : "\n")
|
|
93
|
-
: "# DSH profile patch layer\n"
|
|
94
|
-
const next = `${existing}${header}${existing.trim() ? "\n" : ""}${ENTRIES}`
|
|
95
149
|
fs.writeFileSync(patchFile, next, "utf8")
|
|
96
|
-
act(`
|
|
150
|
+
act(`wrote ${ENTRY_ID} insert block to ${patchFile}`)
|
|
97
151
|
return true
|
|
98
152
|
}
|
|
99
153
|
|
|
100
154
|
function removeEntry(patchFile, dryRun, act, dry) {
|
|
101
155
|
if (!fs.existsSync(patchFile)) return false
|
|
102
156
|
const text = fs.readFileSync(patchFile, "utf8")
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
if (index < 0) return false
|
|
106
|
-
|
|
107
|
-
let end = index + 1
|
|
108
|
-
while (end < lines.length) {
|
|
109
|
-
const line = lines[end]
|
|
110
|
-
if (line.trim() === "" || /^[ \t]/.test(line)) end++
|
|
111
|
-
else break
|
|
112
|
-
}
|
|
157
|
+
const result = removeEntryBlock(text, ENTRY_ID)
|
|
158
|
+
if (!result.changed) return false
|
|
113
159
|
if (dryRun) {
|
|
114
160
|
dry(`remove ${ENTRY_ID} from ${patchFile}`)
|
|
115
161
|
return true
|
|
116
162
|
}
|
|
117
163
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
118
164
|
fs.copyFileSync(patchFile, `${patchFile}.bak-${stamp}`)
|
|
119
|
-
fs.writeFileSync(patchFile,
|
|
165
|
+
fs.writeFileSync(patchFile, result.text, "utf8")
|
|
120
166
|
act(`removed ${ENTRY_ID} from ${patchFile}`)
|
|
121
167
|
return true
|
|
122
168
|
}
|
|
123
169
|
|
|
170
|
+
function copySkill(source, target, dryRun, act, dry) {
|
|
171
|
+
const file = path.join(source, "SKILL.md")
|
|
172
|
+
if (!fs.existsSync(file)) return false
|
|
173
|
+
const targetFile = path.join(target, "SKILL.md")
|
|
174
|
+
if (fs.existsSync(targetFile) && fs.readFileSync(targetFile, "utf8") === fs.readFileSync(file, "utf8")) {
|
|
175
|
+
act(`skill already up to date ${target}`)
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
if (dryRun) {
|
|
179
|
+
dry(`install skill ${SKILL_NAME} into ${target}`)
|
|
180
|
+
return true
|
|
181
|
+
}
|
|
182
|
+
fs.mkdirSync(target, { recursive: true })
|
|
183
|
+
fs.copyFileSync(file, targetFile)
|
|
184
|
+
act(`wrote skill ${targetFile}`)
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
|
|
124
188
|
function removeSkill(target, dryRun, act, dry) {
|
|
125
189
|
const file = path.join(target, "SKILL.md")
|
|
126
190
|
if (!fs.existsSync(file)) return false
|
|
127
|
-
|
|
128
|
-
if (!text.includes("Skill audit workflow")) {
|
|
191
|
+
if (!fs.readFileSync(file, "utf8").includes("Skill audit workflow")) {
|
|
129
192
|
act(`kept ${target} (not the generated skill)`)
|
|
130
193
|
return false
|
|
131
194
|
}
|
|
@@ -157,7 +220,7 @@ export function installDsh({ home = dshHome(), profile = null, dryRun = false }
|
|
|
157
220
|
if (dryRun) dry(`profile: ${picked.profile} (${dir})`)
|
|
158
221
|
else act(`profile: ${picked.profile}`)
|
|
159
222
|
|
|
160
|
-
|
|
223
|
+
ensureEntry(patchFile, dryRun, act, dry)
|
|
161
224
|
copySkill(skillSourceDir(), skillDir, dryRun, act, dry)
|
|
162
225
|
|
|
163
226
|
return { ok: errors.length === 0, home, profile: picked.profile, patchFile, skillDir, actions, errors, dryRun }
|
package/src/i18n.mjs
CHANGED
|
@@ -127,7 +127,7 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
127
127
|
"dsh.profile": " profile: {profile}",
|
|
128
128
|
"dsh.patch": " patch: {file}",
|
|
129
129
|
"dsh.skill": " skill: {dir}",
|
|
130
|
-
"dsh.restart": "Restart DSH to load the MCP tool (the skill updates live).",
|
|
130
|
+
"dsh.restart": "Restart DSH to load the MCP tool (HMR reloads the patch when enabled; the skill updates live).",
|
|
131
131
|
"dsh.failed": "DSH integration failed.",
|
|
132
132
|
"update.toolAvailable":
|
|
133
133
|
" ! skills-doctor {latest} is available (you have {current}); update with: {cmd} · {url}",
|
|
@@ -348,7 +348,7 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
348
348
|
"dsh.profile": " profile: {profile}",
|
|
349
349
|
"dsh.patch": " patch: {file}",
|
|
350
350
|
"dsh.skill": " skill: {dir}",
|
|
351
|
-
"dsh.restart": "重启 DSH 加载 MCP
|
|
351
|
+
"dsh.restart": "重启 DSH 加载 MCP 工具(启用 HMR 时会自动重载 patch;skill 即时更新)。",
|
|
352
352
|
"dsh.failed": "DSH 集成失败。",
|
|
353
353
|
"update.toolAvailable": " ! skills-doctor 有新版本 {latest}(当前 {current});更新:{cmd} · {url}",
|
|
354
354
|
"upgrade.title": "skills-doctor 自更新",
|