opencode-skills-doctor 0.9.4 → 0.9.7
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 -44
- package/SECURITY.md +1 -1
- package/SECURITY.zh-CN.md +1 -1
- package/docs/README.zh-CN.md +18 -26
- package/docs/REFERENCE.md +18 -27
- package/docs/assets/banner.svg +1 -1
- package/package.json +2 -2
- package/skills/dsh-skills-doctor/SKILL.md +2 -7
- package/src/audit.mjs +29 -0
- package/src/cli.mjs +3 -23
- package/src/discover.mjs +2 -0
- package/src/fix.mjs +35 -7
- package/src/i18n.mjs +2 -8
- package/src/install.mjs +8 -11
- package/src/lint.mjs +53 -32
- package/src/mcp.mjs +3 -13
- package/src/notify.mjs +62 -0
- package/src/parse.mjs +91 -4
- package/src/plan.mjs +49 -19
- package/src/plugin-v1.mjs +5 -12
- package/src/plugin.mjs +38 -35
- package/src/warning.mjs +3 -5
- package/src/html.mjs +0 -171
package/src/plugin-v1.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin"
|
|
2
|
-
import { auditSkills,
|
|
3
|
-
import { writeHtmlReport } from "./html.mjs"
|
|
2
|
+
import { auditSkills, formatChat } from "./audit.mjs"
|
|
4
3
|
import { budgetWarning } from "./warning.mjs"
|
|
5
4
|
import { loadConfig } from "./config.mjs"
|
|
6
5
|
import { checkForUpdate, resolveMode, toolVersion } from "./self-update.mjs"
|
|
@@ -33,27 +32,21 @@ export const SkillsDoctorPlugin = async ({ directory }) => ({
|
|
|
33
32
|
tool: {
|
|
34
33
|
skills_doctor: tool({
|
|
35
34
|
description:
|
|
36
|
-
"Audit installed agent skills
|
|
35
|
+
"Audit installed agent skills and return a short briefing to say to the user in the chat. Does not write a file.",
|
|
37
36
|
args: z.object({
|
|
38
37
|
directory: z.string().optional().describe("Directory to audit (defaults to the current project)"),
|
|
39
38
|
budget: z.number().optional().describe("Description token budget (default 1200)"),
|
|
40
|
-
html: z.string().optional().describe("Optional path; when set, a self-contained HTML report is written there"),
|
|
41
39
|
}),
|
|
42
40
|
execute: async (args) => {
|
|
43
41
|
const cwd = args.directory || directory || process.cwd()
|
|
44
|
-
const audit = auditSkills({ cwd, budget: typeof args.budget === "number" ? args.budget : null
|
|
45
|
-
|
|
46
|
-
if (typeof args.html === "string" && args.html.trim()) {
|
|
47
|
-
const file = writeHtmlReport(audit, args.html.trim(), { title: "skills-doctor report" })
|
|
48
|
-
markdown += `\n\nHTML report written to \`${file}\``
|
|
49
|
-
}
|
|
50
|
-
return markdown
|
|
42
|
+
const audit = auditSkills({ cwd, budget: typeof args.budget === "number" ? args.budget : null })
|
|
43
|
+
return formatChat(audit)
|
|
51
44
|
},
|
|
52
45
|
}),
|
|
53
46
|
},
|
|
54
47
|
"experimental.chat.system.transform": async (input, output) => {
|
|
55
48
|
const warning = budgetWarning({ cwd: directory, sessionID: input?.sessionID })
|
|
56
|
-
if (warning) output.system.push(warning)
|
|
49
|
+
if (warning) output.system.push(`Say this to the user in the chat. Do not write a file.\n\n${warning}`)
|
|
57
50
|
const note = await updateNote({ directory, sessionID: input?.sessionID })
|
|
58
51
|
if (note) output.system.push(note)
|
|
59
52
|
},
|
package/src/plugin.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin"
|
|
2
|
-
import { auditSkills,
|
|
3
|
-
import {
|
|
2
|
+
import { auditSkills, formatChat } from "./audit.mjs"
|
|
3
|
+
import { postChatNotice, chatNoticeVisible } from "./notify.mjs"
|
|
4
4
|
import { budgetWarning } from "./warning.mjs"
|
|
5
5
|
import { loadConfig } from "./config.mjs"
|
|
6
6
|
import { checkForUpdate, resolveMode, toolVersion } from "./self-update.mjs"
|
|
@@ -35,16 +35,12 @@ export default Plugin.define({
|
|
|
35
35
|
editor.namespace({ name: "skills", description: "OpenCode skill diagnostics" })
|
|
36
36
|
editor.add({
|
|
37
37
|
name: "doctor",
|
|
38
|
-
description: "Audit installed agent skills
|
|
38
|
+
description: "Audit installed agent skills and return a short briefing to say to the user in the chat. Does not write a file.",
|
|
39
39
|
input: {
|
|
40
40
|
type: "object",
|
|
41
41
|
properties: {
|
|
42
42
|
directory: { type: "string", description: "Directory to audit (defaults to the current project)" },
|
|
43
43
|
budget: { type: "number", description: "Description token budget (default 1200)" },
|
|
44
|
-
html: {
|
|
45
|
-
type: "string",
|
|
46
|
-
description: "Optional path; when set, a self-contained HTML report is written there",
|
|
47
|
-
},
|
|
48
44
|
},
|
|
49
45
|
additionalProperties: false,
|
|
50
46
|
},
|
|
@@ -52,13 +48,8 @@ export default Plugin.define({
|
|
|
52
48
|
execute: async (input) => {
|
|
53
49
|
const directory = (input && typeof input.directory === "string" && input.directory) || ctx.location.directory
|
|
54
50
|
const budget = input && typeof input.budget === "number" ? input.budget : null
|
|
55
|
-
const audit = auditSkills({ cwd: directory, budget
|
|
56
|
-
|
|
57
|
-
if (input && typeof input.html === "string" && input.html.trim()) {
|
|
58
|
-
const file = writeHtmlReport(audit, input.html.trim(), { title: "skills-doctor report" })
|
|
59
|
-
markdown += `\n\nHTML report written to \`${file}\``
|
|
60
|
-
}
|
|
61
|
-
return { content: markdown }
|
|
51
|
+
const audit = auditSkills({ cwd: directory, budget })
|
|
52
|
+
return { content: formatChat(audit) }
|
|
62
53
|
},
|
|
63
54
|
})
|
|
64
55
|
})
|
|
@@ -66,25 +57,15 @@ export default Plugin.define({
|
|
|
66
57
|
await ctx.command.transform((editor) => {
|
|
67
58
|
editor.add({
|
|
68
59
|
name: "skills-doctor",
|
|
69
|
-
description: "
|
|
70
|
-
execute: async ({ sessionID
|
|
71
|
-
await
|
|
72
|
-
...prompt,
|
|
73
|
-
sessionID,
|
|
74
|
-
text: "Run a skills audit with the skills_doctor tool, then summarize the top issues and propose concrete fixes.",
|
|
75
|
-
delivery,
|
|
76
|
-
})
|
|
77
|
-
},
|
|
78
|
-
})
|
|
79
|
-
editor.add({
|
|
80
|
-
name: "skills-report",
|
|
81
|
-
description: "Write a self-contained HTML skills report to a path you choose",
|
|
82
|
-
execute: async ({ sessionID, prompt, delivery }) => {
|
|
83
|
-
await ctx.session.prompt({
|
|
84
|
-
...prompt,
|
|
60
|
+
description: "Tell the user about skill problems in this chat",
|
|
61
|
+
execute: async ({ sessionID }) => {
|
|
62
|
+
await postChatNotice({
|
|
85
63
|
sessionID,
|
|
86
|
-
|
|
87
|
-
|
|
64
|
+
cwd: ctx.location.directory,
|
|
65
|
+
synthetic: (input) => ctx.session.synthetic(input),
|
|
66
|
+
resume: true,
|
|
67
|
+
force: true,
|
|
68
|
+
when: "always",
|
|
88
69
|
})
|
|
89
70
|
},
|
|
90
71
|
})
|
|
@@ -102,10 +83,32 @@ export default Plugin.define({
|
|
|
102
83
|
})
|
|
103
84
|
})
|
|
104
85
|
|
|
105
|
-
//
|
|
86
|
+
// 用户发消息时,把问题直接写进对话。resume 为 false,不额外叫醒模型。
|
|
87
|
+
// 没有 prompt hook 的旧宿主仍靠下面的 context 退路。
|
|
88
|
+
try {
|
|
89
|
+
await ctx.session.hook("prompt", async (event) => {
|
|
90
|
+
await postChatNotice({
|
|
91
|
+
sessionID: event.sessionID,
|
|
92
|
+
cwd: ctx.location.directory,
|
|
93
|
+
synthetic: (input) => ctx.session.synthetic(input),
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
} catch {
|
|
97
|
+
// 旧版本不认识这个 hook
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 通知已经出现在对话里时,不再把同一段话塞进系统提示。
|
|
101
|
+
// 老版本没有 synthetic 时,退回系统提示,让模型在回复里说出来。
|
|
106
102
|
await ctx.session.hook("context", async (event) => {
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
if (!chatNoticeVisible(ctx.location.directory, event.sessionID)) {
|
|
104
|
+
const warning = budgetWarning({ cwd: ctx.location.directory, sessionID: event.sessionID })
|
|
105
|
+
if (warning) {
|
|
106
|
+
event.system.push({
|
|
107
|
+
type: "text",
|
|
108
|
+
text: `Say this to the user in the chat. Do not write a file.\n\n${warning}`,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
109
112
|
const note = await updateNote({ directory: ctx.location.directory, sessionID: event.sessionID })
|
|
110
113
|
if (note) event.system.push({ type: "text", text: note })
|
|
111
114
|
})
|
package/src/warning.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { auditSkills } from "./audit.mjs"
|
|
1
|
+
import { auditSkills, formatChat } from "./audit.mjs"
|
|
2
2
|
|
|
3
3
|
const cache = new Map()
|
|
4
4
|
const warned = new Set()
|
|
@@ -15,12 +15,10 @@ export function budgetWarning({ cwd = process.cwd(), sessionID = "global", ttlMs
|
|
|
15
15
|
let entry = cache.get(cwd)
|
|
16
16
|
if (!entry || now - entry.at > ttlMs) {
|
|
17
17
|
const audit = auditSkills({ cwd, includeGlobal })
|
|
18
|
-
const
|
|
18
|
+
const notable = audit.findings.some((finding) => finding.severity === "error" || finding.severity === "warn")
|
|
19
19
|
entry = {
|
|
20
20
|
at: now,
|
|
21
|
-
text:
|
|
22
|
-
? `[skills-doctor] ${audit.summary.unique} skills use ~${audit.summary.advertisedTokens} tokens of descriptions per step (budget ${audit.budget}); ${audit.counts.errors} error(s), ${audit.counts.warnings} warning(s). Use the skills_doctor tool for details, then hide low-frequency skills with \`skills-doctor fix --hide <ids>\`.`
|
|
23
|
-
: null,
|
|
21
|
+
text: notable ? formatChat(audit) : null,
|
|
24
22
|
}
|
|
25
23
|
cache.set(cwd, entry)
|
|
26
24
|
}
|
package/src/html.mjs
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import { spawn } from "node:child_process"
|
|
4
|
-
import { planBudget } from "./plan.mjs"
|
|
5
|
-
|
|
6
|
-
function esc(value) {
|
|
7
|
-
return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char])
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const SEVERITY_LABEL = { error: "error", warn: "warn", info: "info" }
|
|
11
|
-
|
|
12
|
-
/** 生成自包含 HTML 报告(无外部资源,可直接分享/作 CI artifact)。 */
|
|
13
|
-
export function renderHtml(audit, { title = "skills-doctor report", now = new Date() } = {}) {
|
|
14
|
-
const summary = audit.summary
|
|
15
|
-
const counts = audit.counts
|
|
16
|
-
const budget = audit.budget
|
|
17
|
-
const plan = planBudget(audit)
|
|
18
|
-
const ratio = budget > 0 ? Math.min(1.5, summary.advertisedTokens / (budget * 2)) : 0
|
|
19
|
-
const barWidth = Math.min(100, Math.round(ratio * 100))
|
|
20
|
-
const barClass = summary.advertisedTokens > budget * 2 ? "over" : summary.advertisedTokens > budget ? "warn" : "ok"
|
|
21
|
-
|
|
22
|
-
const findings = audit.findings
|
|
23
|
-
.map(
|
|
24
|
-
(finding) => `<tr class="finding ${finding.severity}" data-severity="${finding.severity}">
|
|
25
|
-
<td><span class="badge ${finding.severity}">${SEVERITY_LABEL[finding.severity] ?? finding.severity}</span></td>
|
|
26
|
-
<td class="rule">${esc(finding.rule)}</td>
|
|
27
|
-
<td>${esc(finding.message)}</td>
|
|
28
|
-
</tr>`,
|
|
29
|
-
)
|
|
30
|
-
.join("\n")
|
|
31
|
-
|
|
32
|
-
const skills = audit.skills
|
|
33
|
-
.map(
|
|
34
|
-
(skill) => `<tr class="skill">
|
|
35
|
-
<td class="mono">${esc(skill.id)}</td>
|
|
36
|
-
<td>${esc(skill.source.scope)}</td>
|
|
37
|
-
<td>${skill.advertised ? "yes" : "no"}</td>
|
|
38
|
-
<td class="num">${skill.descriptionTokens}</td>
|
|
39
|
-
<td class="num">${skill.bodyTokens}</td>
|
|
40
|
-
<td class="desc">${esc(String(skill.description ?? "").slice(0, 80))}</td>
|
|
41
|
-
<td class="mono path">${esc(skill.file)}</td>
|
|
42
|
-
</tr>`,
|
|
43
|
-
)
|
|
44
|
-
.join("\n")
|
|
45
|
-
|
|
46
|
-
return `<!doctype html>
|
|
47
|
-
<html lang="en">
|
|
48
|
-
<head>
|
|
49
|
-
<meta charset="utf-8">
|
|
50
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
51
|
-
<title>${esc(title)}</title>
|
|
52
|
-
<style>
|
|
53
|
-
:root { color-scheme: light dark; --fg:#1f2328; --muted:#656d76; --bg:#fff; --card:#f6f8fa; --border:#d0d7de; --red:#cf222e; --yellow:#9a6700; --green:#1a7f37; --blue:#0969da; }
|
|
54
|
-
@media (prefers-color-scheme: dark) { :root { --fg:#e6edf3; --muted:#8d96a0; --bg:#0d1117; --card:#161b22; --border:#30363d; --red:#ff7b72; --yellow:#d29922; --green:#3fb950; --blue:#58a6ff; } }
|
|
55
|
-
* { box-sizing: border-box; }
|
|
56
|
-
body { margin:0; padding:32px; font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; color:var(--fg); background:var(--bg); }
|
|
57
|
-
h1 { margin:0 0 4px; font-size:24px; }
|
|
58
|
-
h2 { margin:32px 0 12px; font-size:18px; }
|
|
59
|
-
.sub { color:var(--muted); font-size:12px; }
|
|
60
|
-
.cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px; margin:20px 0; }
|
|
61
|
-
.card { background:var(--card); border:1px solid var(--border); border-radius:10px; padding:12px 14px; }
|
|
62
|
-
.card .k { color:var(--muted); font-size:12px; }
|
|
63
|
-
.card .v { font-size:22px; font-weight:600; margin-top:2px; }
|
|
64
|
-
.card.error .v { color:var(--red); } .card.warn .v { color:var(--yellow); } .card.info .v { color:var(--blue); }
|
|
65
|
-
.bar { height:10px; border-radius:6px; background:var(--card); border:1px solid var(--border); overflow:hidden; margin:8px 0 4px; }
|
|
66
|
-
.bar > span { display:block; height:100%; width:${barWidth}%; }
|
|
67
|
-
.bar > span.ok { background:var(--green); } .bar > span.warn { background:var(--yellow); } .bar > span.over { background:var(--red); }
|
|
68
|
-
table { width:100%; border-collapse:collapse; background:var(--bg); border:1px solid var(--border); border-radius:10px; overflow:hidden; }
|
|
69
|
-
th, td { text-align:left; padding:8px 10px; border-bottom:1px solid var(--border); vertical-align:top; }
|
|
70
|
-
th { background:var(--card); font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:.03em; }
|
|
71
|
-
tr:last-child td { border-bottom:none; }
|
|
72
|
-
.badge { display:inline-block; padding:1px 8px; border-radius:999px; font-size:11px; font-weight:600; border:1px solid currentColor; }
|
|
73
|
-
.badge.error { color:var(--red); } .badge.warn { color:var(--yellow); } .badge.info { color:var(--blue); }
|
|
74
|
-
.rule { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; white-space:nowrap; }
|
|
75
|
-
.mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; }
|
|
76
|
-
.path { color:var(--muted); word-break:break-all; }
|
|
77
|
-
.num { text-align:right; font-variant-numeric:tabular-nums; }
|
|
78
|
-
.toolbar { display:flex; gap:8px; flex-wrap:wrap; margin:12px 0; }
|
|
79
|
-
.toolbar button, .toolbar input { font:inherit; padding:6px 10px; border-radius:8px; border:1px solid var(--border); background:var(--card); color:var(--fg); }
|
|
80
|
-
.toolbar button.active { border-color:var(--blue); color:var(--blue); }
|
|
81
|
-
.empty { color:var(--muted); padding:16px; }
|
|
82
|
-
footer { margin-top:40px; color:var(--muted); font-size:12px; }
|
|
83
|
-
</style>
|
|
84
|
-
</head>
|
|
85
|
-
<body>
|
|
86
|
-
<h1>${esc(title)}</h1>
|
|
87
|
-
<div class="sub">generated ${esc(now.toISOString())} · ${esc(audit.cwd)}</div>
|
|
88
|
-
|
|
89
|
-
<div class="cards">
|
|
90
|
-
<div class="card"><div class="k">definitions</div><div class="v">${summary.total}</div></div>
|
|
91
|
-
<div class="card"><div class="k">unique IDs</div><div class="v">${summary.unique}</div></div>
|
|
92
|
-
<div class="card"><div class="k">advertised</div><div class="v">${summary.advertised}</div></div>
|
|
93
|
-
<div class="card"><div class="k">description tokens / step</div><div class="v">${summary.advertisedTokens}</div></div>
|
|
94
|
-
<div class="card"><div class="k">potential savings / step</div><div class="v">${plan.recommended.savings}</div></div>
|
|
95
|
-
<div class="card error"><div class="k">errors</div><div class="v">${counts.errors}</div></div>
|
|
96
|
-
<div class="card warn"><div class="k">warnings</div><div class="v">${counts.warnings}</div></div>
|
|
97
|
-
<div class="card info"><div class="k">info</div><div class="v">${counts.infos}</div></div>
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
<h2>Context budget</h2>
|
|
101
|
-
<div class="bar"><span class="${barClass}"></span></div>
|
|
102
|
-
<div class="sub">${summary.advertisedTokens} tokens vs budget ${budget}${summary.advertisedTokens > budget * 2 ? " (over 2x)" : summary.advertisedTokens > budget ? " (over)" : " (ok)"}</div>
|
|
103
|
-
<div class="sub">plan: trim ${plan.trim.count} + hide ${plan.recommended.hides.length} → ~${plan.recommended.final} tokens/step (saves ~${plan.recommended.savings}; ~${plan.recommended.stepsSavings} over ${plan.steps} steps)</div>
|
|
104
|
-
|
|
105
|
-
<h2>Findings (${audit.findings.length})</h2>
|
|
106
|
-
<div class="toolbar">
|
|
107
|
-
<button data-filter="all" class="active">all</button>
|
|
108
|
-
<button data-filter="error">error</button>
|
|
109
|
-
<button data-filter="warn">warn</button>
|
|
110
|
-
<button data-filter="info">info</button>
|
|
111
|
-
<input id="q" placeholder="filter text…" />
|
|
112
|
-
</div>
|
|
113
|
-
<table id="findings">
|
|
114
|
-
<thead><tr><th>severity</th><th>rule</th><th>message</th></tr></thead>
|
|
115
|
-
<tbody>
|
|
116
|
-
${findings || '<tr><td colspan="3" class="empty">No problems found.</td></tr>'}
|
|
117
|
-
</tbody>
|
|
118
|
-
</table>
|
|
119
|
-
|
|
120
|
-
<h2>Skills (${audit.skills.length})</h2>
|
|
121
|
-
<table>
|
|
122
|
-
<thead><tr><th>ID</th><th>scope</th><th>advertised</th><th>desc tokens</th><th>body tokens</th><th>description</th><th>file</th></tr></thead>
|
|
123
|
-
<tbody>
|
|
124
|
-
${skills}
|
|
125
|
-
</tbody>
|
|
126
|
-
</table>
|
|
127
|
-
|
|
128
|
-
<footer>skills-doctor · ${esc(audit.version ?? "")} · updates: re-run <code>skills-doctor check --html</code></footer>
|
|
129
|
-
|
|
130
|
-
<script>
|
|
131
|
-
const buttons = [...document.querySelectorAll("[data-filter]")]
|
|
132
|
-
const q = document.getElementById("q")
|
|
133
|
-
let active = "all"
|
|
134
|
-
function apply() {
|
|
135
|
-
const text = (q.value || "").toLowerCase()
|
|
136
|
-
for (const row of document.querySelectorAll("tr.finding")) {
|
|
137
|
-
const okSeverity = active === "all" || row.dataset.severity === active
|
|
138
|
-
const okText = !text || row.textContent.toLowerCase().includes(text)
|
|
139
|
-
row.style.display = okSeverity && okText ? "" : "none"
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
for (const button of buttons) {
|
|
143
|
-
button.addEventListener("click", () => {
|
|
144
|
-
active = button.dataset.filter
|
|
145
|
-
for (const other of buttons) other.classList.toggle("active", other === button)
|
|
146
|
-
apply()
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
q.addEventListener("input", apply)
|
|
150
|
-
</script>
|
|
151
|
-
</body>
|
|
152
|
-
</html>
|
|
153
|
-
`
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function writeHtmlReport(audit, file, options = {}) {
|
|
157
|
-
const target = path.resolve(file)
|
|
158
|
-
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
159
|
-
fs.writeFileSync(target, renderHtml(audit, options), "utf8")
|
|
160
|
-
return target
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** 用系统默认程序打开报告;失败时静默(调用方已经知道路径)。 */
|
|
164
|
-
export function openReport(file) {
|
|
165
|
-
const target = path.resolve(file)
|
|
166
|
-
try {
|
|
167
|
-
if (process.platform === "win32") spawn("cmd", ["/c", "start", "", target], { detached: true, stdio: "ignore" }).unref()
|
|
168
|
-
else if (process.platform === "darwin") spawn("open", [target], { detached: true, stdio: "ignore" }).unref()
|
|
169
|
-
else spawn("xdg-open", [target], { detached: true, stdio: "ignore" }).unref()
|
|
170
|
-
} catch {}
|
|
171
|
-
}
|