opencode-skills-doctor 0.5.0 → 0.5.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 +28 -3
- package/docs/README.zh-CN.md +28 -3
- package/package.json +2 -1
- package/skills/dsh-skills-doctor/SKILL.md +54 -0
- package/src/cli.mjs +25 -0
- package/src/desktop.mjs +80 -0
- package/src/i18n.mjs +6 -0
- package/src/install.mjs +46 -10
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** | 55 tests, CI on ubuntu/macos/windows × Node 20/22/24 |
|
|
27
27
|
|
|
28
28
|
## Why
|
|
29
29
|
|
|
@@ -264,6 +264,21 @@ It registers the same `skills_doctor` tool. 1.x plugins cannot register commands
|
|
|
264
264
|
|
|
265
265
|
> The package root entry is the V2 plugin and requires OpenCode 2.x. The CLI works on both lines.
|
|
266
266
|
|
|
267
|
+
### OpenCode Desktop
|
|
268
|
+
|
|
269
|
+
The desktop app bundles the same OpenCode core and reads the **same config directory** as the CLI (`~/.config/opencode`; its UI settings live elsewhere), so one install covers both:
|
|
270
|
+
|
|
271
|
+
```powershell
|
|
272
|
+
npx opencode-skills-doctor install --client desktop
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- detects the app in the standard install locations and reads its bundled core version from `resources/app.asar` (e.g. 1.17.11), then uses the matching plugin API;
|
|
276
|
+
- writes the same plugin entry and `/skills-doctor`, `/skills-report`, `/skills-upgrade` commands as the CLI install — restart the desktop app afterwards;
|
|
277
|
+
- `--client auto` (default) prefers the CLI version when both are installed; `--client opencode` skips desktop detection;
|
|
278
|
+
- `uninstall --client desktop` reverses it.
|
|
279
|
+
|
|
280
|
+
Verified on Windows: app at `%LOCALAPPDATA%\Programs\@opencode-aidesktop\OpenCode.exe`, sharing config with the `opencode` CLI.
|
|
281
|
+
|
|
267
282
|
### Deep integration (one command)
|
|
268
283
|
|
|
269
284
|
```powershell
|
|
@@ -339,10 +354,20 @@ DSH ships an MCP client bundle (`@deepseek-ai/dsh-mcp-client`). Add one entry to
|
|
|
339
354
|
args: ["-y", "opencode-skills-doctor", "mcp"]
|
|
340
355
|
```
|
|
341
356
|
|
|
342
|
-
Restart DSH. The tool shows up as `mcp__skills_doctor__skills_doctor`.
|
|
357
|
+
Restart DSH. The tool shows up as `mcp__skills_doctor__skills_doctor` and accepts `directory`, `budget`, `lang`, `global` and `html`.
|
|
343
358
|
|
|
344
359
|
On Windows, if the stdio spawn cannot resolve `npx`, use `command: cmd` and `args: ["/c", "npx", "-y", "opencode-skills-doctor", "mcp"]`.
|
|
345
360
|
|
|
361
|
+
#### Optional skill (tells the model when to audit)
|
|
362
|
+
|
|
363
|
+
The repo ships a DSH skill at `skills/dsh-skills-doctor/`:
|
|
364
|
+
|
|
365
|
+
```powershell
|
|
366
|
+
Copy-Item -Recurse skills\dsh-skills-doctor "$env:USERPROFILE\.dsh\skills\"
|
|
367
|
+
```
|
|
368
|
+
|
|
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
|
+
|
|
346
371
|
## How it works
|
|
347
372
|
|
|
348
373
|
**Audit pipeline**
|
|
@@ -492,7 +517,7 @@ Executing code is free, but the tool definition is part of the model's context.
|
|
|
492
517
|
## Development and contributing
|
|
493
518
|
|
|
494
519
|
```powershell
|
|
495
|
-
npm test #
|
|
520
|
+
npm test # 55 tests (node test/doctor.test.mjs)
|
|
496
521
|
npm run bench # lint timing for 100/500/1000 skills
|
|
497
522
|
```
|
|
498
523
|
|
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
|
+
| **质量** | 55 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
|
|
27
27
|
|
|
28
28
|
## 为什么需要它
|
|
29
29
|
|
|
@@ -264,6 +264,21 @@ export { SkillsDoctorPlugin } from "opencode-skills-doctor/v1"
|
|
|
264
264
|
|
|
265
265
|
> 包根入口是 V2 插件,需要 OpenCode 2.x;CLI 两条线都能用。
|
|
266
266
|
|
|
267
|
+
### OpenCode Desktop
|
|
268
|
+
|
|
269
|
+
桌面版内置的是同一套 OpenCode core,读的也是**同一个配置目录**(`~/.config/opencode`;桌面 UI 的设置另存),所以一次安装两边都生效:
|
|
270
|
+
|
|
271
|
+
```powershell
|
|
272
|
+
npx opencode-skills-doctor install --client desktop
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- 在标准安装位置探测桌面版,并从 `resources/app.asar` 读出内置 core 版本(例如 1.17.11),据此选择对应插件 API;
|
|
276
|
+
- 写入与 CLI 相同的插件条目和 `/skills-doctor`、`/skills-report`、`/skills-upgrade` 命令 —— 装完重启桌面应用;
|
|
277
|
+
- `--client auto`(默认)在同时装了 CLI 时优先 CLI 版本;`--client opencode` 跳过桌面探测;
|
|
278
|
+
- `uninstall --client desktop` 反向撤销。
|
|
279
|
+
|
|
280
|
+
已在 Windows 验证:应用位于 `%LOCALAPPDATA%\Programs\@opencode-aidesktop\OpenCode.exe`,与 `opencode` CLI 共用配置。
|
|
281
|
+
|
|
267
282
|
### 一键深度集成
|
|
268
283
|
|
|
269
284
|
```powershell
|
|
@@ -339,10 +354,20 @@ DSH 自带 MCP 客户端 bundle(`@deepseek-ai/dsh-mcp-client`)。在 profile
|
|
|
339
354
|
args: ["-y", "opencode-skills-doctor", "mcp"]
|
|
340
355
|
```
|
|
341
356
|
|
|
342
|
-
重启 DSH。工具会以 `mcp__skills_doctor__skills_doctor`
|
|
357
|
+
重启 DSH。工具会以 `mcp__skills_doctor__skills_doctor` 出现,参数支持 `directory`、`budget`、`lang`、`global`、`html`。
|
|
343
358
|
|
|
344
359
|
Windows 上如果 stdio 起不来 `npx`,改成 `command: cmd`、`args: ["/c", "npx", "-y", "opencode-skills-doctor", "mcp"]`。
|
|
345
360
|
|
|
361
|
+
#### 可选技能包(告诉模型什么时候该体检)
|
|
362
|
+
|
|
363
|
+
仓库自带一个 DSH skill:`skills/dsh-skills-doctor/`:
|
|
364
|
+
|
|
365
|
+
```powershell
|
|
366
|
+
Copy-Item -Recurse skills\dsh-skills-doctor "$env:USERPROFILE\.dsh\skills\"
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
DSH 会监听 skill 目录,`skills-doctor-audit` 无需重启就会出现在目录里。它会跑 `check`/`plan`、解释 token 影响,并且只提出修复建议 —— 不经确认不会改 skill 文件。
|
|
370
|
+
|
|
346
371
|
## 工作原理
|
|
347
372
|
|
|
348
373
|
**审计流程**
|
|
@@ -492,7 +517,7 @@ token 是启发式估算,1200 是建议预算。可以用 `--budget` 或 `.ski
|
|
|
492
517
|
## 开发与贡献
|
|
493
518
|
|
|
494
519
|
```powershell
|
|
495
|
-
npm test #
|
|
520
|
+
npm test # 55 个测试(node test/doctor.test.mjs)
|
|
496
521
|
npm run bench # 100/500/1000 个 skill 的 lint 耗时
|
|
497
522
|
```
|
|
498
523
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-skills-doctor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.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 and Cursor skill layouts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"bin",
|
|
38
38
|
"src",
|
|
39
39
|
"docs",
|
|
40
|
+
"skills",
|
|
40
41
|
"README.md",
|
|
41
42
|
"SECURITY.md",
|
|
42
43
|
"SECURITY.zh-CN.md"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skills-doctor-audit
|
|
3
|
+
description: Audit installed agent skills for per-step token cost, shadowed duplicates, broken references and budget overruns, then propose concrete fixes.
|
|
4
|
+
whenToUse: Use when the user mentions too many skills, context bloat, token cost, duplicate or missing skills, or asks for a skill health check.
|
|
5
|
+
metadata:
|
|
6
|
+
author: wangyuchen5110
|
|
7
|
+
source: opencode-skills-doctor
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Skill audit workflow
|
|
11
|
+
|
|
12
|
+
Audit the user's installed skills and turn the result into a short, actionable answer.
|
|
13
|
+
Never modify skill files without explicit confirmation.
|
|
14
|
+
|
|
15
|
+
## 1. Run the read-only audit
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
npx -y opencode-skills-doctor check --json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Read `summary.advertisedTokens` (cost per model step), `summary.advertised` and `findings[]`.
|
|
22
|
+
If the command fails, fall back to `opencode-skills-doctor` when installed globally, or ask the user to run it from the repo.
|
|
23
|
+
|
|
24
|
+
## 2. Explain the top findings
|
|
25
|
+
|
|
26
|
+
Report at most three findings, in plain language, each with its token impact:
|
|
27
|
+
|
|
28
|
+
- `token-budget` — descriptions cost roughly N tokens on **every** model step;
|
|
29
|
+
- `shadowed` — the same skill ID loads from several places, only one wins;
|
|
30
|
+
- `long-description` / `large-body` — descriptions or bodies that are paid on every step / every invocation;
|
|
31
|
+
- `broken-reference` — a `references/` or `scripts/` file the skill points at is missing.
|
|
32
|
+
|
|
33
|
+
## 3. Show what can actually be saved
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
npx -y opencode-skills-doctor plan --json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Report `recommended.savings` (per step), `recommended.final` and `recommended.stepsSavings`
|
|
40
|
+
(per 100 steps). Explain the difference: trimming descriptions keeps discoverability; hiding a skill
|
|
41
|
+
(`autoinvoke: false`) removes it from the advertised list but keeps `@id` loading.
|
|
42
|
+
|
|
43
|
+
## 4. Propose fixes — do not apply silently
|
|
44
|
+
|
|
45
|
+
- Hide low-frequency skills: `npx -y opencode-skills-doctor fix --hide id1,id2`
|
|
46
|
+
- Archive shadowed duplicates (reversible): `npx -y opencode-skills-doctor fix`
|
|
47
|
+
- Trim descriptions manually, keeping them discoverable
|
|
48
|
+
- Ask first, then re-run `check` to confirm the new cost
|
|
49
|
+
|
|
50
|
+
## 5. Optional shareable report
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
npx -y opencode-skills-doctor check --html skills-report.html --open
|
|
54
|
+
```
|
package/src/cli.mjs
CHANGED
|
@@ -71,6 +71,7 @@ function parseArgs(argv) {
|
|
|
71
71
|
into: null,
|
|
72
72
|
checkOnly: false,
|
|
73
73
|
prune: false,
|
|
74
|
+
client: "auto",
|
|
74
75
|
configDir: null,
|
|
75
76
|
noNpm: false,
|
|
76
77
|
html: null,
|
|
@@ -148,6 +149,10 @@ function parseArgs(argv) {
|
|
|
148
149
|
case "--config-dir":
|
|
149
150
|
options.configDir = path.resolve(args[++i] ?? ".")
|
|
150
151
|
break
|
|
152
|
+
case "--client": {
|
|
153
|
+
options.client = String(args[++i] ?? "").toLowerCase()
|
|
154
|
+
break
|
|
155
|
+
}
|
|
151
156
|
case "--no-npm":
|
|
152
157
|
options.noNpm = true
|
|
153
158
|
break
|
|
@@ -586,6 +591,12 @@ export async function main(argv) {
|
|
|
586
591
|
process.exit(2)
|
|
587
592
|
}
|
|
588
593
|
|
|
594
|
+
if (!["auto", "opencode", "desktop"].includes(options.client)) {
|
|
595
|
+
console.error("unsupported --client: expected auto, opencode or desktop")
|
|
596
|
+
process.exitCode = 2
|
|
597
|
+
return
|
|
598
|
+
}
|
|
599
|
+
|
|
589
600
|
const cwd = path.resolve(options.root)
|
|
590
601
|
const loaded = loadConfig(cwd, options.config)
|
|
591
602
|
const lang = normalizeLang(options.lang) ?? normalizeLang(loaded.config.lang) ?? detectLang()
|
|
@@ -610,6 +621,7 @@ export async function main(argv) {
|
|
|
610
621
|
configDir: options.configDir ?? undefined,
|
|
611
622
|
dryRun: options.dryRun,
|
|
612
623
|
installPackage: !options.noNpm,
|
|
624
|
+
client: options.client,
|
|
613
625
|
})
|
|
614
626
|
if (options.json) {
|
|
615
627
|
console.log(JSON.stringify(result, null, 2))
|
|
@@ -617,6 +629,12 @@ export async function main(argv) {
|
|
|
617
629
|
console.log(bold(t("install.title")))
|
|
618
630
|
console.log(dim(t("install.version", { version: result.version ?? "unknown" })))
|
|
619
631
|
console.log(dim(t("install.configDir", { dir: result.configDir })))
|
|
632
|
+
console.log(dim(t("install.client", { client: result.client?.mode ?? options.client })))
|
|
633
|
+
if (result.client?.desktop) {
|
|
634
|
+
console.log(
|
|
635
|
+
dim(t("install.desktop", { version: result.client.desktop.version ?? "unknown", exe: result.client.desktop.exe })),
|
|
636
|
+
)
|
|
637
|
+
}
|
|
620
638
|
if (result.dryRun) console.log(dim(t("install.dryRun")))
|
|
621
639
|
for (const action of result.actions) console.log(` · ${action}`)
|
|
622
640
|
for (const error of result.errors) console.log(red(t("install.error", { error })))
|
|
@@ -632,6 +650,7 @@ export async function main(argv) {
|
|
|
632
650
|
configDir: options.configDir ?? undefined,
|
|
633
651
|
dryRun: options.dryRun,
|
|
634
652
|
removePackage: !options.noNpm,
|
|
653
|
+
client: options.client,
|
|
635
654
|
})
|
|
636
655
|
if (options.json) {
|
|
637
656
|
console.log(JSON.stringify(result, null, 2))
|
|
@@ -639,6 +658,12 @@ export async function main(argv) {
|
|
|
639
658
|
console.log(bold(t("uninstall.title")))
|
|
640
659
|
console.log(dim(t("install.version", { version: result.version ?? "unknown" })))
|
|
641
660
|
console.log(dim(t("install.configDir", { dir: result.configDir })))
|
|
661
|
+
console.log(dim(t("install.client", { client: result.client?.mode ?? options.client })))
|
|
662
|
+
if (result.client?.desktop) {
|
|
663
|
+
console.log(
|
|
664
|
+
dim(t("install.desktop", { version: result.client.desktop.version ?? "unknown", exe: result.client.desktop.exe })),
|
|
665
|
+
)
|
|
666
|
+
}
|
|
642
667
|
if (result.dryRun) console.log(dim(t("install.dryRun")))
|
|
643
668
|
for (const action of result.actions) console.log(` · ${action}`)
|
|
644
669
|
for (const error of result.errors) console.log(red(t("install.error", { error })))
|
package/src/desktop.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import os from "node:os"
|
|
4
|
+
|
|
5
|
+
const APP_NAME = "OpenCode"
|
|
6
|
+
|
|
7
|
+
/** 各平台 OpenCode Desktop 的常见安装位置。 */
|
|
8
|
+
export function desktopCandidates(platform = process.platform, env = process.env, home = os.homedir()) {
|
|
9
|
+
if (platform === "win32") {
|
|
10
|
+
const local = env.LOCALAPPDATA || path.join(home, "AppData", "Local")
|
|
11
|
+
return [
|
|
12
|
+
path.join(local, "Programs", "@opencode-aidesktop", "OpenCode.exe"),
|
|
13
|
+
path.join(local, "Programs", "opencode", "OpenCode.exe"),
|
|
14
|
+
path.join(local, "Programs", "opencode-desktop", "OpenCode.exe"),
|
|
15
|
+
path.join(local, "opencode-desktop", "OpenCode.exe"),
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
if (platform === "darwin") {
|
|
19
|
+
return ["/Applications/OpenCode.app/Contents/MacOS/OpenCode", path.join(home, "Applications", "OpenCode.app", "Contents", "MacOS", "OpenCode")]
|
|
20
|
+
}
|
|
21
|
+
return ["/usr/bin/opencode-desktop", "/usr/local/bin/opencode-desktop", "/opt/OpenCode/opencode-desktop", "/opt/opencode-desktop/opencode-desktop"]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function asarPathFor(exe) {
|
|
25
|
+
if (process.platform === "darwin" || exe.includes(".app")) {
|
|
26
|
+
return path.join(path.dirname(path.dirname(path.dirname(path.dirname(exe)))), "Resources", "app.asar")
|
|
27
|
+
}
|
|
28
|
+
return path.join(path.dirname(exe), "resources", "app.asar")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 读取 app.asar 里的 package.json.version(不解包整个 asar)。
|
|
33
|
+
* asar 头结构:[u32 4][u32 headerSize][u32 stringSize][u32 jsonSize][json][content…]
|
|
34
|
+
*/
|
|
35
|
+
export function readAsarVersion(asarFile) {
|
|
36
|
+
let fd
|
|
37
|
+
try {
|
|
38
|
+
fd = fs.openSync(asarFile, "r")
|
|
39
|
+
const head = Buffer.alloc(16)
|
|
40
|
+
fs.readSync(fd, head, 0, 16, 0)
|
|
41
|
+
const headerSize = head.readUInt32LE(4)
|
|
42
|
+
const jsonSize = head.readUInt32LE(12)
|
|
43
|
+
if (!jsonSize || jsonSize > 64 * 1024 * 1024) return null
|
|
44
|
+
const json = Buffer.alloc(jsonSize)
|
|
45
|
+
fs.readSync(fd, json, 0, jsonSize, 16)
|
|
46
|
+
const header = JSON.parse(json.toString("utf8"))
|
|
47
|
+
const entry = header?.files?.["package.json"]
|
|
48
|
+
if (!entry || typeof entry.offset !== "string") return null
|
|
49
|
+
const size = Number(entry.size ?? 0)
|
|
50
|
+
if (!Number.isFinite(size) || size <= 0 || size > 1024 * 1024) return null
|
|
51
|
+
const contentStart = 8 + headerSize
|
|
52
|
+
const data = Buffer.alloc(size)
|
|
53
|
+
fs.readSync(fd, data, 0, size, contentStart + Number(entry.offset))
|
|
54
|
+
const pkg = JSON.parse(data.toString("utf8"))
|
|
55
|
+
return typeof pkg.version === "string" ? pkg.version : null
|
|
56
|
+
} catch {
|
|
57
|
+
return null
|
|
58
|
+
} finally {
|
|
59
|
+
if (fd !== undefined) {
|
|
60
|
+
try {
|
|
61
|
+
fs.closeSync(fd)
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 探测本机是否装了 OpenCode Desktop,并读取它内置 core 的版本。
|
|
69
|
+
* Desktop 与 CLI 共用 ~/.config/opencode,所以插件/命令配置不需要第二份。
|
|
70
|
+
*/
|
|
71
|
+
export function detectDesktop({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
72
|
+
for (const exe of desktopCandidates(platform, env, home)) {
|
|
73
|
+
if (!fs.existsSync(exe)) continue
|
|
74
|
+
const version = readAsarVersion(asarPathFor(exe))
|
|
75
|
+
return { installed: true, exe, version, asar: asarPathFor(exe) }
|
|
76
|
+
}
|
|
77
|
+
return { installed: false, exe: null, version: null, asar: null }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const DESKTOP_APP = APP_NAME
|
package/src/i18n.mjs
CHANGED
|
@@ -36,6 +36,7 @@ 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
40
|
--no-npm install/uninstall: do not run npm (1.x)
|
|
40
41
|
--apply upgrade: run npm install -g opencode-skills-doctor@latest
|
|
41
42
|
--no-update-check skip the automatic update check for this run
|
|
@@ -111,6 +112,8 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
111
112
|
"install.title": "OpenCode integration",
|
|
112
113
|
"install.version": " OpenCode: {version}",
|
|
113
114
|
"install.configDir": " config dir: {dir}",
|
|
115
|
+
"install.client": " client: {client}",
|
|
116
|
+
"install.desktop": " OpenCode Desktop: {version} ({exe})",
|
|
114
117
|
"install.dryRun": " (dry run: nothing is written)",
|
|
115
118
|
"install.error": " ✗ {error}",
|
|
116
119
|
"install.failed": "Integration failed.",
|
|
@@ -245,6 +248,7 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
245
248
|
--html [file] check: 生成自包含 HTML 报告(默认 skills-report.html)
|
|
246
249
|
--open check: 写完报告后用浏览器打开
|
|
247
250
|
--config-dir <dir> install/uninstall: OpenCode 配置目录(默认 ~/.config/opencode)
|
|
251
|
+
--client <name> install/uninstall: auto | opencode | desktop(默认 auto)
|
|
248
252
|
--no-npm install/uninstall: 不执行 npm(1.x)
|
|
249
253
|
--apply upgrade: 执行 npm install -g opencode-skills-doctor@latest
|
|
250
254
|
--no-update-check 本次运行跳过自动更新检查
|
|
@@ -320,6 +324,8 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
320
324
|
"install.title": "OpenCode 集成",
|
|
321
325
|
"install.version": " OpenCode: {version}",
|
|
322
326
|
"install.configDir": " 配置目录: {dir}",
|
|
327
|
+
"install.client": " 客户端: {client}",
|
|
328
|
+
"install.desktop": " OpenCode Desktop: {version}({exe})",
|
|
323
329
|
"install.dryRun": " (dry run:不会写入任何内容)",
|
|
324
330
|
"install.error": " ✗ {error}",
|
|
325
331
|
"install.failed": "集成失败。",
|
package/src/install.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path"
|
|
|
3
3
|
import os from "node:os"
|
|
4
4
|
import { spawnSync, execFileSync } from "node:child_process"
|
|
5
5
|
import { stripJsonc } from "./discover.mjs"
|
|
6
|
+
import { detectDesktop } from "./desktop.mjs"
|
|
6
7
|
|
|
7
8
|
export const PACKAGE_NAME = "opencode-skills-doctor"
|
|
8
9
|
const PLUGIN_FILE = "skills-doctor.js"
|
|
@@ -106,7 +107,13 @@ function installPackage(dir) {
|
|
|
106
107
|
* - 2.x:写入 plugins 数组(OpenCode 自己从 npm 拉包)
|
|
107
108
|
* - 1.x:在配置目录安装包、写本地插件入口、写 /skills-doctor 命令、补插件数组
|
|
108
109
|
*/
|
|
109
|
-
export function installOpenCode({
|
|
110
|
+
export function installOpenCode({
|
|
111
|
+
configDir = defaultConfigDir(),
|
|
112
|
+
version = null,
|
|
113
|
+
dryRun = false,
|
|
114
|
+
installPackage: withPackage = true,
|
|
115
|
+
client = "auto",
|
|
116
|
+
} = {}) {
|
|
110
117
|
const dir = path.resolve(configDir)
|
|
111
118
|
const actions = []
|
|
112
119
|
const errors = []
|
|
@@ -125,14 +132,31 @@ export function installOpenCode({ configDir = defaultConfigDir(), version = null
|
|
|
125
132
|
}
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
const
|
|
135
|
+
const desktop = client === "opencode" ? { installed: false, exe: null, version: null } : detectDesktop()
|
|
136
|
+
if (client === "desktop" && !desktop.installed) {
|
|
137
|
+
errors.push("OpenCode Desktop not found in the standard install locations")
|
|
138
|
+
act("install OpenCode Desktop first (https://opencode.ai/download), or use --client auto")
|
|
139
|
+
return { version: null, major: null, configDir: dir, configFile: cfgFile, actions, errors, dryRun, client: { mode: client, desktop: null } }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const resolvedVersion = version ?? detectVersion() ?? desktop.version
|
|
129
143
|
const inferredMajor = config.plugin !== undefined ? 1 : 2
|
|
130
144
|
const major = resolvedVersion ? Number(resolvedVersion.split(".")[0]) : inferredMajor
|
|
145
|
+
const clientInfo = {
|
|
146
|
+
mode: client,
|
|
147
|
+
desktop: desktop.installed ? { exe: desktop.exe, version: desktop.version } : null,
|
|
148
|
+
}
|
|
149
|
+
if (desktop.installed) act(`OpenCode Desktop ${desktop.version ?? "unknown"} detected at ${desktop.exe}`)
|
|
150
|
+
if (desktop.installed) act("desktop and CLI share the same config directory")
|
|
131
151
|
|
|
132
152
|
if (dryRun) dry(`config dir: ${dir}`)
|
|
133
153
|
if (!fs.existsSync(dir)) {
|
|
134
|
-
if (!dryRun)
|
|
135
|
-
|
|
154
|
+
if (!dryRun) {
|
|
155
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
156
|
+
act(`created config dir ${dir}`)
|
|
157
|
+
} else {
|
|
158
|
+
dry(`create config dir ${dir}`)
|
|
159
|
+
}
|
|
136
160
|
}
|
|
137
161
|
|
|
138
162
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
@@ -151,7 +175,7 @@ export function installOpenCode({ configDir = defaultConfigDir(), version = null
|
|
|
151
175
|
act(`plugins already contains "${PACKAGE_NAME}"`)
|
|
152
176
|
}
|
|
153
177
|
act("restart OpenCode; the /skills-doctor command and skills_doctor tool will be available")
|
|
154
|
-
return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
|
|
178
|
+
return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun, client: clientInfo }
|
|
155
179
|
}
|
|
156
180
|
|
|
157
181
|
// 1.x
|
|
@@ -226,7 +250,7 @@ export function installOpenCode({ configDir = defaultConfigDir(), version = null
|
|
|
226
250
|
}
|
|
227
251
|
|
|
228
252
|
act("restart OpenCode; the skills_doctor tool and /skills-doctor command will be available")
|
|
229
|
-
return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
|
|
253
|
+
return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun, client: clientInfo }
|
|
230
254
|
}
|
|
231
255
|
|
|
232
256
|
function removePackage(dir) {
|
|
@@ -243,7 +267,13 @@ function removePackage(dir) {
|
|
|
243
267
|
* 撤销 install:移除配置里的插件条目、删除安装器生成的文件、可选卸载 npm 包。
|
|
244
268
|
* 只删除「由安装器生成」的文件,用户自己改过的一律保留并提示。
|
|
245
269
|
*/
|
|
246
|
-
export function uninstallOpenCode({
|
|
270
|
+
export function uninstallOpenCode({
|
|
271
|
+
configDir = defaultConfigDir(),
|
|
272
|
+
version = null,
|
|
273
|
+
dryRun = false,
|
|
274
|
+
removePackage: withPackage = true,
|
|
275
|
+
client = "auto",
|
|
276
|
+
} = {}) {
|
|
247
277
|
const dir = path.resolve(configDir)
|
|
248
278
|
const actions = []
|
|
249
279
|
const errors = []
|
|
@@ -262,7 +292,13 @@ export function uninstallOpenCode({ configDir = defaultConfigDir(), version = nu
|
|
|
262
292
|
}
|
|
263
293
|
}
|
|
264
294
|
|
|
265
|
-
const
|
|
295
|
+
const desktop = client === "opencode" ? { installed: false, exe: null, version: null } : detectDesktop()
|
|
296
|
+
const clientInfo = {
|
|
297
|
+
mode: client,
|
|
298
|
+
desktop: desktop.installed ? { exe: desktop.exe, version: desktop.version } : null,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const resolvedVersion = version ?? detectVersion() ?? desktop.version
|
|
266
302
|
const inferredMajor = config.plugin !== undefined ? 1 : 2
|
|
267
303
|
const major = resolvedVersion ? Number(resolvedVersion.split(".")[0]) : inferredMajor
|
|
268
304
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
@@ -280,7 +316,7 @@ export function uninstallOpenCode({ configDir = defaultConfigDir(), version = nu
|
|
|
280
316
|
act(`"${PACKAGE_NAME}" is not configured in plugins`)
|
|
281
317
|
}
|
|
282
318
|
act("restart OpenCode to finish removal")
|
|
283
|
-
return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
|
|
319
|
+
return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun, client: clientInfo }
|
|
284
320
|
}
|
|
285
321
|
|
|
286
322
|
const pluginFile = path.join(dir, "plugins", PLUGIN_FILE)
|
|
@@ -338,5 +374,5 @@ export function uninstallOpenCode({ configDir = defaultConfigDir(), version = nu
|
|
|
338
374
|
}
|
|
339
375
|
|
|
340
376
|
act("restart OpenCode to finish removal")
|
|
341
|
-
return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
|
|
377
|
+
return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun, client: clientInfo }
|
|
342
378
|
}
|