opencode-skills-doctor 0.3.15 → 0.4.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/src/refs.mjs ADDED
@@ -0,0 +1,40 @@
1
+ const URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:|#|\/|~)/i
2
+ const GLOB_LIKE = /[*?[\]{}]/
3
+
4
+ function cleanTarget(raw) {
5
+ let target = String(raw ?? "").trim()
6
+ if (!target) return null
7
+ if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1).trim()
8
+ target = target.split("#")[0].split("?")[0].trim()
9
+ if (!target) return null
10
+ return target
11
+ }
12
+
13
+ /**
14
+ * 从 SKILL.md 正文里提取「看起来是相对文件路径」的引用:
15
+ * - markdown 链接 [x](path)
16
+ * - 行内代码 `references/foo.md`
17
+ * 跳过 URL、绝对路径、~/、占位符、glob。
18
+ */
19
+ export function extractReferences(body) {
20
+ const refs = new Set()
21
+
22
+ const add = (raw) => {
23
+ let target = cleanTarget(raw)
24
+ if (!target) return
25
+ if (URL_LIKE.test(target)) return
26
+ if (GLOB_LIKE.test(target)) return
27
+ if (target.includes("<") || target.includes(">") || target.includes("${")) return
28
+ if (target.startsWith("$") || target.startsWith("-")) return
29
+ if (/\s/.test(target)) return
30
+
31
+ const looksLikePath = target.includes("/") || /\.(md|markdown|sh|bash|py|ts|js|mjs|cjs|json|ya?ml|toml|txt|svg|png|jpe?g|gif|webp|csv)$/i.test(target)
32
+ if (!looksLikePath) return
33
+ refs.add(target)
34
+ }
35
+
36
+ for (const match of String(body ?? "").matchAll(/\]\(\s*([^)\s]+)\s*\)/g)) add(match[1])
37
+ for (const match of String(body ?? "").matchAll(/`([^`\n]+)`/g)) add(match[1])
38
+
39
+ return [...refs]
40
+ }
@@ -0,0 +1,142 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import os from "node:os"
4
+ import { spawnSync, spawn } from "node:child_process"
5
+
6
+ export const REGISTRY_URL =
7
+ process.env.SKILLS_DOCTOR_REGISTRY_URL || "https://registry.npmjs.org/opencode-skills-doctor/latest"
8
+ export const RELEASES_URL = "https://github.com/wangyuchen5110/opencodeplugginprogram/releases/latest"
9
+
10
+ export const CACHE_DIR = path.join(os.homedir(), ".skills-doctor")
11
+ export const CACHE_FILE = path.join(CACHE_DIR, "self-update.json")
12
+ const DAY_MS = 24 * 60 * 60 * 1000
13
+
14
+ let cachedVersion = null
15
+ /** 当前安装版本(读取包内 package.json,插件/MCP 里也能用)。 */
16
+ export function toolVersion() {
17
+ if (cachedVersion) return cachedVersion
18
+ try {
19
+ const file = new URL("../package.json", import.meta.url)
20
+ cachedVersion = JSON.parse(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")).version ?? "0.0.0"
21
+ } catch {
22
+ cachedVersion = "0.0.0"
23
+ }
24
+ return cachedVersion
25
+ }
26
+
27
+ /** 语义化版本比较:只看 major.minor.patch,预发布版视为更低。 */
28
+ export function compareVersions(a, b) {
29
+ const parse = (value) => {
30
+ const [core, pre = ""] = String(value ?? "").trim().replace(/^v/i, "").split("-")
31
+ const numbers = core.split(".").map((part) => Number.parseInt(part, 10) || 0)
32
+ return { numbers: [numbers[0] ?? 0, numbers[1] ?? 0, numbers[2] ?? 0], pre }
33
+ }
34
+ const left = parse(a)
35
+ const right = parse(b)
36
+ for (let i = 0; i < 3; i++) {
37
+ if (left.numbers[i] !== right.numbers[i]) return left.numbers[i] > right.numbers[i] ? 1 : -1
38
+ }
39
+ if (left.pre === right.pre) return 0
40
+ if (!left.pre) return 1
41
+ if (!right.pre) return -1
42
+ return left.pre > right.pre ? 1 : -1
43
+ }
44
+
45
+ /** off | notify | open;环境变量 SKILLS_DOCTOR_UPDATE_CHECK 可以强制关闭/开启。 */
46
+ export function resolveMode(config = {}, env = process.env) {
47
+ const envValue = String(env.SKILLS_DOCTOR_UPDATE_CHECK ?? "").trim().toLowerCase()
48
+ if (["0", "false", "off", "no"].includes(envValue)) return "off"
49
+ if (["1", "true", "on", "yes"].includes(envValue)) return "notify"
50
+
51
+ const value = config.checkUpdates ?? config.updateCheck
52
+ if (value === false) return "off"
53
+ if (value === true) return "notify"
54
+ if (value === "off" || value === "notify" || value === "open") return value
55
+ return "notify"
56
+ }
57
+
58
+ export async function fetchLatest({ fetchImpl = globalThis.fetch, url = REGISTRY_URL, timeoutMs = 4000 } = {}) {
59
+ if (typeof fetchImpl !== "function") throw new Error("fetch is not available in this Node runtime")
60
+ const controller = new AbortController()
61
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
62
+ try {
63
+ const response = await fetchImpl(url, { signal: controller.signal, headers: { accept: "application/json" } })
64
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
65
+ const data = await response.json()
66
+ const version = String(data?.version ?? "").trim()
67
+ if (!version) throw new Error("missing version")
68
+ return { version, url: RELEASES_URL }
69
+ } finally {
70
+ clearTimeout(timer)
71
+ }
72
+ }
73
+
74
+ function readCache(file) {
75
+ try {
76
+ return JSON.parse(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, ""))
77
+ } catch {
78
+ return null
79
+ }
80
+ }
81
+
82
+ function writeCache(file, value) {
83
+ try {
84
+ fs.mkdirSync(path.dirname(file), { recursive: true })
85
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", "utf8")
86
+ } catch {}
87
+ }
88
+
89
+ /**
90
+ * 检查工具自身是否有新版本。
91
+ * 返回 null(没有/关闭/失败)或 { latest, url, current, checkedAt, cached }。
92
+ */
93
+ export async function checkForUpdate({
94
+ current,
95
+ mode = "notify",
96
+ ttlMs = DAY_MS,
97
+ now = Date.now(),
98
+ fetchImpl,
99
+ cacheFile = CACHE_FILE,
100
+ force = false,
101
+ } = {}) {
102
+ if (mode === "off" || !current) return null
103
+ const cache = readCache(cacheFile)
104
+
105
+ if (!force && cache && cache.current === current && Number.isFinite(cache.checkedAt) && now - cache.checkedAt < ttlMs) {
106
+ if (cache.latest && compareVersions(cache.latest, current) > 0) return { ...cache, cached: true }
107
+ return null
108
+ }
109
+
110
+ try {
111
+ const latest = await fetchLatest({ fetchImpl })
112
+ writeCache(cacheFile, { current, latest: latest.version, url: latest.url, checkedAt: now })
113
+ if (compareVersions(latest.version, current) > 0) {
114
+ return { latest: latest.version, url: latest.url, current, checkedAt: now, cached: false }
115
+ }
116
+ return null
117
+ } catch {
118
+ return null
119
+ }
120
+ }
121
+
122
+ export function upgradeCommand() {
123
+ return "npm install -g opencode-skills-doctor@latest"
124
+ }
125
+
126
+ /** 真正执行全局升级;调用方负责确认(CLI 的 --apply)。 */
127
+ export function applyUpgrade({ spawnImpl = spawnSync, npm = process.platform === "win32" ? "npm.cmd" : "npm" } = {}) {
128
+ const result = spawnImpl(npm, ["install", "-g", "opencode-skills-doctor@latest", "--no-audit", "--no-fund"], {
129
+ stdio: "inherit",
130
+ env: process.env,
131
+ timeout: 300000,
132
+ })
133
+ return result.status === 0
134
+ }
135
+
136
+ export function openReleasePage({ url = RELEASES_URL, spawnImpl = spawn } = {}) {
137
+ try {
138
+ if (process.platform === "win32") spawnImpl("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref()
139
+ else if (process.platform === "darwin") spawnImpl("open", [url], { detached: true, stdio: "ignore" }).unref()
140
+ else spawnImpl("xdg-open", [url], { detached: true, stdio: "ignore" }).unref()
141
+ } catch {}
142
+ }