@kisev/skills-opencode 1.2.0 → 2.0.0
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 +134 -112
- package/README.ru.md +242 -0
- package/dist/agent-profiles.d.ts +1 -1
- package/dist/agent-profiles.js +18 -9
- package/dist/assets/agents/architect.md +67 -0
- package/{assets → dist/assets}/agents/critic.md +17 -2
- package/dist/assets/agents/manager.md +36 -0
- package/{assets → dist/assets}/agents/mapper.md +2 -2
- package/dist/assets/agents/review.md +25 -0
- package/{assets → dist/assets}/agents/worker.md +7 -5
- package/dist/assets/commands/agent-profiles.md +9 -0
- package/dist/assets/commands/agents-md.md +10 -0
- package/dist/assets/commands/askme.md +10 -0
- package/dist/assets/commands/ast-grep.md +10 -0
- package/dist/assets/commands/capabilities.md +9 -0
- package/dist/assets/commands/code-explain.md +10 -0
- package/dist/assets/commands/code-review.md +10 -0
- package/dist/assets/commands/commit-msg.md +10 -0
- package/dist/assets/commands/docs-prepare.md +10 -0
- package/dist/assets/commands/docs-review.md +10 -0
- package/dist/assets/commands/doctor.md +9 -0
- package/dist/assets/commands/doit.md +10 -0
- package/dist/assets/commands/goal.md +10 -0
- package/dist/assets/commands/humanize.md +10 -0
- package/dist/assets/commands/lsp-report.md +10 -0
- package/dist/assets/commands/mattermost.md +10 -0
- package/dist/assets/commands/mr-prepare.md +10 -0
- package/dist/assets/commands/reconcile.md +9 -0
- package/dist/assets/commands/release-prepare.md +10 -0
- package/dist/assets/commands/release-review.md +10 -0
- package/dist/assets/commands/rtk.md +10 -0
- package/dist/assets/commands/skill-improve.md +10 -0
- package/dist/assets/commands/slides-prompts-prepare.md +10 -0
- package/dist/assets/commands/spec-manage.md +10 -0
- package/dist/assets/commands/stopit.md +10 -0
- package/dist/assets/commands/summary.md +10 -0
- package/dist/assets/commands/task-prepare.md +10 -0
- package/dist/assets/commands/task-review.md +10 -0
- package/dist/assets/commands/task-triage.md +10 -0
- package/dist/assets/commands/team-retro.md +10 -0
- package/dist/assets/commands/team-roadmap.md +10 -0
- package/dist/assets/commands/team-sprint-close.md +10 -0
- package/dist/assets/commands/team-sprint-start.md +10 -0
- package/dist/assets/lsp-catalog.json +30 -0
- package/dist/assets/migration-inventory.json +294 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.js +38 -0
- package/dist/cli-output.d.ts +7 -0
- package/dist/cli-output.js +68 -1
- package/dist/cli.js +136 -10
- package/dist/contracts.d.ts +52 -0
- package/dist/contracts.js +98 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.js +595 -0
- package/dist/generate-assets.js +17 -2
- package/dist/index.d.ts +80 -56
- package/dist/index.js +162 -37
- package/dist/installer.d.ts +33 -5
- package/dist/installer.js +252 -28
- package/dist/lifecycle.d.ts +3 -0
- package/dist/lifecycle.js +49 -17
- package/dist/plugins/rtk.js +4 -1
- package/dist/reconcile.d.ts +43 -0
- package/dist/reconcile.js +507 -0
- package/dist/registry.d.ts +7 -4
- package/dist/registry.js +28 -88
- package/dist/routing.d.ts +39 -2
- package/dist/routing.js +339 -27
- package/dist/runtime/worktree.d.ts +33 -0
- package/dist/runtime/worktree.js +220 -0
- package/package.json +10 -26
- package/assets/agents/architect.md +0 -33
- package/assets/agents/manager.md +0 -82
- package/assets/agents/review.md +0 -25
- package/assets/commands/agent-list.md +0 -9
- package/assets/commands/agent-model-set.md +0 -9
- package/assets/commands/askme.md +0 -10
- package/assets/commands/ast-grep-rewrite.md +0 -10
- package/assets/commands/ast-grep-search.md +0 -10
- package/assets/commands/attempt-cancel.md +0 -10
- package/assets/commands/attempt-list.md +0 -10
- package/assets/commands/attempt-result.md +0 -10
- package/assets/commands/attempt-show.md +0 -10
- package/assets/commands/capabilities.md +0 -9
- package/assets/commands/code-review.md +0 -10
- package/assets/commands/commit-msg.md +0 -10
- package/assets/commands/critic-add.md +0 -9
- package/assets/commands/critic-remove.md +0 -9
- package/assets/commands/docs-prepare.md +0 -10
- package/assets/commands/docs-review.md +0 -10
- package/assets/commands/doctor.md +0 -9
- package/assets/commands/doit.md +0 -10
- package/assets/commands/goal-list.md +0 -10
- package/assets/commands/goal-pause.md +0 -10
- package/assets/commands/goal-prepare.md +0 -10
- package/assets/commands/goal-remove.md +0 -10
- package/assets/commands/goal-show.md +0 -10
- package/assets/commands/goal-start.md +0 -10
- package/assets/commands/lsp-report.md +0 -10
- package/assets/commands/mattermost.md +0 -10
- package/assets/commands/mr-prepare.md +0 -10
- package/assets/commands/mr-review.md +0 -10
- package/assets/commands/multi-run-cancel.md +0 -10
- package/assets/commands/multi-run-compare.md +0 -10
- package/assets/commands/multi-run-fusion.md +0 -10
- package/assets/commands/multi-run-start.md +0 -10
- package/assets/commands/multi-run-status.md +0 -10
- package/assets/commands/overview.md +0 -10
- package/assets/commands/release-prepare.md +0 -10
- package/assets/commands/release-review.md +0 -10
- package/assets/commands/route.md +0 -9
- package/assets/commands/schedule-add.md +0 -10
- package/assets/commands/schedule-disable.md +0 -10
- package/assets/commands/schedule-enable.md +0 -10
- package/assets/commands/schedule-list.md +0 -10
- package/assets/commands/schedule-remove.md +0 -10
- package/assets/commands/schedule-status.md +0 -10
- package/assets/commands/skill-improver.md +0 -10
- package/assets/commands/spec-audit.md +0 -10
- package/assets/commands/spec-init.md +0 -10
- package/assets/commands/spec-onboard.md +0 -10
- package/assets/commands/spec-update.md +0 -10
- package/assets/commands/stopit.md +0 -10
- package/assets/commands/summary.md +0 -10
- package/assets/commands/task-prepare-batch.md +0 -10
- package/assets/commands/task-prepare.md +0 -10
- package/assets/commands/task-review.md +0 -10
- package/assets/commands/task-triage.md +0 -10
- package/assets/commands/team-planning.md +0 -10
- package/assets/commands/team-retro.md +0 -10
- package/assets/commands/team-roadmap.md +0 -10
- package/assets/commands/team-slides-prompts.md +0 -10
- package/assets/commands/team-sprint-close.md +0 -10
- package/assets/commands/team-sprint-status.md +0 -10
- package/assets/commands/walkthrough.md +0 -10
- package/assets/plugins/autonomy-policy.js +0 -3
- package/assets/plugins/background-attempts.js +0 -3
- package/assets/plugins/goal-loop.js +0 -3
- package/assets/plugins/schedule.js +0 -3
- package/assets/plugins/zed-clickable-paths.js +0 -3
- package/dist/plugins/autonomy-policy.d.ts +0 -21
- package/dist/plugins/autonomy-policy.js +0 -71
- package/dist/plugins/background-attempts.d.ts +0 -76
- package/dist/plugins/background-attempts.js +0 -124
- package/dist/plugins/goal-loop.d.ts +0 -62
- package/dist/plugins/goal-loop.js +0 -99
- package/dist/plugins/schedule.d.ts +0 -36
- package/dist/plugins/schedule.js +0 -100
- package/dist/plugins/zed-clickable-paths.d.ts +0 -11
- package/dist/plugins/zed-clickable-paths.js +0 -10
- /package/{assets → dist/assets}/plugins/rtk.js +0 -0
- /package/{assets → dist/assets}/plugins/rules-injector.js +0 -0
- /package/{assets → dist/assets}/plugins/zed-bell.js +0 -0
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import { readFile, readdir, lstat, stat } from "node:fs/promises";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { CATALOG } from "./catalog.js";
|
|
10
|
+
import { listAgentProfiles } from "./agent-profiles.js";
|
|
11
|
+
import { archiveRoot, assertSafePath, deploymentRoot, lifecycleRoot, } from "./lifecycle.js";
|
|
12
|
+
import { inspectReconcile } from "./reconcile.js";
|
|
13
|
+
import { stateRoot } from "./runtime/state.js";
|
|
14
|
+
const run = promisify(execFile);
|
|
15
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
+
const packageName = "@kisev/skills-opencode";
|
|
17
|
+
const lspCatalog = JSON.parse(readFileSync(resolve(packageRoot, "dist", "assets", "lsp-catalog.json"), "utf8"));
|
|
18
|
+
function check(id, status, summary, evidence, remediation) {
|
|
19
|
+
return { id, status, summary, evidence, ...(remediation?.length ? { remediation } : {}) };
|
|
20
|
+
}
|
|
21
|
+
function errorText(error) {
|
|
22
|
+
const code = error && typeof error === "object" && "code" in error
|
|
23
|
+
? String(error.code)
|
|
24
|
+
: "error";
|
|
25
|
+
return code === "EACCES" || code === "EPERM" ? "permission denied" : code;
|
|
26
|
+
}
|
|
27
|
+
async function regular(path) {
|
|
28
|
+
try {
|
|
29
|
+
await assertSafePath(path);
|
|
30
|
+
const info = await lstat(path);
|
|
31
|
+
if (info.isSymbolicLink() || !info.isFile() || info.nlink !== 1)
|
|
32
|
+
return { status: "incomplete" };
|
|
33
|
+
return { raw: await readFile(path), status: "present" };
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === "ENOENT")
|
|
37
|
+
return { status: "missing" };
|
|
38
|
+
return { status: "incomplete" };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function directoryEntries(path) {
|
|
42
|
+
try {
|
|
43
|
+
await assertSafePath(path, { target: "directory" });
|
|
44
|
+
const info = await lstat(path);
|
|
45
|
+
if (info.isSymbolicLink() || !info.isDirectory())
|
|
46
|
+
return { files: [], incomplete: true };
|
|
47
|
+
const files = [];
|
|
48
|
+
let incomplete = false;
|
|
49
|
+
for (const entry of (await readdir(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
50
|
+
if (entry.isSymbolicLink()) {
|
|
51
|
+
incomplete = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (entry.isFile())
|
|
55
|
+
files.push(entry.name);
|
|
56
|
+
else if (entry.isDirectory()) {
|
|
57
|
+
const nested = await directoryEntries(join(path, entry.name));
|
|
58
|
+
files.push(...nested.files.map((name) => `${entry.name}/${name}`));
|
|
59
|
+
incomplete ||= nested.incomplete;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { files, incomplete };
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return { files: [], incomplete: errorText(error) !== "ENOENT" };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function json(raw) {
|
|
69
|
+
try {
|
|
70
|
+
const value = JSON.parse(raw.toString("utf8"));
|
|
71
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
72
|
+
? value
|
|
73
|
+
: undefined;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function packageVersion() {
|
|
80
|
+
try {
|
|
81
|
+
const value = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
|
|
82
|
+
return typeof value.version === "string" ? value.version : null;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function manifestVersion(raw) {
|
|
89
|
+
const value = raw && json(raw);
|
|
90
|
+
const files = value?.files;
|
|
91
|
+
const validFiles = files &&
|
|
92
|
+
typeof files === "object" &&
|
|
93
|
+
!Array.isArray(files) &&
|
|
94
|
+
Object.entries(files).every(([path, record]) => path.length > 0 &&
|
|
95
|
+
!path.startsWith("/") &&
|
|
96
|
+
!path.includes("\\") &&
|
|
97
|
+
!path.split("/").some((part) => part === "" || part === "." || part === "..") &&
|
|
98
|
+
record &&
|
|
99
|
+
typeof record === "object" &&
|
|
100
|
+
/^[a-f0-9]{64}$/.test(String(record.sha256 ?? "")));
|
|
101
|
+
return value &&
|
|
102
|
+
(value.schema_version === 1 || value.schema_version === 2) &&
|
|
103
|
+
value.package === packageName &&
|
|
104
|
+
typeof value.version === "string" &&
|
|
105
|
+
validFiles
|
|
106
|
+
? value.version
|
|
107
|
+
: null;
|
|
108
|
+
}
|
|
109
|
+
function agentManifestValid(raw) {
|
|
110
|
+
const value = raw && json(raw);
|
|
111
|
+
return Boolean(value &&
|
|
112
|
+
value.schema_version === 1 &&
|
|
113
|
+
value.package === packageName &&
|
|
114
|
+
typeof value.package_version === "string" &&
|
|
115
|
+
(value.scope === "global" || value.scope === "project") &&
|
|
116
|
+
Array.isArray(value.critic_pool) &&
|
|
117
|
+
value.profiles &&
|
|
118
|
+
typeof value.profiles === "object" &&
|
|
119
|
+
!Array.isArray(value.profiles));
|
|
120
|
+
}
|
|
121
|
+
function redactedConfig(value) {
|
|
122
|
+
const object = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
123
|
+
const rawPlugins = Array.isArray(object.plugin)
|
|
124
|
+
? object.plugin.flatMap((item) => {
|
|
125
|
+
if (typeof item === "string")
|
|
126
|
+
return [item];
|
|
127
|
+
if (Array.isArray(item) && typeof item[0] === "string")
|
|
128
|
+
return [item[0]];
|
|
129
|
+
return [];
|
|
130
|
+
})
|
|
131
|
+
: [];
|
|
132
|
+
const knownPlugins = new Set([packageName, ...CATALOG.plugins]);
|
|
133
|
+
const plugins = rawPlugins.filter((plugin) => knownPlugins.has(plugin));
|
|
134
|
+
const enabled = plugins.filter((plugin) => plugin === packageName ||
|
|
135
|
+
CATALOG.plugins.includes(plugin));
|
|
136
|
+
return {
|
|
137
|
+
plugins: plugins.sort(),
|
|
138
|
+
enabled_plugins: enabled.sort(),
|
|
139
|
+
disabled_plugins: CATALOG.plugins.filter((plugin) => !enabled.includes(plugin)),
|
|
140
|
+
unknown_plugins: rawPlugins.length - plugins.length,
|
|
141
|
+
keys: Object.keys(object)
|
|
142
|
+
.filter((key) => [
|
|
143
|
+
"$schema",
|
|
144
|
+
"theme",
|
|
145
|
+
"keybinds",
|
|
146
|
+
"logLevel",
|
|
147
|
+
"command",
|
|
148
|
+
"watcher",
|
|
149
|
+
"plugin",
|
|
150
|
+
"lsp",
|
|
151
|
+
"formatter",
|
|
152
|
+
"provider",
|
|
153
|
+
"agent",
|
|
154
|
+
"permission",
|
|
155
|
+
"compaction",
|
|
156
|
+
"model",
|
|
157
|
+
].includes(key))
|
|
158
|
+
.sort(),
|
|
159
|
+
source: "allowlisted-projection",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function localConfig(scope, cwd, home) {
|
|
163
|
+
const paths = scope === "project"
|
|
164
|
+
? [join(cwd, "opencode.json"), join(cwd, ".opencode", "opencode.json")]
|
|
165
|
+
: [join(home, ".config", "opencode", "opencode.json")];
|
|
166
|
+
for (const path of paths) {
|
|
167
|
+
const value = await regular(path);
|
|
168
|
+
if (value.status === "present") {
|
|
169
|
+
const parsed = json(value.raw);
|
|
170
|
+
return parsed
|
|
171
|
+
? { projection: redactedConfig(parsed), source: path }
|
|
172
|
+
: { incomplete: true, source: path };
|
|
173
|
+
}
|
|
174
|
+
if (value.status === "incomplete")
|
|
175
|
+
return { incomplete: true, source: path };
|
|
176
|
+
}
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
async function expectedAssets(group) {
|
|
180
|
+
try {
|
|
181
|
+
return (await readdir(join(packageRoot, "assets", group), { withFileTypes: true }))
|
|
182
|
+
.filter((entry) => entry.isFile())
|
|
183
|
+
.map((entry) => entry.name)
|
|
184
|
+
.sort();
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function filesInState(root) {
|
|
191
|
+
const result = await directoryEntries(root);
|
|
192
|
+
let malformed = 0;
|
|
193
|
+
let incomplete = result.incomplete;
|
|
194
|
+
for (const name of result.files.filter((value) => value.endsWith(".json") || value.endsWith(".jsonl"))) {
|
|
195
|
+
const value = await regular(join(root, name));
|
|
196
|
+
if (value.status === "incomplete") {
|
|
197
|
+
incomplete = true;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (value.status !== "present")
|
|
201
|
+
continue;
|
|
202
|
+
const lines = name.endsWith(".jsonl")
|
|
203
|
+
? value.raw.toString("utf8").split(/\r?\n/).filter(Boolean)
|
|
204
|
+
: [value.raw.toString("utf8")];
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
try {
|
|
207
|
+
JSON.parse(line);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
malformed += 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
count: result.files.filter((name) => name.endsWith(".json") || name.endsWith(".jsonl")).length,
|
|
216
|
+
malformed,
|
|
217
|
+
incomplete,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function diagnosticStateRoot(name, home) {
|
|
221
|
+
return home === homedir()
|
|
222
|
+
? stateRoot(name, home)
|
|
223
|
+
: join(home, ".local", "state", "opencode", "skills", name);
|
|
224
|
+
}
|
|
225
|
+
async function lifecycleArtifacts(root) {
|
|
226
|
+
const result = await filesInState(root);
|
|
227
|
+
const entries = await directoryEntries(root);
|
|
228
|
+
const files = entries.files.map((name) => name.toLowerCase());
|
|
229
|
+
return {
|
|
230
|
+
count: result.count,
|
|
231
|
+
locks: files.filter((name) => name.includes("lock")).length,
|
|
232
|
+
receipts: files.filter((name) => name.includes("receipt")).length,
|
|
233
|
+
journals: files.filter((name) => name.includes("journal")).length,
|
|
234
|
+
malformed: result.malformed,
|
|
235
|
+
incomplete: result.incomplete || entries.incomplete,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
async function executableAvailable(name) {
|
|
239
|
+
const pathValue = process.env.PATH ?? "";
|
|
240
|
+
for (const directory of pathValue.split(delimiter)) {
|
|
241
|
+
if (!directory)
|
|
242
|
+
continue;
|
|
243
|
+
try {
|
|
244
|
+
const info = await stat(join(directory, name));
|
|
245
|
+
if (info.isFile() && (info.mode & fsConstants.X_OK) !== 0)
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// A missing or inaccessible PATH entry is equivalent to an unavailable binary.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
async function lspFacts(project, host, partial) {
|
|
255
|
+
const suffixes = new Set();
|
|
256
|
+
let scanIncomplete = false;
|
|
257
|
+
const visit = async (root) => {
|
|
258
|
+
try {
|
|
259
|
+
const info = await lstat(root);
|
|
260
|
+
if (info.isSymbolicLink()) {
|
|
261
|
+
scanIncomplete = true;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (info.isFile()) {
|
|
265
|
+
suffixes.add(root.slice(root.lastIndexOf(".")).toLowerCase());
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (!info.isDirectory())
|
|
269
|
+
return;
|
|
270
|
+
for (const name of await readdir(root))
|
|
271
|
+
await visit(join(root, name));
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (error.code !== "ENOENT")
|
|
275
|
+
scanIncomplete = true;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
await visit(project);
|
|
279
|
+
if (scanIncomplete)
|
|
280
|
+
partial.push("lsp.project-files");
|
|
281
|
+
const disabled = process.env.OPENCODE_DISABLE_LSP_DOWNLOAD === "true";
|
|
282
|
+
const servers = await Promise.all(lspCatalog.servers.map(async (server) => {
|
|
283
|
+
let available = false;
|
|
284
|
+
available = await executableAvailable(server.executable);
|
|
285
|
+
const applicable = server.extensions.some((extension) => suffixes.has(extension));
|
|
286
|
+
return {
|
|
287
|
+
name: server.name,
|
|
288
|
+
applicable,
|
|
289
|
+
configured: applicable,
|
|
290
|
+
binary_available: available,
|
|
291
|
+
active: applicable && available && !disabled,
|
|
292
|
+
runtime_status: "unavailable",
|
|
293
|
+
requirement_class: server.requirement_class,
|
|
294
|
+
reason: !applicable
|
|
295
|
+
? "not-selected"
|
|
296
|
+
: disabled
|
|
297
|
+
? "download-disabled"
|
|
298
|
+
: !available
|
|
299
|
+
? "missing-dependency"
|
|
300
|
+
: "available",
|
|
301
|
+
missing: available ? [] : [server.executable],
|
|
302
|
+
install: `Install ${server.executable} with your project toolchain.`,
|
|
303
|
+
};
|
|
304
|
+
}));
|
|
305
|
+
let runtimeStatus = "unavailable";
|
|
306
|
+
if (host?.lsp) {
|
|
307
|
+
try {
|
|
308
|
+
const value = await host.lsp();
|
|
309
|
+
runtimeStatus = Array.isArray(value)
|
|
310
|
+
? value.map((item) => {
|
|
311
|
+
const entry = item && typeof item === "object" ? item : {};
|
|
312
|
+
return {
|
|
313
|
+
id: typeof entry.id === "string" ? entry.id : "unknown",
|
|
314
|
+
name: typeof entry.name === "string" ? entry.name : "unknown",
|
|
315
|
+
status: entry.status === "connected" || entry.status === "error" ? entry.status : "unknown",
|
|
316
|
+
};
|
|
317
|
+
})
|
|
318
|
+
: { status: "unavailable" };
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
partial.push("lsp.runtime-status");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else
|
|
325
|
+
partial.push("lsp.runtime-status");
|
|
326
|
+
return {
|
|
327
|
+
schema_version: 1,
|
|
328
|
+
catalog_version: lspCatalog.catalog_version,
|
|
329
|
+
download_disabled: disabled,
|
|
330
|
+
runtime_status: runtimeStatus,
|
|
331
|
+
servers,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function reconcileProjection(plan) {
|
|
335
|
+
const groups = [
|
|
336
|
+
"current",
|
|
337
|
+
"renamed",
|
|
338
|
+
"retired",
|
|
339
|
+
"archive-pending",
|
|
340
|
+
"modified_managed",
|
|
341
|
+
"user_owned",
|
|
342
|
+
"unknown",
|
|
343
|
+
"diagnostic_state_only",
|
|
344
|
+
];
|
|
345
|
+
const retired = Object.fromEntries(groups.map((name) => [
|
|
346
|
+
name,
|
|
347
|
+
plan[name].map((item) => ({
|
|
348
|
+
path: item.path,
|
|
349
|
+
status: item.status,
|
|
350
|
+
replacement: item.replacement ?? null,
|
|
351
|
+
})),
|
|
352
|
+
]));
|
|
353
|
+
const conflicts = plan.conflicts.map((item) => ({
|
|
354
|
+
path: item.path,
|
|
355
|
+
status: item.status,
|
|
356
|
+
reason: item.reason,
|
|
357
|
+
replacement: item.replacement ?? null,
|
|
358
|
+
}));
|
|
359
|
+
return { retired, conflicts };
|
|
360
|
+
}
|
|
361
|
+
async function archiveFacts(scope, cwd, home) {
|
|
362
|
+
const root = archiveRoot(scope, cwd, home);
|
|
363
|
+
const rootInfo = await lstat(root).catch((error) => {
|
|
364
|
+
if (error.code === "ENOENT")
|
|
365
|
+
return undefined;
|
|
366
|
+
return null;
|
|
367
|
+
});
|
|
368
|
+
if (rootInfo === null ||
|
|
369
|
+
(rootInfo &&
|
|
370
|
+
(!rootInfo.isDirectory() || rootInfo.isSymbolicLink() || (rootInfo.mode & 0o077) !== 0)))
|
|
371
|
+
return { path: root, entries: 0, valid: false };
|
|
372
|
+
const value = await regular(join(root, "index.json"));
|
|
373
|
+
if (value.status === "missing")
|
|
374
|
+
return { path: root, entries: 0, valid: true };
|
|
375
|
+
if (value.status !== "present")
|
|
376
|
+
return { path: root, entries: 0, valid: false };
|
|
377
|
+
const indexInfo = await lstat(join(root, "index.json")).catch(() => undefined);
|
|
378
|
+
if (!indexInfo || (indexInfo.mode & 0o777) !== 0o600)
|
|
379
|
+
return { path: root, entries: 0, valid: false };
|
|
380
|
+
try {
|
|
381
|
+
const parsed = JSON.parse(value.raw.toString("utf8"));
|
|
382
|
+
return {
|
|
383
|
+
path: root,
|
|
384
|
+
entries: parsed.schema_version === 1 && Array.isArray(parsed.entries) ? parsed.entries.length : 0,
|
|
385
|
+
valid: parsed.schema_version === 1 && Array.isArray(parsed.entries),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return { path: root, entries: 0, valid: false };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
export async function collectDoctorFacts(scope, cwd = process.cwd(), home = homedir(), host) {
|
|
393
|
+
const project = resolve(cwd);
|
|
394
|
+
const homeRoot = resolve(home);
|
|
395
|
+
const deployment = deploymentRoot(scope, project, homeRoot);
|
|
396
|
+
const lifecycle = lifecycleRoot(scope, project, homeRoot);
|
|
397
|
+
const checks = [];
|
|
398
|
+
const partial = [];
|
|
399
|
+
const unavailable = [];
|
|
400
|
+
let installedRaw;
|
|
401
|
+
const packageVersionValue = packageVersion();
|
|
402
|
+
checks.push(check("package.version", packageVersionValue ? "pass" : "fail", "Package metadata is readable", { package: packageName, version: packageVersionValue ?? "unavailable" }, ["Reinstall the package if metadata is missing."]));
|
|
403
|
+
checks.push(check("catalog.version", CATALOG.version === packageVersionValue ? "pass" : "fail", "Capability catalog matches package metadata", { catalog_version: CATALOG.version, package_version: packageVersionValue ?? "unavailable" }, ["Regenerate the package catalog from package metadata."]));
|
|
404
|
+
const manifest = await regular(join(deployment, ".skills-opencode-manifest.json"));
|
|
405
|
+
if (manifest.status === "present")
|
|
406
|
+
installedRaw = manifest.raw;
|
|
407
|
+
if (manifest.status === "incomplete")
|
|
408
|
+
partial.push("installed-manifest");
|
|
409
|
+
const installedVersion = manifestVersion(installedRaw);
|
|
410
|
+
checks.push(check("manifest.version", manifest.status === "missing"
|
|
411
|
+
? "incomplete"
|
|
412
|
+
: installedVersion === null
|
|
413
|
+
? "warn"
|
|
414
|
+
: installedVersion === packageVersionValue
|
|
415
|
+
? "pass"
|
|
416
|
+
: "warn", manifest.status === "present"
|
|
417
|
+
? "Installed manifest format and version are classified"
|
|
418
|
+
: "Installed manifest is not available", {
|
|
419
|
+
path: ".skills-opencode-manifest.json",
|
|
420
|
+
present: manifest.status === "present",
|
|
421
|
+
valid: installedVersion !== null,
|
|
422
|
+
version: installedVersion ?? "unavailable",
|
|
423
|
+
}, ["Run the installer preview for this scope."]));
|
|
424
|
+
const assetGroups = await Promise.all(["commands", "plugins", "agents"].map(async (group) => [
|
|
425
|
+
group,
|
|
426
|
+
await directoryEntries(join(deployment, group)),
|
|
427
|
+
await expectedAssets(group),
|
|
428
|
+
]));
|
|
429
|
+
for (const [group, entries, expected] of assetGroups) {
|
|
430
|
+
const missing = expected.filter((name) => !entries.files.includes(name));
|
|
431
|
+
const extra = entries.files.filter((name) => !expected.includes(name));
|
|
432
|
+
const status = entries.incomplete
|
|
433
|
+
? "incomplete"
|
|
434
|
+
: missing.length || (group !== "agents" && extra.length)
|
|
435
|
+
? "fail"
|
|
436
|
+
: "pass";
|
|
437
|
+
checks.push(check(`assets.${group}`, status, `${group} completeness and collisions are classified`, {
|
|
438
|
+
count: entries.files.length,
|
|
439
|
+
expected: expected.length,
|
|
440
|
+
missing,
|
|
441
|
+
collisions: group === "agents" ? [] : extra,
|
|
442
|
+
}, status === "fail"
|
|
443
|
+
? ["Run the installer preview and review ownership conflicts before applying."]
|
|
444
|
+
: entries.incomplete
|
|
445
|
+
? ["Remove symlinks or restore inaccessible asset paths."]
|
|
446
|
+
: undefined));
|
|
447
|
+
}
|
|
448
|
+
let inventory;
|
|
449
|
+
try {
|
|
450
|
+
inventory = await listAgentProfiles(scope, project, homeRoot);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
partial.push("agent-profiles");
|
|
454
|
+
}
|
|
455
|
+
const agentStatus = inventory
|
|
456
|
+
? inventory.collisions.length || inventory.drift.length
|
|
457
|
+
? "fail"
|
|
458
|
+
: "pass"
|
|
459
|
+
: "incomplete";
|
|
460
|
+
checks.push(check("agents.completeness-ownership-drift", agentStatus, "Agent profile inventory is classified", {
|
|
461
|
+
profiles: inventory?.profiles.length ?? 0,
|
|
462
|
+
collisions: inventory?.collisions.length ?? 0,
|
|
463
|
+
drift: inventory?.drift.length ?? 0,
|
|
464
|
+
user_owned: inventory?.user_owned.length ?? 0,
|
|
465
|
+
}, ["Review collisions and run agent reconcile only after explicit confirmation."]));
|
|
466
|
+
const config = await localConfig(scope, project, homeRoot);
|
|
467
|
+
if (config.incomplete)
|
|
468
|
+
partial.push("config.local");
|
|
469
|
+
let hostConfig;
|
|
470
|
+
if (host?.config) {
|
|
471
|
+
try {
|
|
472
|
+
hostConfig = redactedConfig(await host.config());
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
partial.push("config.resolved");
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else
|
|
479
|
+
unavailable.push("config.resolved");
|
|
480
|
+
checks.push(check("config.plugins", config.projection || hostConfig ? "pass" : "incomplete", "Config plugin projection is available", {
|
|
481
|
+
local: config.projection?.plugins ?? [],
|
|
482
|
+
local_enabled: config.projection?.enabled_plugins ?? [],
|
|
483
|
+
local_disabled: config.projection?.disabled_plugins ?? [...CATALOG.plugins],
|
|
484
|
+
local_unknown: config.projection?.unknown_plugins ?? 0,
|
|
485
|
+
resolved: hostConfig?.plugins ?? [],
|
|
486
|
+
resolved_enabled: hostConfig?.enabled_plugins ?? [],
|
|
487
|
+
resolved_disabled: hostConfig?.disabled_plugins ?? [...CATALOG.plugins],
|
|
488
|
+
resolved_unknown: hostConfig?.unknown_plugins ?? 0,
|
|
489
|
+
source: config.source ?? "host-or-file-unavailable",
|
|
490
|
+
}, ["Inspect OpenCode configuration without exposing secrets."]));
|
|
491
|
+
const profileManifest = await regular(join(deployment, ".skills-opencode", "agent-profiles.manifest.json"));
|
|
492
|
+
const profileManifestIsValid = agentManifestValid(profileManifest.raw);
|
|
493
|
+
checks.push(check("agents.manifest", profileManifest.status === "present"
|
|
494
|
+
? profileManifestIsValid
|
|
495
|
+
? "pass"
|
|
496
|
+
: "warn"
|
|
497
|
+
: profileManifest.status === "missing"
|
|
498
|
+
? "incomplete"
|
|
499
|
+
: "warn", "Agent profile manifest is classified", {
|
|
500
|
+
present: profileManifest.status === "present",
|
|
501
|
+
valid: profileManifestIsValid,
|
|
502
|
+
path: ".skills-opencode/agent-profiles.manifest.json",
|
|
503
|
+
}, ["Run the installer preview for this scope."]));
|
|
504
|
+
let reconcile;
|
|
505
|
+
try {
|
|
506
|
+
reconcile = await inspectReconcile(scope, project, homeRoot);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
partial.push("stage-8-reconcile");
|
|
510
|
+
}
|
|
511
|
+
const projected = reconcile ? reconcileProjection(reconcile) : { retired: {}, conflicts: [] };
|
|
512
|
+
const archive = await archiveFacts(scope, project, homeRoot);
|
|
513
|
+
checks.push(check("archive.index", archive.valid ? "pass" : "incomplete", "Private archive index is inspected without mutation", archive, archive.valid ? undefined : ["Repair or remove the invalid archive index manually."]));
|
|
514
|
+
checks.push(check("migration.stage-8", reconcile ? (projected.conflicts.length ? "fail" : "pass") : "incomplete", "Stage-8 inventory classifications are read-only", {
|
|
515
|
+
inventory_version: reconcile?.inventory_version ?? "unavailable",
|
|
516
|
+
retired: reconcile?.retired.length ?? 0,
|
|
517
|
+
conflicts: projected.conflicts.length,
|
|
518
|
+
}, ["Review retired or conflicting entries; do not use historical backup state."]));
|
|
519
|
+
const stateRoots = ["attempt", "schedule", "worktree", "goal", "multi-run"].map((name) => [name, diagnosticStateRoot(name, homeRoot)]);
|
|
520
|
+
for (const [name, root] of stateRoots) {
|
|
521
|
+
const value = await filesInState(root);
|
|
522
|
+
if (value.incomplete)
|
|
523
|
+
partial.push(`state.${name}`);
|
|
524
|
+
if (value.malformed)
|
|
525
|
+
partial.push(`state.${name}.malformed`);
|
|
526
|
+
checks.push(check(`state.${name}`, value.incomplete || value.malformed ? "incomplete" : "pass", `${name} state is inspected without recovery`, {
|
|
527
|
+
records: value.count,
|
|
528
|
+
malformed: value.malformed,
|
|
529
|
+
diagnostic_state_only: name === "goal" || name === "multi-run",
|
|
530
|
+
}, value.incomplete || value.malformed
|
|
531
|
+
? ["Repair permissions or malformed state manually; doctor never recovers it."]
|
|
532
|
+
: undefined));
|
|
533
|
+
}
|
|
534
|
+
const lifecycleFiles = await lifecycleArtifacts(lifecycle);
|
|
535
|
+
checks.push(check("lifecycle.artifacts", lifecycleFiles.incomplete || lifecycleFiles.malformed ? "incomplete" : "pass", "Lifecycle locks, receipts, and journals are inspected without mutation", {
|
|
536
|
+
records: lifecycleFiles.count,
|
|
537
|
+
locks: lifecycleFiles.locks,
|
|
538
|
+
receipts: lifecycleFiles.receipts,
|
|
539
|
+
journals: lifecycleFiles.journals,
|
|
540
|
+
malformed: lifecycleFiles.malformed,
|
|
541
|
+
recovery: false,
|
|
542
|
+
}, lifecycleFiles.incomplete
|
|
543
|
+
? ["Resolve inaccessible lifecycle state before applying a mutation."]
|
|
544
|
+
: undefined));
|
|
545
|
+
const tools = await Promise.all(["opencode", ...lspCatalog.servers.map((server) => server.executable)].map(async (name) => {
|
|
546
|
+
try {
|
|
547
|
+
const result = await run(name, ["--version"], { timeout: 1000 });
|
|
548
|
+
return {
|
|
549
|
+
name,
|
|
550
|
+
available: true,
|
|
551
|
+
version: result.stdout
|
|
552
|
+
.trim()
|
|
553
|
+
.split(/\s+/)
|
|
554
|
+
.find((value) => /\d+\.\d+/.test(value)) ?? null,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
return { name, available: false, version: null };
|
|
559
|
+
}
|
|
560
|
+
}));
|
|
561
|
+
checks.push(check("host.tools", tools.some((item) => item.name === "opencode" && item.available) ? "pass" : "incomplete", "External tool availability is reported", { tools: tools.map((item) => `${item.name}:${item.available ? "available" : "missing"}`) }, ["Install OpenCode or missing external tools when needed."]));
|
|
562
|
+
const lsp = await lspFacts(project, host, partial);
|
|
563
|
+
const counts = { pass: 0, warn: 0, fail: 0, incomplete: 0 };
|
|
564
|
+
for (const item of checks)
|
|
565
|
+
counts[item.status] += 1;
|
|
566
|
+
const status = counts.fail
|
|
567
|
+
? "problems"
|
|
568
|
+
: counts.incomplete || partial.length > 0 || unavailable.length > 0
|
|
569
|
+
? "incomplete"
|
|
570
|
+
: "clean";
|
|
571
|
+
return {
|
|
572
|
+
schema_version: 1,
|
|
573
|
+
status,
|
|
574
|
+
mutations: false,
|
|
575
|
+
scope,
|
|
576
|
+
roots: { project, deployment, lifecycle },
|
|
577
|
+
versions: {
|
|
578
|
+
package: packageVersionValue,
|
|
579
|
+
catalog: CATALOG.version,
|
|
580
|
+
installed_manifest: installedVersion,
|
|
581
|
+
opencode: tools.find((item) => item.name === "opencode")?.version ?? null,
|
|
582
|
+
},
|
|
583
|
+
checks,
|
|
584
|
+
counts,
|
|
585
|
+
partial: [...new Set(partial)].sort(),
|
|
586
|
+
unavailable: [...new Set(unavailable)].sort(),
|
|
587
|
+
lsp,
|
|
588
|
+
retired: projected.retired,
|
|
589
|
+
conflicts: projected.conflicts,
|
|
590
|
+
archive,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
export function doctorExitCode(report) {
|
|
594
|
+
return report.status === "clean" ? 0 : report.status === "problems" ? 1 : 2;
|
|
595
|
+
}
|
package/dist/generate-assets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
1
|
+
import { copyFile, cp, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { COMMAND_REGISTRY, renderCommand } from "./registry.js";
|
|
@@ -8,7 +8,7 @@ const rootIndex = arguments_.indexOf("--root");
|
|
|
8
8
|
if (rootIndex !== -1 && !arguments_[rootIndex + 1])
|
|
9
9
|
throw new Error("--root requires a path");
|
|
10
10
|
const commandsRoot = rootIndex === -1
|
|
11
|
-
? resolve(packageRoot, "assets", "commands")
|
|
11
|
+
? resolve(packageRoot, "dist", "assets", "commands")
|
|
12
12
|
: resolve(arguments_[rootIndex + 1] ?? "");
|
|
13
13
|
const check = arguments_.includes("--check");
|
|
14
14
|
const expectedNames = new Set(COMMAND_REGISTRY.map((command) => `${command.name}.md`));
|
|
@@ -37,3 +37,18 @@ for (const command of COMMAND_REGISTRY) {
|
|
|
37
37
|
await writeFile(destination, expected, "utf8");
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
if (rootIndex === -1) {
|
|
41
|
+
const catalog = resolve(packageRoot, "..", "..", "shared", "references", "lsp-catalog.json");
|
|
42
|
+
const destination = resolve(packageRoot, "dist", "assets", "lsp-catalog.json");
|
|
43
|
+
if (check) {
|
|
44
|
+
if ((await readFile(destination).catch(() => Buffer.alloc(0))).compare(await readFile(catalog)) !== 0)
|
|
45
|
+
throw new Error("generated asset drift: lsp-catalog.json");
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
49
|
+
await copyFile(catalog, destination);
|
|
50
|
+
await cp(resolve(packageRoot, "assets", "agents"), resolve(packageRoot, "dist", "assets", "agents"), { recursive: true });
|
|
51
|
+
await cp(resolve(packageRoot, "assets", "plugins"), resolve(packageRoot, "dist", "assets", "plugins"), { recursive: true });
|
|
52
|
+
await copyFile(resolve(packageRoot, "assets", "migration-inventory.json"), resolve(packageRoot, "dist", "assets", "migration-inventory.json"));
|
|
53
|
+
}
|
|
54
|
+
}
|