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 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** | 55 tests, CI on ubuntu/macos/windows × Node 20/22/24 |
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
- DSH ships an MCP client bundle (`@deepseek-ai/dsh-mcp-client`). Add one entry to your profile patch, e.g. `~/.dsh/profiles/<profile>/cordis.patch.yml`:
345
+ One command installs both the MCP tool and a skill that tells the model when to audit:
346
346
 
347
- ```yaml
348
- - id: mcp-skills-doctor
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
- Restart DSH. The tool shows up as `mcp__skills_doctor__skills_doctor` and accepts `directory`, `budget`, `lang`, `global` and `html`.
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
- #### Optional skill (tells the model when to audit)
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
- The repo ships a DSH skill at `skills/dsh-skills-doctor/`:
357
+ Manual fallback — DSH patches add new entries under `insert`, so append this block:
364
358
 
365
- ```powershell
366
- Copy-Item -Recurse skills\dsh-skills-doctor "$env:USERPROFILE\.dsh\skills\"
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
- DSH watches skill roots, so `skills-doctor-audit` appears in the catalog without a restart. The skill runs `check`/`plan`, explains the token impact, and only proposes fixes — it never edits skills without confirmation.
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 # 55 tests (node test/doctor.test.mjs)
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
 
@@ -23,7 +23,7 @@ OpenCode skill 的**本地检测器 + 更新器 + 修复器**,并附带 **Open
23
23
  | **规划** | 精确 token 节省:精简描述 / 隐藏 skill,按 N 步折算 |
24
24
  | **成本** | CLI 0 token;插件工具定义约 191/步,另有每会话一次预警 |
25
25
  | **要求** | Node ≥ 20,零运行时依赖 |
26
- | **质量** | 55 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
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
- DSH 自带 MCP 客户端 bundle(`@deepseek-ai/dsh-mcp-client`)。在 profile 的 patch 里加一条,例如 `~/.dsh/profiles/<profile>/cordis.patch.yml`:
345
+ 一条命令同时装好 MCP 工具和「告诉模型什么时候该体检」的 skill:
346
346
 
347
- ```yaml
348
- - id: mcp-skills-doctor
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
- 重启 DSH。工具会以 `mcp__skills_doctor__skills_doctor` 出现,参数支持 `directory`、`budget`、`lang`、`global`、`html`。
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
- 仓库自带一个 DSH skill:`skills/dsh-skills-doctor/`:
357
+ 手动兜底 —— DSH 的 patch 里新增条目必须放在 `insert` 下,追加这一块:
364
358
 
365
- ```powershell
366
- Copy-Item -Recurse skills\dsh-skills-doctor "$env:USERPROFILE\.dsh\skills\"
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
- DSH 会监听 skill 目录,`skills-doctor-audit` 无需重启就会出现在目录里。它会跑 `check`/`plan`、解释 token 影响,并且只提出修复建议 —— 不经确认不会改 skill 文件。
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 # 55 个测试(node test/doctor.test.mjs)
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.5.2",
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 Cursor skill layouts.",
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 desktop")
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}",