opencode-skills-doctor 0.5.2 → 0.6.1
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 +20 -19
- package/docs/README.zh-CN.md +20 -19
- package/package.json +2 -2
- package/src/cli.mjs +55 -2
- package/src/dsh.mjs +248 -0
- package/src/i18n.mjs +20 -2
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Local scanner, linter, fixer, updater and **OpenCode plugin** for agent skills
|
|
|
23
23
|
| **Plan** | exact token savings: trim descriptions and/or hide skills, projected over N steps |
|
|
24
24
|
| **Cost** | CLI: 0 tokens. Plugin: ~191 tokens/step for the tool definition, plus a one-time warning per session |
|
|
25
25
|
| **Requirements** | Node ≥ 20, zero runtime dependencies |
|
|
26
|
-
| **Quality** |
|
|
26
|
+
| **Quality** | 58 tests, CI on ubuntu/macos/windows × Node 20/22/24 |
|
|
27
27
|
|
|
28
28
|
## Why
|
|
29
29
|
|
|
@@ -342,31 +342,32 @@ It scans `.opencode/skills`, `.claude/skills` and `.agents/skills` (project and
|
|
|
342
342
|
|
|
343
343
|
### DeepSeek Harness (DSH)
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
One command installs both the MCP tool and a skill that tells the model when to audit:
|
|
346
346
|
|
|
347
|
-
```
|
|
348
|
-
-
|
|
349
|
-
name: "@deepseek-ai/dsh-mcp-client"
|
|
350
|
-
config:
|
|
351
|
-
serverName: skills_doctor
|
|
352
|
-
transport: stdio
|
|
353
|
-
command: npx
|
|
354
|
-
args: ["-y", "opencode-skills-doctor", "mcp"]
|
|
347
|
+
```powershell
|
|
348
|
+
npx -y opencode-skills-doctor install --client dsh
|
|
355
349
|
```
|
|
356
350
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
On Windows, if the stdio spawn cannot resolve `npx`, use `command: cmd` and `args: ["/c", "npx", "-y", "opencode-skills-doctor", "mcp"]`.
|
|
351
|
+
It locates `~/.dsh/profiles/<profile>/cordis.patch.yml` (default `desktop`; otherwise `--profile <name>`), inserts the `mcp-skills-doctor` row under an `- insert:` block idempotently, and copies the `skills-doctor-audit` skill into `~/.dsh/skills/`. Restart DSH (or let HMR reload the patch) to load the MCP tool; the skill is watched and appears without a restart.
|
|
360
352
|
|
|
361
|
-
|
|
353
|
+
- tool name `mcp__skills_doctor__skills_doctor`, arguments `directory`, `budget`, `lang`, `global`, `html`;
|
|
354
|
+
- `uninstall --client dsh` removes both the patch entry and the skill;
|
|
355
|
+
- `--dsh-home <dir>` for a non-standard DSH home.
|
|
362
356
|
|
|
363
|
-
|
|
357
|
+
Manual fallback — DSH patches add new entries under `insert`, so append this block:
|
|
364
358
|
|
|
365
|
-
```
|
|
366
|
-
|
|
359
|
+
```yaml
|
|
360
|
+
- insert:
|
|
361
|
+
- id: mcp-skills-doctor
|
|
362
|
+
name: "@deepseek-ai/dsh-mcp-client"
|
|
363
|
+
config:
|
|
364
|
+
serverName: skills_doctor
|
|
365
|
+
transport: stdio
|
|
366
|
+
command: npx
|
|
367
|
+
args: ["-y", "opencode-skills-doctor", "mcp"]
|
|
367
368
|
```
|
|
368
369
|
|
|
369
|
-
|
|
370
|
+
On Windows, if the stdio spawn cannot resolve `npx`, use `command: cmd` and `args: ["/c", "npx", "-y", "opencode-skills-doctor", "mcp"]`.
|
|
370
371
|
|
|
371
372
|
## How it works
|
|
372
373
|
|
|
@@ -517,7 +518,7 @@ Executing code is free, but the tool definition is part of the model's context.
|
|
|
517
518
|
## Development and contributing
|
|
518
519
|
|
|
519
520
|
```powershell
|
|
520
|
-
npm test #
|
|
521
|
+
npm test # 58 tests (node test/doctor.test.mjs)
|
|
521
522
|
npm run bench # lint timing for 100/500/1000 skills
|
|
522
523
|
```
|
|
523
524
|
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -23,7 +23,7 @@ OpenCode skill 的**本地检测器 + 更新器 + 修复器**,并附带 **Open
|
|
|
23
23
|
| **规划** | 精确 token 节省:精简描述 / 隐藏 skill,按 N 步折算 |
|
|
24
24
|
| **成本** | CLI 0 token;插件工具定义约 191/步,另有每会话一次预警 |
|
|
25
25
|
| **要求** | Node ≥ 20,零运行时依赖 |
|
|
26
|
-
| **质量** |
|
|
26
|
+
| **质量** | 58 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
|
|
27
27
|
|
|
28
28
|
## 为什么需要它
|
|
29
29
|
|
|
@@ -342,31 +342,32 @@ args = ["-y", "opencode-skills-doctor", "mcp"]
|
|
|
342
342
|
|
|
343
343
|
### DeepSeek Harness(DSH)
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
一条命令同时装好 MCP 工具和「告诉模型什么时候该体检」的 skill:
|
|
346
346
|
|
|
347
|
-
```
|
|
348
|
-
-
|
|
349
|
-
name: "@deepseek-ai/dsh-mcp-client"
|
|
350
|
-
config:
|
|
351
|
-
serverName: skills_doctor
|
|
352
|
-
transport: stdio
|
|
353
|
-
command: npx
|
|
354
|
-
args: ["-y", "opencode-skills-doctor", "mcp"]
|
|
347
|
+
```powershell
|
|
348
|
+
npx -y opencode-skills-doctor install --client dsh
|
|
355
349
|
```
|
|
356
350
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
Windows 上如果 stdio 起不来 `npx`,改成 `command: cmd`、`args: ["/c", "npx", "-y", "opencode-skills-doctor", "mcp"]`。
|
|
351
|
+
它会找到 `~/.dsh/profiles/<profile>/cordis.patch.yml`(默认 `desktop`,否则用 `--profile <name>`),把 `mcp-skills-doctor` 这一行放在 `- insert:` 块里幂等写入,并把 `skills-doctor-audit` 技能复制到 `~/.dsh/skills/`。重启 DSH(启用 HMR 时会自动重载 patch)加载 MCP 工具;skill 目录有监听,无需重启。
|
|
360
352
|
|
|
361
|
-
|
|
353
|
+
- 工具名 `mcp__skills_doctor__skills_doctor`,参数 `directory`、`budget`、`lang`、`global`、`html`;
|
|
354
|
+
- `uninstall --client dsh` 同时移除配置条目和 skill;
|
|
355
|
+
- `--dsh-home <dir>` 用于非默认的 DSH 主目录。
|
|
362
356
|
|
|
363
|
-
|
|
357
|
+
手动兜底 —— DSH 的 patch 里新增条目必须放在 `insert` 下,追加这一块:
|
|
364
358
|
|
|
365
|
-
```
|
|
366
|
-
|
|
359
|
+
```yaml
|
|
360
|
+
- insert:
|
|
361
|
+
- id: mcp-skills-doctor
|
|
362
|
+
name: "@deepseek-ai/dsh-mcp-client"
|
|
363
|
+
config:
|
|
364
|
+
serverName: skills_doctor
|
|
365
|
+
transport: stdio
|
|
366
|
+
command: npx
|
|
367
|
+
args: ["-y", "opencode-skills-doctor", "mcp"]
|
|
367
368
|
```
|
|
368
369
|
|
|
369
|
-
|
|
370
|
+
Windows 上如果 stdio 起不来 `npx`,改成 `command: cmd`、`args: ["/c", "npx", "-y", "opencode-skills-doctor", "mcp"]`。
|
|
370
371
|
|
|
371
372
|
## 工作原理
|
|
372
373
|
|
|
@@ -517,7 +518,7 @@ token 是启发式估算,1200 是建议预算。可以用 `--budget` 或 `.ski
|
|
|
517
518
|
## 开发与贡献
|
|
518
519
|
|
|
519
520
|
```powershell
|
|
520
|
-
npm test #
|
|
521
|
+
npm test # 58 个测试(node test/doctor.test.mjs)
|
|
521
522
|
npm run bench # 100/500/1000 个 skill 的 lint 耗时
|
|
522
523
|
```
|
|
523
524
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-skills-doctor",
|
|
3
|
-
"version": "0.
|
|
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 and
|
|
3
|
+
"version": "0.6.1",
|
|
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": {
|
|
7
7
|
".": "./src/plugin.mjs",
|
package/src/cli.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { auditSkills } from "./audit.mjs"
|
|
|
9
9
|
import { loadConfig } from "./config.mjs"
|
|
10
10
|
import { startMcpServer } from "./mcp.mjs"
|
|
11
11
|
import { installOpenCode, uninstallOpenCode } from "./install.mjs"
|
|
12
|
+
import { installDsh, uninstallDsh } from "./dsh.mjs"
|
|
12
13
|
import { writeHtmlReport, openReport } from "./html.mjs"
|
|
13
14
|
import { planBudget } from "./plan.mjs"
|
|
14
15
|
import {
|
|
@@ -72,6 +73,8 @@ function parseArgs(argv) {
|
|
|
72
73
|
checkOnly: false,
|
|
73
74
|
prune: false,
|
|
74
75
|
client: "auto",
|
|
76
|
+
profile: null,
|
|
77
|
+
dshHome: null,
|
|
75
78
|
configDir: null,
|
|
76
79
|
noNpm: false,
|
|
77
80
|
html: null,
|
|
@@ -153,6 +156,12 @@ function parseArgs(argv) {
|
|
|
153
156
|
options.client = String(args[++i] ?? "").toLowerCase()
|
|
154
157
|
break
|
|
155
158
|
}
|
|
159
|
+
case "--profile":
|
|
160
|
+
options.profile = args[++i] ?? null
|
|
161
|
+
break
|
|
162
|
+
case "--dsh-home":
|
|
163
|
+
options.dshHome = path.resolve(args[++i] ?? ".")
|
|
164
|
+
break
|
|
156
165
|
case "--no-npm":
|
|
157
166
|
options.noNpm = true
|
|
158
167
|
break
|
|
@@ -591,8 +600,8 @@ export async function main(argv) {
|
|
|
591
600
|
process.exit(2)
|
|
592
601
|
}
|
|
593
602
|
|
|
594
|
-
if (!["auto", "opencode", "desktop"].includes(options.client)) {
|
|
595
|
-
console.error("unsupported --client: expected auto, opencode or
|
|
603
|
+
if (!["auto", "opencode", "desktop", "dsh"].includes(options.client)) {
|
|
604
|
+
console.error("unsupported --client: expected auto, opencode, desktop or dsh")
|
|
596
605
|
process.exitCode = 2
|
|
597
606
|
return
|
|
598
607
|
}
|
|
@@ -616,6 +625,29 @@ export async function main(argv) {
|
|
|
616
625
|
return
|
|
617
626
|
}
|
|
618
627
|
|
|
628
|
+
if (options.command === "install" && options.client === "dsh") {
|
|
629
|
+
const result = installDsh({
|
|
630
|
+
home: options.dshHome ?? undefined,
|
|
631
|
+
profile: options.profile ?? undefined,
|
|
632
|
+
dryRun: options.dryRun,
|
|
633
|
+
})
|
|
634
|
+
if (options.json) {
|
|
635
|
+
console.log(JSON.stringify(result, null, 2))
|
|
636
|
+
} else {
|
|
637
|
+
console.log(bold(t("dsh.title")))
|
|
638
|
+
if (result.profile) console.log(dim(t("dsh.profile", { profile: result.profile })))
|
|
639
|
+
if (result.patchFile) console.log(dim(t("dsh.patch", { file: result.patchFile })))
|
|
640
|
+
if (result.skillDir) console.log(dim(t("dsh.skill", { dir: result.skillDir })))
|
|
641
|
+
if (result.dryRun) console.log(dim(t("install.dryRun")))
|
|
642
|
+
for (const action of result.actions) console.log(` · ${action}`)
|
|
643
|
+
for (const error of result.errors) console.log(red(t("install.error", { error })))
|
|
644
|
+
console.log("")
|
|
645
|
+
console.log(result.errors.length ? red(t("dsh.failed")) : dim(t("dsh.restart")))
|
|
646
|
+
}
|
|
647
|
+
process.exitCode = result.errors.length ? 1 : 0
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
|
|
619
651
|
if (options.command === "install") {
|
|
620
652
|
const result = installOpenCode({
|
|
621
653
|
configDir: options.configDir ?? undefined,
|
|
@@ -645,6 +677,27 @@ export async function main(argv) {
|
|
|
645
677
|
return
|
|
646
678
|
}
|
|
647
679
|
|
|
680
|
+
if (options.command === "uninstall" && options.client === "dsh") {
|
|
681
|
+
const result = uninstallDsh({
|
|
682
|
+
home: options.dshHome ?? undefined,
|
|
683
|
+
profile: options.profile ?? undefined,
|
|
684
|
+
dryRun: options.dryRun,
|
|
685
|
+
})
|
|
686
|
+
if (options.json) {
|
|
687
|
+
console.log(JSON.stringify(result, null, 2))
|
|
688
|
+
} else {
|
|
689
|
+
console.log(bold(t("dsh.uninstallTitle")))
|
|
690
|
+
if (result.profile) console.log(dim(t("dsh.profile", { profile: result.profile })))
|
|
691
|
+
if (result.dryRun) console.log(dim(t("install.dryRun")))
|
|
692
|
+
for (const action of result.actions) console.log(` · ${action}`)
|
|
693
|
+
for (const error of result.errors) console.log(red(t("install.error", { error })))
|
|
694
|
+
console.log("")
|
|
695
|
+
console.log(result.errors.length ? red(t("dsh.failed")) : dim(t("dsh.restart")))
|
|
696
|
+
}
|
|
697
|
+
process.exitCode = result.errors.length ? 1 : 0
|
|
698
|
+
return
|
|
699
|
+
}
|
|
700
|
+
|
|
648
701
|
if (options.command === "uninstall") {
|
|
649
702
|
const result = uninstallOpenCode({
|
|
650
703
|
configDir: options.configDir ?? undefined,
|
package/src/dsh.mjs
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import os from "node:os"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
|
|
6
|
+
export const ENTRY_ID = "mcp-skills-doctor"
|
|
7
|
+
export const SERVER_NAME = "skills_doctor"
|
|
8
|
+
export const SKILL_NAME = "skills-doctor-audit"
|
|
9
|
+
|
|
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"]
|
|
20
|
+
`
|
|
21
|
+
|
|
22
|
+
export function dshHome(env = process.env) {
|
|
23
|
+
return env.DSH_HOME || path.join(os.homedir(), ".dsh")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function profileDir(home, profile) {
|
|
27
|
+
return path.join(home, "profiles", profile)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** DSH profile 目录(含 package.json 的子目录)。 */
|
|
31
|
+
export function listProfiles(home) {
|
|
32
|
+
const dir = path.join(home, "profiles")
|
|
33
|
+
try {
|
|
34
|
+
return fs
|
|
35
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
36
|
+
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(dir, entry.name, "package.json")))
|
|
37
|
+
.map((entry) => entry.name)
|
|
38
|
+
.sort()
|
|
39
|
+
} catch {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pickProfile({ home, profile }) {
|
|
45
|
+
const profiles = listProfiles(home)
|
|
46
|
+
if (profile) {
|
|
47
|
+
if (!profiles.includes(profile)) {
|
|
48
|
+
return { error: `DSH profile "${profile}" not found under ${path.join(home, "profiles")}` }
|
|
49
|
+
}
|
|
50
|
+
return { profile, profiles }
|
|
51
|
+
}
|
|
52
|
+
if (profiles.includes("desktop")) return { profile: "desktop", profiles }
|
|
53
|
+
if (profiles.length === 1) return { profile: profiles[0], profiles }
|
|
54
|
+
if (!profiles.length) return { error: `no DSH profile found under ${path.join(home, "profiles")}` }
|
|
55
|
+
return { error: `multiple DSH profiles found (${profiles.join(", ")}); pass --profile <name>` }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function skillSourceDir() {
|
|
59
|
+
return fileURLToPath(new URL("../skills/dsh-skills-doctor", import.meta.url))
|
|
60
|
+
}
|
|
61
|
+
|
|
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
|
|
72
|
+
}
|
|
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
|
+
}
|
|
90
|
+
}
|
|
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}`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ensureEntry(patchFile, dryRun, act, dry) {
|
|
127
|
+
const existing = fs.existsSync(patchFile) ? fs.readFileSync(patchFile, "utf8") : ""
|
|
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}`)
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
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
|
+
|
|
143
|
+
if (dryRun) {
|
|
144
|
+
dry(`write ${ENTRY_ID} insert block to ${patchFile}`)
|
|
145
|
+
return true
|
|
146
|
+
}
|
|
147
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
148
|
+
if (existing) fs.copyFileSync(patchFile, `${patchFile}.bak-${stamp}`)
|
|
149
|
+
fs.writeFileSync(patchFile, next, "utf8")
|
|
150
|
+
act(`wrote ${ENTRY_ID} insert block to ${patchFile}`)
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function removeEntry(patchFile, dryRun, act, dry) {
|
|
155
|
+
if (!fs.existsSync(patchFile)) return false
|
|
156
|
+
const text = fs.readFileSync(patchFile, "utf8")
|
|
157
|
+
const result = removeEntryBlock(text, ENTRY_ID)
|
|
158
|
+
if (!result.changed) return false
|
|
159
|
+
if (dryRun) {
|
|
160
|
+
dry(`remove ${ENTRY_ID} from ${patchFile}`)
|
|
161
|
+
return true
|
|
162
|
+
}
|
|
163
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
164
|
+
fs.copyFileSync(patchFile, `${patchFile}.bak-${stamp}`)
|
|
165
|
+
fs.writeFileSync(patchFile, result.text, "utf8")
|
|
166
|
+
act(`removed ${ENTRY_ID} from ${patchFile}`)
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
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
|
+
|
|
188
|
+
function removeSkill(target, dryRun, act, dry) {
|
|
189
|
+
const file = path.join(target, "SKILL.md")
|
|
190
|
+
if (!fs.existsSync(file)) return false
|
|
191
|
+
if (!fs.readFileSync(file, "utf8").includes("Skill audit workflow")) {
|
|
192
|
+
act(`kept ${target} (not the generated skill)`)
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
195
|
+
if (dryRun) {
|
|
196
|
+
dry(`delete skill ${target}`)
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
fs.rmSync(target, { recursive: true, force: true })
|
|
200
|
+
act(`deleted skill ${target}`)
|
|
201
|
+
return true
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function installDsh({ home = dshHome(), profile = null, dryRun = false } = {}) {
|
|
205
|
+
const actions = []
|
|
206
|
+
const errors = []
|
|
207
|
+
const dry = (message) => actions.push(`[dry-run] ${message}`)
|
|
208
|
+
const act = (message) => actions.push(message)
|
|
209
|
+
|
|
210
|
+
const picked = pickProfile({ home, profile })
|
|
211
|
+
if (picked.error) {
|
|
212
|
+
errors.push(picked.error)
|
|
213
|
+
return { ok: false, home, actions, errors, dryRun }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const dir = profileDir(home, picked.profile)
|
|
217
|
+
const patchFile = path.join(dir, "cordis.patch.yml")
|
|
218
|
+
const skillDir = path.join(home, "skills", SKILL_NAME)
|
|
219
|
+
|
|
220
|
+
if (dryRun) dry(`profile: ${picked.profile} (${dir})`)
|
|
221
|
+
else act(`profile: ${picked.profile}`)
|
|
222
|
+
|
|
223
|
+
ensureEntry(patchFile, dryRun, act, dry)
|
|
224
|
+
copySkill(skillSourceDir(), skillDir, dryRun, act, dry)
|
|
225
|
+
|
|
226
|
+
return { ok: errors.length === 0, home, profile: picked.profile, patchFile, skillDir, actions, errors, dryRun }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function uninstallDsh({ home = dshHome(), profile = null, dryRun = false } = {}) {
|
|
230
|
+
const actions = []
|
|
231
|
+
const errors = []
|
|
232
|
+
const dry = (message) => actions.push(`[dry-run] ${message}`)
|
|
233
|
+
const act = (message) => actions.push(message)
|
|
234
|
+
|
|
235
|
+
const picked = pickProfile({ home, profile })
|
|
236
|
+
if (picked.error) {
|
|
237
|
+
errors.push(picked.error)
|
|
238
|
+
return { ok: false, home, actions, errors, dryRun }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const dir = profileDir(home, picked.profile)
|
|
242
|
+
const patchFile = path.join(dir, "cordis.patch.yml")
|
|
243
|
+
const skillDir = path.join(home, "skills", SKILL_NAME)
|
|
244
|
+
|
|
245
|
+
removeEntry(patchFile, dryRun, act, dry)
|
|
246
|
+
removeSkill(skillDir, dryRun, act, dry)
|
|
247
|
+
return { ok: errors.length === 0, home, profile: picked.profile, patchFile, skillDir, actions, errors, dryRun }
|
|
248
|
+
}
|
package/src/i18n.mjs
CHANGED
|
@@ -36,7 +36,9 @@ Options:
|
|
|
36
36
|
--html [file] check: write a self-contained HTML report (default: skills-report.html)
|
|
37
37
|
--open check: open the HTML report after writing it
|
|
38
38
|
--config-dir <dir> install/uninstall: OpenCode config directory (default: ~/.config/opencode)
|
|
39
|
-
--client <name> install/uninstall: auto | opencode | desktop (default auto)
|
|
39
|
+
--client <name> install/uninstall: auto | opencode | desktop | dsh (default auto)
|
|
40
|
+
--profile <name> dsh: profile to install into (default: desktop, or the only one)
|
|
41
|
+
--dsh-home <dir> dsh: DSH home directory (default: ~/.dsh)
|
|
40
42
|
--no-npm install/uninstall: do not run npm (1.x)
|
|
41
43
|
--apply upgrade: run npm install -g opencode-skills-doctor@latest
|
|
42
44
|
--no-update-check skip the automatic update check for this run
|
|
@@ -120,6 +122,13 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
120
122
|
"install.next": "Restart OpenCode to activate.",
|
|
121
123
|
"uninstall.title": "Remove OpenCode integration",
|
|
122
124
|
"uninstall.next": "Restart OpenCode to finish removal.",
|
|
125
|
+
"dsh.title": "DSH integration",
|
|
126
|
+
"dsh.uninstallTitle": "Remove DSH integration",
|
|
127
|
+
"dsh.profile": " profile: {profile}",
|
|
128
|
+
"dsh.patch": " patch: {file}",
|
|
129
|
+
"dsh.skill": " skill: {dir}",
|
|
130
|
+
"dsh.restart": "Restart DSH to load the MCP tool (HMR reloads the patch when enabled; the skill updates live).",
|
|
131
|
+
"dsh.failed": "DSH integration failed.",
|
|
123
132
|
"update.toolAvailable":
|
|
124
133
|
" ! skills-doctor {latest} is available (you have {current}); update with: {cmd} · {url}",
|
|
125
134
|
"upgrade.title": "skills-doctor upgrade",
|
|
@@ -248,7 +257,9 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
248
257
|
--html [file] check: 生成自包含 HTML 报告(默认 skills-report.html)
|
|
249
258
|
--open check: 写完报告后用浏览器打开
|
|
250
259
|
--config-dir <dir> install/uninstall: OpenCode 配置目录(默认 ~/.config/opencode)
|
|
251
|
-
--client <name> install/uninstall: auto | opencode | desktop(默认 auto)
|
|
260
|
+
--client <name> install/uninstall: auto | opencode | desktop | dsh(默认 auto)
|
|
261
|
+
--profile <name> dsh: 安装到哪个 profile(默认 desktop,或唯一的一个)
|
|
262
|
+
--dsh-home <dir> dsh: DSH 主目录(默认 ~/.dsh)
|
|
252
263
|
--no-npm install/uninstall: 不执行 npm(1.x)
|
|
253
264
|
--apply upgrade: 执行 npm install -g opencode-skills-doctor@latest
|
|
254
265
|
--no-update-check 本次运行跳过自动更新检查
|
|
@@ -332,6 +343,13 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
332
343
|
"install.next": "重启 OpenCode 生效。",
|
|
333
344
|
"uninstall.title": "撤销 OpenCode 集成",
|
|
334
345
|
"uninstall.next": "重启 OpenCode 完成卸载。",
|
|
346
|
+
"dsh.title": "DSH 集成",
|
|
347
|
+
"dsh.uninstallTitle": "撤销 DSH 集成",
|
|
348
|
+
"dsh.profile": " profile: {profile}",
|
|
349
|
+
"dsh.patch": " patch: {file}",
|
|
350
|
+
"dsh.skill": " skill: {dir}",
|
|
351
|
+
"dsh.restart": "重启 DSH 加载 MCP 工具(启用 HMR 时会自动重载 patch;skill 即时更新)。",
|
|
352
|
+
"dsh.failed": "DSH 集成失败。",
|
|
335
353
|
"update.toolAvailable": " ! skills-doctor 有新版本 {latest}(当前 {current});更新:{cmd} · {url}",
|
|
336
354
|
"upgrade.title": "skills-doctor 自更新",
|
|
337
355
|
"upgrade.current": " 当前版本: {version}",
|