kanbot 0.4.0 → 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/package.json +1 -1
- package/src/color.ts +16 -0
- package/src/commands/doctor.ts +127 -14
package/package.json
CHANGED
package/src/color.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// ANSI colors for TTY output. Off when piped or NO_COLOR is set; FORCE_COLOR turns them on.
|
|
2
|
+
const on =
|
|
3
|
+
!process.env.NO_COLOR &&
|
|
4
|
+
(!!process.env.FORCE_COLOR || !!process.stdout.isTTY);
|
|
5
|
+
|
|
6
|
+
const wrap = (open: number, close: number) => (s: string) =>
|
|
7
|
+
on ? `\x1b[${open}m${s}\x1b[${close}m` : s;
|
|
8
|
+
|
|
9
|
+
export const color = {
|
|
10
|
+
bold: wrap(1, 22),
|
|
11
|
+
dim: wrap(2, 22),
|
|
12
|
+
red: wrap(31, 39),
|
|
13
|
+
green: wrap(32, 39),
|
|
14
|
+
yellow: wrap(33, 39),
|
|
15
|
+
cyan: wrap(36, 39),
|
|
16
|
+
};
|
package/src/commands/doctor.ts
CHANGED
|
@@ -4,9 +4,10 @@ import { defineCommand } from "citty";
|
|
|
4
4
|
import { loadAgents, missingSkills } from "../agents/load.ts";
|
|
5
5
|
import { sync } from "../agents/sync.ts";
|
|
6
6
|
import { capabilitySummary, findCapability } from "../capabilities.ts";
|
|
7
|
+
import { color } from "../color.ts";
|
|
7
8
|
import { loadConfig, writeConfigSchema } from "../config.ts";
|
|
8
9
|
import { git } from "../daemon/git.ts";
|
|
9
|
-
import { describeError } from "../errors.ts";
|
|
10
|
+
import { DOCS_URL, describeError } from "../errors.ts";
|
|
10
11
|
import { copilotReviewRule } from "../github/api.ts";
|
|
11
12
|
import { appKeyPath, appTokens } from "../github/app.ts";
|
|
12
13
|
import { Gh, run } from "../github/gh.ts";
|
|
@@ -29,7 +30,11 @@ type Check = {
|
|
|
29
30
|
/** Shown with a warning mark; never fails doctor. */
|
|
30
31
|
warn?: boolean;
|
|
31
32
|
detail?: string;
|
|
33
|
+
/** Why it matters. Shown only when the check fails or warns. */
|
|
34
|
+
why?: string;
|
|
32
35
|
fix?: string;
|
|
36
|
+
/** Docs page and anchor, e.g. "configuration#config-file". */
|
|
37
|
+
docs?: string;
|
|
33
38
|
};
|
|
34
39
|
|
|
35
40
|
async function cmd(args: string[]) {
|
|
@@ -45,13 +50,69 @@ async function cmd(args: string[]) {
|
|
|
45
50
|
/** Failure detail and fix for a check, from a thrown error. */
|
|
46
51
|
function explain(e: unknown, fallback?: string) {
|
|
47
52
|
const d = describeError(e);
|
|
48
|
-
const detail = [d.message, d.why].filter(Boolean).join("\n");
|
|
49
53
|
return {
|
|
50
|
-
detail:
|
|
54
|
+
detail: d.message,
|
|
55
|
+
why: d.why,
|
|
51
56
|
fix: d.fix ?? fallback,
|
|
57
|
+
docs: d.docs,
|
|
52
58
|
};
|
|
53
59
|
}
|
|
54
60
|
|
|
61
|
+
const indent = (s: string, pad: string) => s.replace(/^(?=.)/gm, pad);
|
|
62
|
+
|
|
63
|
+
function print(c: Check) {
|
|
64
|
+
const state = c.warn ? "warn" : c.ok ? "ok" : "fail";
|
|
65
|
+
const mark = {
|
|
66
|
+
ok: color.green("✔"),
|
|
67
|
+
warn: color.yellow("!"),
|
|
68
|
+
fail: color.red("✖"),
|
|
69
|
+
}[state];
|
|
70
|
+
if (state === "ok") {
|
|
71
|
+
const detail = c.detail ? color.dim(` — ${c.detail}`) : "";
|
|
72
|
+
console.log(` ${mark} ${c.name}${detail}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const paint = state === "warn" ? color.yellow : color.red;
|
|
76
|
+
const lines = [` ${mark} ${paint(c.name)}`];
|
|
77
|
+
if (c.detail) lines.push(indent(c.detail, " "));
|
|
78
|
+
if (c.why) lines.push(color.dim(indent(c.why, " ")));
|
|
79
|
+
if (c.fix) lines.push(` ${color.bold("Fix:")} ${color.cyan(c.fix)}`);
|
|
80
|
+
if (c.docs)
|
|
81
|
+
lines.push(
|
|
82
|
+
` ${color.bold("Docs:")} ${color.dim(`${DOCS_URL}/${c.docs}`)}`,
|
|
83
|
+
);
|
|
84
|
+
console.log(lines.join("\n"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function section(title: string) {
|
|
88
|
+
console.log(`\n${color.bold(title)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Print totals and next step, then exit 1 if anything failed. */
|
|
92
|
+
function summary(checks: Check[]) {
|
|
93
|
+
const failed = checks.filter((c) => !c.ok).length;
|
|
94
|
+
const warned = checks.filter((c) => c.ok && c.warn).length;
|
|
95
|
+
const passed = checks.length - failed - warned;
|
|
96
|
+
const parts = [color.green(`${passed} passed`)];
|
|
97
|
+
if (warned)
|
|
98
|
+
parts.push(
|
|
99
|
+
color.yellow(`${warned} ${warned === 1 ? "warning" : "warnings"}`),
|
|
100
|
+
);
|
|
101
|
+
if (failed) parts.push(color.red(`${failed} failed`));
|
|
102
|
+
console.log(`\n${parts.join(color.dim(" · "))}`);
|
|
103
|
+
if (failed) {
|
|
104
|
+
console.log(
|
|
105
|
+
`Fix the ${color.red("✖")} items above, top to bottom, then run ${color.cyan("kanbot doctor")} again.`,
|
|
106
|
+
);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
if (warned)
|
|
110
|
+
console.log(
|
|
111
|
+
`Ready to run. The ${color.yellow("!")} items are worth a look, but won't stop kanbot.`,
|
|
112
|
+
);
|
|
113
|
+
else console.log(`All set. Start kanbot with ${color.cyan("kanbot run")}.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
55
116
|
export default defineCommand({
|
|
56
117
|
meta: {
|
|
57
118
|
name: "doctor",
|
|
@@ -62,19 +123,32 @@ export default defineCommand({
|
|
|
62
123
|
const checks: Check[] = [];
|
|
63
124
|
const add = (c: Check) => {
|
|
64
125
|
checks.push(c);
|
|
65
|
-
|
|
66
|
-
`${c.warn ? "!" : c.ok ? "✔" : "✖"} ${c.name}${c.detail ? ` — ${c.detail}` : ""}${!c.ok && c.fix ? `\n fix: ${c.fix}` : ""}`,
|
|
67
|
-
);
|
|
126
|
+
print(c);
|
|
68
127
|
};
|
|
69
128
|
|
|
129
|
+
console.log(
|
|
130
|
+
`${color.bold("kanbot doctor")} ${color.dim("checks that everything kanbot needs is in place.")}`,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
section("GitHub CLI");
|
|
70
134
|
const auth = await cmd(["gh", "auth", "status"]);
|
|
71
|
-
add({
|
|
135
|
+
add({
|
|
136
|
+
name: "gh logged in",
|
|
137
|
+
ok: auth.ok,
|
|
138
|
+
why: "kanbot talks to GitHub through gh.",
|
|
139
|
+
fix: "gh auth login",
|
|
140
|
+
docs: "getting-started#requirements",
|
|
141
|
+
});
|
|
72
142
|
add({
|
|
73
143
|
name: "gh token has project scope",
|
|
74
144
|
ok: /'project'/.test(auth.out),
|
|
145
|
+
why: "Needed to read and move cards on the project board.",
|
|
75
146
|
fix: "gh auth refresh -s project",
|
|
147
|
+
docs: "getting-started#requirements",
|
|
76
148
|
});
|
|
77
149
|
|
|
150
|
+
section("Config");
|
|
151
|
+
|
|
78
152
|
let config: Awaited<ReturnType<typeof loadConfig>>;
|
|
79
153
|
try {
|
|
80
154
|
config = await loadConfig(root);
|
|
@@ -85,27 +159,34 @@ export default defineCommand({
|
|
|
85
159
|
});
|
|
86
160
|
} catch (e) {
|
|
87
161
|
add({ name: "config", ok: false, ...explain(e, "kanbot init") });
|
|
88
|
-
|
|
162
|
+
console.log(
|
|
163
|
+
color.dim("\n The other checks need a config, so doctor stops here."),
|
|
164
|
+
);
|
|
165
|
+
return summary(checks);
|
|
89
166
|
}
|
|
90
167
|
|
|
91
168
|
if (config.provider === "claude") {
|
|
169
|
+
section("Claude Code");
|
|
92
170
|
const s = await cmd(["claude", "auth", "status"]);
|
|
93
171
|
const sub = /"authMethod":\s*"claude\.ai"/.test(s.out);
|
|
94
172
|
add({
|
|
95
173
|
name: "claude CLI logged in with a subscription",
|
|
96
174
|
ok: s.ok && sub,
|
|
97
|
-
detail: sub
|
|
98
|
-
|
|
99
|
-
: "not using a claude.ai login (would bill API usage)",
|
|
175
|
+
detail: sub ? undefined : "Not using a claude.ai login.",
|
|
176
|
+
why: "Workers run `claude`. An API key login bills per token instead of your plan.",
|
|
100
177
|
fix: "claude auth login",
|
|
178
|
+
docs: "providers#claude-code",
|
|
101
179
|
});
|
|
102
180
|
} else {
|
|
181
|
+
section("Codex");
|
|
103
182
|
const s = await cmd(["codex", "login", "status"]);
|
|
104
183
|
add({
|
|
105
184
|
name: "codex CLI logged in with ChatGPT",
|
|
106
185
|
ok: s.ok && /ChatGPT/i.test(s.out),
|
|
107
186
|
detail: s.out.split("\n")[0],
|
|
187
|
+
why: "Workers run `codex` on your ChatGPT plan.",
|
|
108
188
|
fix: "codex login",
|
|
189
|
+
docs: "providers#codex",
|
|
109
190
|
});
|
|
110
191
|
add({
|
|
111
192
|
name: "codex workers ignore ~/.codex/config.toml",
|
|
@@ -115,6 +196,7 @@ export default defineCommand({
|
|
|
115
196
|
});
|
|
116
197
|
}
|
|
117
198
|
|
|
199
|
+
section("Agents and skills");
|
|
118
200
|
try {
|
|
119
201
|
const agents = await loadAgents(root);
|
|
120
202
|
add({
|
|
@@ -137,6 +219,7 @@ export default defineCommand({
|
|
|
137
219
|
name: "agents referenced by config exist",
|
|
138
220
|
ok: !missing.length,
|
|
139
221
|
detail: missing.join(", ") || undefined,
|
|
222
|
+
why: "Columns hand work to these agents, but .kanbot/agents has no file for them.",
|
|
140
223
|
fix: "kanbot upgrade",
|
|
141
224
|
});
|
|
142
225
|
if (config.implementers)
|
|
@@ -153,6 +236,7 @@ export default defineCommand({
|
|
|
153
236
|
name: "skills referenced by agents exist",
|
|
154
237
|
ok: !noSkill.length,
|
|
155
238
|
detail: noSkill.join(", ") || undefined,
|
|
239
|
+
why: "Agents list these skills, but .agents/skills has none by that name.",
|
|
156
240
|
fix: "add them to .agents/skills or remove them from the agent's `skills`",
|
|
157
241
|
});
|
|
158
242
|
add({
|
|
@@ -171,6 +255,9 @@ export default defineCommand({
|
|
|
171
255
|
detail: found
|
|
172
256
|
? `${found} agent, works on ${found} only`
|
|
173
257
|
: "not found",
|
|
258
|
+
why: found
|
|
259
|
+
? undefined
|
|
260
|
+
: "Jobs that need this capability can't load it.",
|
|
174
261
|
fix: "add the skill to .agents/skills or the agent to .kanbot/agents",
|
|
175
262
|
});
|
|
176
263
|
}
|
|
@@ -179,12 +266,15 @@ export default defineCommand({
|
|
|
179
266
|
name: "generated agent files up to date",
|
|
180
267
|
ok: !stale.length,
|
|
181
268
|
detail: stale.join(", ") || undefined,
|
|
269
|
+
why: "Workers read .claude/agents and .codex/agents, which are generated from .kanbot/agents.",
|
|
182
270
|
fix: "kanbot sync",
|
|
183
271
|
});
|
|
184
272
|
add({
|
|
185
273
|
name: "config schema up to date",
|
|
186
274
|
ok: !(await writeConfigSchema(root, { check: true })),
|
|
275
|
+
why: "Editors use it for autocomplete and errors in config.yml.",
|
|
187
276
|
fix: "kanbot sync",
|
|
277
|
+
docs: "configuration",
|
|
188
278
|
});
|
|
189
279
|
} catch (e) {
|
|
190
280
|
add({ name: "agents", ok: false, ...explain(e) });
|
|
@@ -208,6 +298,7 @@ export default defineCommand({
|
|
|
208
298
|
.join("; "),
|
|
209
299
|
});
|
|
210
300
|
|
|
301
|
+
section("Templates");
|
|
211
302
|
const plan = await planUpgrade(root);
|
|
212
303
|
const behind = plan.changes.filter((c) =>
|
|
213
304
|
["add", "update", "conflict"].includes(c.action),
|
|
@@ -218,16 +309,21 @@ export default defineCommand({
|
|
|
218
309
|
detail: behind.length
|
|
219
310
|
? `${behind.length} file(s) differ from this kanbot version`
|
|
220
311
|
: undefined,
|
|
312
|
+
why: "You get fixes to agents, skills and workflows by upgrading.",
|
|
221
313
|
fix: "kanbot upgrade",
|
|
314
|
+
docs: "upgrading",
|
|
222
315
|
});
|
|
223
316
|
const merges = await pendingMerges(root);
|
|
224
317
|
add({
|
|
225
318
|
name: `no unmerged ${NEW_SUFFIX} files`,
|
|
226
319
|
ok: !merges.length,
|
|
227
320
|
detail: merges.join(", ") || undefined,
|
|
321
|
+
why: "upgrade put the new version next to files you changed.",
|
|
322
|
+
docs: "upgrading",
|
|
228
323
|
fix: `merge each into the file next to it and delete it (kanbot-admin skill can help)`,
|
|
229
324
|
});
|
|
230
325
|
|
|
326
|
+
section(`Pushed to origin/${config.baseBranch}`);
|
|
231
327
|
await git(root, "fetch", "origin", config.baseBranch).catch(() => {});
|
|
232
328
|
const tree = await git(
|
|
233
329
|
root,
|
|
@@ -245,15 +341,18 @@ export default defineCommand({
|
|
|
245
341
|
add({
|
|
246
342
|
name: `${path} committed on origin/${config.baseBranch}`,
|
|
247
343
|
ok: tree.split("\n").some((f) => f.startsWith(path)),
|
|
248
|
-
|
|
344
|
+
why: "Workers run in worktrees made from the base branch, so they only see what is pushed.",
|
|
249
345
|
fix: "commit and push the kanbot files",
|
|
250
346
|
});
|
|
251
347
|
}
|
|
252
348
|
add({
|
|
253
349
|
name: ".github/workflows/kanbot-merged.yml",
|
|
254
350
|
ok: existsSync(join(root, ".github/workflows/kanbot-merged.yml")),
|
|
351
|
+
why: "Closes the other approaches' PRs when one is merged.",
|
|
352
|
+
fix: "kanbot upgrade",
|
|
255
353
|
});
|
|
256
354
|
|
|
355
|
+
section("Project board");
|
|
257
356
|
const gh = new Gh(config.repo);
|
|
258
357
|
try {
|
|
259
358
|
const board = await loadBoard(gh, config);
|
|
@@ -264,7 +363,11 @@ export default defineCommand({
|
|
|
264
363
|
detail: missing.length
|
|
265
364
|
? `missing columns: ${missing.join(", ")}`
|
|
266
365
|
: board.url,
|
|
366
|
+
why: missing.length
|
|
367
|
+
? "kanbot moves cards by the column names in config.yml."
|
|
368
|
+
: undefined,
|
|
267
369
|
fix: "kanbot board create",
|
|
370
|
+
docs: "getting-started#create-the-board",
|
|
268
371
|
});
|
|
269
372
|
// Moving cards to Done on close/merge and adding new issues are left to GitHub.
|
|
270
373
|
const workflows = await boardWorkflows(gh, board);
|
|
@@ -276,6 +379,7 @@ export default defineCommand({
|
|
|
276
379
|
add({
|
|
277
380
|
name: `board workflow "${name}" enabled`,
|
|
278
381
|
ok: workflows.some((w) => w.name === name && w.enabled),
|
|
382
|
+
why: "kanbot leaves adding new issues and moving closed ones to Done to GitHub.",
|
|
279
383
|
fix: `enable it in ${board.url} → ⋯ → Workflows`,
|
|
280
384
|
});
|
|
281
385
|
}
|
|
@@ -307,16 +411,21 @@ export default defineCommand({
|
|
|
307
411
|
add({
|
|
308
412
|
name: "labels",
|
|
309
413
|
ok: !missingLabels.length,
|
|
310
|
-
detail: missingLabels.
|
|
414
|
+
detail: missingLabels.length
|
|
415
|
+
? `missing: ${missingLabels.join(", ")}`
|
|
416
|
+
: undefined,
|
|
417
|
+
why: "kanbot tags issues and PRs with these labels.",
|
|
311
418
|
fix: "kanbot board create",
|
|
312
419
|
});
|
|
313
420
|
|
|
314
421
|
const { mode, app } = config.identity;
|
|
315
422
|
if (mode === "app") {
|
|
423
|
+
section("GitHub App");
|
|
316
424
|
add({
|
|
317
425
|
name: "kanbot app set",
|
|
318
426
|
ok: app !== null,
|
|
319
427
|
detail: app ? `${app.slug} (id ${app.id})` : undefined,
|
|
428
|
+
why: "identity.mode is app, so kanbot acts as a GitHub App.",
|
|
320
429
|
fix: "kanbot app create",
|
|
321
430
|
});
|
|
322
431
|
if (app) {
|
|
@@ -325,6 +434,7 @@ export default defineCommand({
|
|
|
325
434
|
name: "kanbot app private key",
|
|
326
435
|
ok: existsSync(key),
|
|
327
436
|
detail: key,
|
|
437
|
+
why: "kanbot signs in as the app with this key.",
|
|
328
438
|
fix: "kanbot app create, or copy the key from the machine that created the app",
|
|
329
439
|
});
|
|
330
440
|
if (existsSync(key)) {
|
|
@@ -346,6 +456,7 @@ export default defineCommand({
|
|
|
346
456
|
}
|
|
347
457
|
|
|
348
458
|
if (config.review.copilot) {
|
|
459
|
+
section("Copilot review");
|
|
349
460
|
const rule = await copilotReviewRule(gh, config.baseBranch).catch(
|
|
350
461
|
() => null,
|
|
351
462
|
);
|
|
@@ -356,14 +467,16 @@ export default defineCommand({
|
|
|
356
467
|
? `re-review on push: ${rule.parameters?.review_on_push ? "on" : "off"}`
|
|
357
468
|
: "review.copilot is on, but no ruleset requests Copilot review",
|
|
358
469
|
fix: "kanbot board create",
|
|
470
|
+
docs: "copilot-review",
|
|
359
471
|
});
|
|
360
472
|
add({
|
|
361
473
|
name: ".github/copilot-instructions.md committed",
|
|
474
|
+
why: "Copilot reads it to review the way your team wants.",
|
|
362
475
|
ok: tree.split("\n").includes(".github/copilot-instructions.md"),
|
|
363
476
|
fix: "kanbot sync, then commit and push",
|
|
364
477
|
});
|
|
365
478
|
}
|
|
366
479
|
|
|
367
|
-
|
|
480
|
+
summary(checks);
|
|
368
481
|
},
|
|
369
482
|
});
|