@os-eco/overstory-cli 0.6.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/LICENSE +21 -0
- package/README.md +381 -0
- package/agents/builder.md +137 -0
- package/agents/coordinator.md +263 -0
- package/agents/lead.md +301 -0
- package/agents/merger.md +160 -0
- package/agents/monitor.md +214 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +119 -0
- package/agents/supervisor.md +423 -0
- package/package.json +47 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/hooks-deployer.test.ts +2040 -0
- package/src/agents/hooks-deployer.ts +607 -0
- package/src/agents/identity.test.ts +603 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/manifest.test.ts +746 -0
- package/src/agents/manifest.ts +354 -0
- package/src/agents/overlay.test.ts +676 -0
- package/src/agents/overlay.ts +308 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +202 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +322 -0
- package/src/commands/agents.ts +287 -0
- package/src/commands/clean.test.ts +670 -0
- package/src/commands/clean.ts +618 -0
- package/src/commands/completions.test.ts +342 -0
- package/src/commands/completions.ts +887 -0
- package/src/commands/coordinator.test.ts +1530 -0
- package/src/commands/coordinator.ts +733 -0
- package/src/commands/costs.test.ts +1119 -0
- package/src/commands/costs.ts +564 -0
- package/src/commands/dashboard.test.ts +308 -0
- package/src/commands/dashboard.ts +838 -0
- package/src/commands/doctor.test.ts +294 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/errors.test.ts +647 -0
- package/src/commands/errors.ts +248 -0
- package/src/commands/feed.test.ts +578 -0
- package/src/commands/feed.ts +361 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +511 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +253 -0
- package/src/commands/init.test.ts +347 -0
- package/src/commands/init.ts +650 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +431 -0
- package/src/commands/log.test.ts +1454 -0
- package/src/commands/log.ts +724 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +546 -0
- package/src/commands/mail.test.ts +1270 -0
- package/src/commands/mail.ts +771 -0
- package/src/commands/merge.test.ts +670 -0
- package/src/commands/merge.ts +355 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +143 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +390 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +372 -0
- package/src/commands/prime.test.ts +470 -0
- package/src/commands/prime.ts +381 -0
- package/src/commands/replay.test.ts +741 -0
- package/src/commands/replay.ts +360 -0
- package/src/commands/run.test.ts +431 -0
- package/src/commands/run.ts +351 -0
- package/src/commands/sling.test.ts +657 -0
- package/src/commands/sling.ts +661 -0
- package/src/commands/spec.test.ts +203 -0
- package/src/commands/spec.ts +168 -0
- package/src/commands/status.test.ts +430 -0
- package/src/commands/status.ts +398 -0
- package/src/commands/stop.test.ts +420 -0
- package/src/commands/stop.ts +151 -0
- package/src/commands/supervisor.test.ts +187 -0
- package/src/commands/supervisor.ts +535 -0
- package/src/commands/trace.test.ts +745 -0
- package/src/commands/trace.ts +325 -0
- package/src/commands/watch.test.ts +145 -0
- package/src/commands/watch.ts +247 -0
- package/src/commands/worktree.test.ts +786 -0
- package/src/commands/worktree.ts +311 -0
- package/src/config.test.ts +822 -0
- package/src/config.ts +829 -0
- package/src/doctor/agents.test.ts +454 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +190 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +651 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +290 -0
- package/src/doctor/databases.ts +218 -0
- package/src/doctor/dependencies.test.ts +184 -0
- package/src/doctor/dependencies.ts +175 -0
- package/src/doctor/logs.test.ts +251 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +216 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +291 -0
- package/src/doctor/structure.ts +198 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +136 -0
- package/src/doctor/version.ts +129 -0
- package/src/e2e/init-sling-lifecycle.test.ts +277 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +316 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +142 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +813 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +773 -0
- package/src/mail/client.ts +223 -0
- package/src/mail/store.test.ts +705 -0
- package/src/mail/store.ts +387 -0
- package/src/merge/queue.test.ts +359 -0
- package/src/merge/queue.ts +231 -0
- package/src/merge/resolver.test.ts +1345 -0
- package/src/merge/resolver.ts +645 -0
- package/src/metrics/store.test.ts +667 -0
- package/src/metrics/store.ts +445 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +356 -0
- package/src/metrics/transcript.ts +175 -0
- package/src/mulch/client.test.ts +671 -0
- package/src/mulch/client.ts +332 -0
- package/src/sessions/compat.test.ts +280 -0
- package/src/sessions/compat.ts +104 -0
- package/src/sessions/store.test.ts +873 -0
- package/src/sessions/store.ts +494 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +126 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +80 -0
- package/src/tracker/factory.ts +64 -0
- package/src/tracker/seeds.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/types.ts +724 -0
- package/src/watchdog/daemon.test.ts +1975 -0
- package/src/watchdog/daemon.ts +671 -0
- package/src/watchdog/health.test.ts +431 -0
- package/src/watchdog/health.ts +264 -0
- package/src/watchdog/triage.test.ts +164 -0
- package/src/watchdog/triage.ts +179 -0
- package/src/worktree/manager.test.ts +439 -0
- package/src/worktree/manager.ts +198 -0
- package/src/worktree/tmux.test.ts +1009 -0
- package/src/worktree/tmux.ts +509 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +105 -0
- package/templates/overlay.md.tmpl +81 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `overstory doctor` command.
|
|
3
|
+
*
|
|
4
|
+
* Uses temp directories with real config.yaml to test the doctor scaffold.
|
|
5
|
+
* All check modules return empty arrays (stubs), so tests verify the scaffold
|
|
6
|
+
* structure, not individual check implementations.
|
|
7
|
+
*
|
|
8
|
+
* Real implementations used for: filesystem (temp dirs), config loading.
|
|
9
|
+
* No mocks needed -- all dependencies are cheap and local.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { ValidationError } from "../errors.ts";
|
|
17
|
+
import { doctorCommand } from "./doctor.ts";
|
|
18
|
+
|
|
19
|
+
describe("doctorCommand", () => {
|
|
20
|
+
let chunks: string[];
|
|
21
|
+
let originalWrite: typeof process.stdout.write;
|
|
22
|
+
let tempDir: string;
|
|
23
|
+
let originalCwd: string;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
// Spy on stdout
|
|
27
|
+
chunks = [];
|
|
28
|
+
originalWrite = process.stdout.write;
|
|
29
|
+
process.stdout.write = ((chunk: string) => {
|
|
30
|
+
chunks.push(chunk);
|
|
31
|
+
return true;
|
|
32
|
+
}) as typeof process.stdout.write;
|
|
33
|
+
|
|
34
|
+
// Create temp dir with .overstory/config.yaml structure
|
|
35
|
+
tempDir = await mkdtemp(join(tmpdir(), "doctor-test-"));
|
|
36
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
37
|
+
await Bun.write(
|
|
38
|
+
join(overstoryDir, "config.yaml"),
|
|
39
|
+
`project:\n name: test\n root: ${tempDir}\n canonicalBranch: main\n`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Change to temp dir so loadConfig() works
|
|
43
|
+
originalCwd = process.cwd();
|
|
44
|
+
process.chdir(tempDir);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(async () => {
|
|
48
|
+
process.stdout.write = originalWrite;
|
|
49
|
+
process.chdir(originalCwd);
|
|
50
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function output(): string {
|
|
54
|
+
return chunks.join("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// === Help flag ===
|
|
58
|
+
|
|
59
|
+
describe("help flag", () => {
|
|
60
|
+
test("--help shows help text", async () => {
|
|
61
|
+
await doctorCommand(["--help"], { checkRunners: [] });
|
|
62
|
+
const out = output();
|
|
63
|
+
|
|
64
|
+
expect(out).toContain("overstory doctor");
|
|
65
|
+
expect(out).toContain("Run health checks");
|
|
66
|
+
expect(out).toContain("--json");
|
|
67
|
+
expect(out).toContain("--verbose");
|
|
68
|
+
expect(out).toContain("--category");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("-h shows help text", async () => {
|
|
72
|
+
await doctorCommand(["-h"], { checkRunners: [] });
|
|
73
|
+
const out = output();
|
|
74
|
+
|
|
75
|
+
expect(out).toContain("overstory doctor");
|
|
76
|
+
expect(out).toContain("--help");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// === JSON output ===
|
|
81
|
+
|
|
82
|
+
describe("JSON output mode", () => {
|
|
83
|
+
test("outputs valid JSON with checks array and summary", async () => {
|
|
84
|
+
await doctorCommand(["--json"], { checkRunners: [] });
|
|
85
|
+
const out = output();
|
|
86
|
+
|
|
87
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
88
|
+
checks: unknown[];
|
|
89
|
+
summary: { pass: number; warn: number; fail: number };
|
|
90
|
+
};
|
|
91
|
+
expect(parsed).toBeDefined();
|
|
92
|
+
expect(Array.isArray(parsed.checks)).toBe(true);
|
|
93
|
+
expect(parsed.summary).toBeDefined();
|
|
94
|
+
expect(typeof parsed.summary.pass).toBe("number");
|
|
95
|
+
expect(typeof parsed.summary.warn).toBe("number");
|
|
96
|
+
expect(typeof parsed.summary.fail).toBe("number");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("empty stubs produce zero counts in summary", async () => {
|
|
100
|
+
await doctorCommand(["--json"], { checkRunners: [] });
|
|
101
|
+
const out = output();
|
|
102
|
+
|
|
103
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
104
|
+
checks: unknown[];
|
|
105
|
+
summary: { pass: number; warn: number; fail: number };
|
|
106
|
+
};
|
|
107
|
+
expect(parsed.checks).toEqual([]);
|
|
108
|
+
expect(parsed.summary.pass).toBe(0);
|
|
109
|
+
expect(parsed.summary.warn).toBe(0);
|
|
110
|
+
expect(parsed.summary.fail).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// === Human-readable output ===
|
|
115
|
+
|
|
116
|
+
describe("human-readable output", () => {
|
|
117
|
+
test("shows header", async () => {
|
|
118
|
+
await doctorCommand([], { checkRunners: [] });
|
|
119
|
+
const out = output();
|
|
120
|
+
|
|
121
|
+
expect(out).toContain("Overstory Doctor");
|
|
122
|
+
expect(out).toContain("================");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("shows summary line with zero counts", async () => {
|
|
126
|
+
await doctorCommand([], { checkRunners: [] });
|
|
127
|
+
const out = output();
|
|
128
|
+
|
|
129
|
+
expect(out).toContain("Summary:");
|
|
130
|
+
expect(out).toContain("0 passed");
|
|
131
|
+
expect(out).toContain("0 warning");
|
|
132
|
+
expect(out).toContain("0 failure");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("summary uses singular form for one failure", async () => {
|
|
136
|
+
// This test can't verify "1 failure" without real checks, but we can test the logic
|
|
137
|
+
// by checking that the scaffold doesn't crash on empty checks
|
|
138
|
+
await doctorCommand([], { checkRunners: [] });
|
|
139
|
+
const out = output();
|
|
140
|
+
|
|
141
|
+
// Should show "0 failures" (plural) when count is 0
|
|
142
|
+
expect(out).toContain("0 failure");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("default mode does not show empty categories", async () => {
|
|
146
|
+
await doctorCommand([], { checkRunners: [] });
|
|
147
|
+
const out = output();
|
|
148
|
+
|
|
149
|
+
// Since all stubs return empty arrays, no category headers should appear
|
|
150
|
+
// in non-verbose mode
|
|
151
|
+
expect(out).not.toContain("[dependencies]");
|
|
152
|
+
expect(out).not.toContain("[structure]");
|
|
153
|
+
expect(out).not.toContain("[config]");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// === --verbose flag ===
|
|
158
|
+
|
|
159
|
+
describe("--verbose flag", () => {
|
|
160
|
+
test("shows header and summary even with no check runners", async () => {
|
|
161
|
+
await doctorCommand(["--verbose"], { checkRunners: [] });
|
|
162
|
+
const out = output();
|
|
163
|
+
|
|
164
|
+
// With no check runners, no categories appear (even in verbose mode)
|
|
165
|
+
expect(out).toContain("Overstory Doctor");
|
|
166
|
+
expect(out).toContain("Summary:");
|
|
167
|
+
expect(out).toContain("0 passed");
|
|
168
|
+
// No categories should appear with empty checkRunners
|
|
169
|
+
expect(out).not.toContain("[dependencies]");
|
|
170
|
+
expect(out).not.toContain("[structure]");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("verbose mode works with empty check runners", async () => {
|
|
174
|
+
await doctorCommand(["--verbose"], { checkRunners: [] });
|
|
175
|
+
const out = output();
|
|
176
|
+
|
|
177
|
+
// Should still produce valid output with no categories
|
|
178
|
+
expect(out).toContain("Overstory Doctor");
|
|
179
|
+
expect(out).toContain("Summary:");
|
|
180
|
+
// With no check runners, "No checks" doesn't appear (no categories to show it under)
|
|
181
|
+
expect(out).not.toContain("No checks");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// === --category flag ===
|
|
186
|
+
|
|
187
|
+
describe("--category flag", () => {
|
|
188
|
+
test("runs only specified category", async () => {
|
|
189
|
+
await doctorCommand(["--category", "dependencies", "--json"], { checkRunners: [] });
|
|
190
|
+
const out = output();
|
|
191
|
+
|
|
192
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
193
|
+
checks: Array<{ category: string }>;
|
|
194
|
+
};
|
|
195
|
+
// Since dependencies stub returns [], checks should be empty
|
|
196
|
+
expect(parsed.checks).toEqual([]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("validates category name", async () => {
|
|
200
|
+
await expect(
|
|
201
|
+
doctorCommand(["--category", "invalid-category"], { checkRunners: [] }),
|
|
202
|
+
).rejects.toThrow(ValidationError);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("invalid category error mentions valid categories", async () => {
|
|
206
|
+
try {
|
|
207
|
+
await doctorCommand(["--category", "bad"], { checkRunners: [] });
|
|
208
|
+
expect.unreachable("should have thrown");
|
|
209
|
+
} catch (err) {
|
|
210
|
+
expect(err).toBeInstanceOf(ValidationError);
|
|
211
|
+
const message = (err as ValidationError).message;
|
|
212
|
+
expect(message).toContain("Invalid category");
|
|
213
|
+
expect(message).toContain("dependencies");
|
|
214
|
+
expect(message).toContain("structure");
|
|
215
|
+
expect(message).toContain("config");
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("accepts all valid category names", async () => {
|
|
220
|
+
const categories = [
|
|
221
|
+
"dependencies",
|
|
222
|
+
"structure",
|
|
223
|
+
"config",
|
|
224
|
+
"databases",
|
|
225
|
+
"consistency",
|
|
226
|
+
"agents",
|
|
227
|
+
"merge",
|
|
228
|
+
"logs",
|
|
229
|
+
"version",
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
for (const category of categories) {
|
|
233
|
+
chunks = []; // Reset output
|
|
234
|
+
await doctorCommand(["--category", category, "--json"], { checkRunners: [] });
|
|
235
|
+
const out = output();
|
|
236
|
+
// Should not throw, and output should be valid JSON
|
|
237
|
+
JSON.parse(out.trim());
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// === Exit code ===
|
|
243
|
+
|
|
244
|
+
describe("exit code", () => {
|
|
245
|
+
test("returns undefined when all checks pass or warn", async () => {
|
|
246
|
+
const exitCode = await doctorCommand([], { checkRunners: [] });
|
|
247
|
+
expect(exitCode).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("returns undefined on success (no failures)", async () => {
|
|
251
|
+
const exitCode = await doctorCommand([], { checkRunners: [] });
|
|
252
|
+
expect(exitCode).toBeUndefined();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// === Edge cases ===
|
|
257
|
+
|
|
258
|
+
describe("edge cases", () => {
|
|
259
|
+
test("handles multiple flags together", async () => {
|
|
260
|
+
await doctorCommand(["--json", "--verbose", "--category", "config"], {
|
|
261
|
+
checkRunners: [],
|
|
262
|
+
});
|
|
263
|
+
const out = output();
|
|
264
|
+
|
|
265
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
266
|
+
checks: unknown[];
|
|
267
|
+
summary: unknown;
|
|
268
|
+
};
|
|
269
|
+
expect(parsed.checks).toEqual([]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("flags can appear in any order", async () => {
|
|
273
|
+
chunks = [];
|
|
274
|
+
await doctorCommand(["--category", "logs", "--json"], { checkRunners: [] });
|
|
275
|
+
const out1 = output();
|
|
276
|
+
|
|
277
|
+
chunks = [];
|
|
278
|
+
await doctorCommand(["--json", "--category", "logs"], { checkRunners: [] });
|
|
279
|
+
const out2 = output();
|
|
280
|
+
|
|
281
|
+
// Both should produce the same JSON
|
|
282
|
+
expect(JSON.parse(out1.trim())).toEqual(JSON.parse(out2.trim()));
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("runs without crashing on minimal config", async () => {
|
|
286
|
+
// The beforeEach already sets up minimal config, so this just
|
|
287
|
+
// verifies the command doesn't crash
|
|
288
|
+
await doctorCommand([], { checkRunners: [] });
|
|
289
|
+
const out = output();
|
|
290
|
+
|
|
291
|
+
expect(out).toContain("Overstory Doctor");
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: overstory doctor [options]
|
|
3
|
+
*
|
|
4
|
+
* Runs health checks on overstory subsystems and reports problems.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { loadConfig } from "../config.ts";
|
|
9
|
+
import { checkAgents } from "../doctor/agents.ts";
|
|
10
|
+
import { checkConfig } from "../doctor/config-check.ts";
|
|
11
|
+
import { checkConsistency } from "../doctor/consistency.ts";
|
|
12
|
+
import { checkDatabases } from "../doctor/databases.ts";
|
|
13
|
+
import { checkDependencies } from "../doctor/dependencies.ts";
|
|
14
|
+
import { checkLogs } from "../doctor/logs.ts";
|
|
15
|
+
import { checkMergeQueue } from "../doctor/merge-queue.ts";
|
|
16
|
+
import { checkStructure } from "../doctor/structure.ts";
|
|
17
|
+
import type { DoctorCategory, DoctorCheck, DoctorCheckFn } from "../doctor/types.ts";
|
|
18
|
+
import { checkVersion } from "../doctor/version.ts";
|
|
19
|
+
import { ValidationError } from "../errors.ts";
|
|
20
|
+
import { color } from "../logging/color.ts";
|
|
21
|
+
|
|
22
|
+
/** Registry of all check modules in execution order. */
|
|
23
|
+
const ALL_CHECKS: Array<{ category: DoctorCategory; fn: DoctorCheckFn }> = [
|
|
24
|
+
{ category: "dependencies", fn: checkDependencies },
|
|
25
|
+
{ category: "config", fn: checkConfig },
|
|
26
|
+
{ category: "structure", fn: checkStructure },
|
|
27
|
+
{ category: "databases", fn: checkDatabases },
|
|
28
|
+
{ category: "consistency", fn: checkConsistency },
|
|
29
|
+
{ category: "agents", fn: checkAgents },
|
|
30
|
+
{ category: "merge", fn: checkMergeQueue },
|
|
31
|
+
{ category: "logs", fn: checkLogs },
|
|
32
|
+
{ category: "version", fn: checkVersion },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function hasFlag(args: string[], flag: string): boolean {
|
|
36
|
+
return args.includes(flag);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getFlag(args: string[], flag: string): string | undefined {
|
|
40
|
+
const idx = args.indexOf(flag);
|
|
41
|
+
if (idx === -1 || idx + 1 >= args.length) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return args[idx + 1];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format human-readable output for doctor checks.
|
|
49
|
+
*/
|
|
50
|
+
function printHumanReadable(
|
|
51
|
+
checks: DoctorCheck[],
|
|
52
|
+
verbose: boolean,
|
|
53
|
+
checkRegistry: Array<{ category: DoctorCategory; fn: DoctorCheckFn }>,
|
|
54
|
+
): void {
|
|
55
|
+
const w = process.stdout.write.bind(process.stdout);
|
|
56
|
+
|
|
57
|
+
w(`${color.bold}Overstory Doctor${color.reset}\n`);
|
|
58
|
+
w("================\n\n");
|
|
59
|
+
|
|
60
|
+
// Group checks by category
|
|
61
|
+
const byCategory = new Map<DoctorCategory, DoctorCheck[]>();
|
|
62
|
+
for (const check of checks) {
|
|
63
|
+
const existing = byCategory.get(check.category);
|
|
64
|
+
if (existing) {
|
|
65
|
+
existing.push(check);
|
|
66
|
+
} else {
|
|
67
|
+
byCategory.set(check.category, [check]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Print each category
|
|
72
|
+
for (const { category } of checkRegistry) {
|
|
73
|
+
const categoryChecks = byCategory.get(category) ?? [];
|
|
74
|
+
if (categoryChecks.length === 0 && !verbose) {
|
|
75
|
+
continue; // Skip empty categories unless verbose
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
w(`${color.bold}[${category}]${color.reset}\n`);
|
|
79
|
+
|
|
80
|
+
if (categoryChecks.length === 0) {
|
|
81
|
+
w(` ${color.dim}No checks${color.reset}\n`);
|
|
82
|
+
} else {
|
|
83
|
+
for (const check of categoryChecks) {
|
|
84
|
+
// Skip passing checks unless verbose
|
|
85
|
+
if (check.status === "pass" && !verbose) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const icon =
|
|
90
|
+
check.status === "pass"
|
|
91
|
+
? `${color.green}✔${color.reset}`
|
|
92
|
+
: check.status === "warn"
|
|
93
|
+
? `${color.yellow}⚠${color.reset}`
|
|
94
|
+
: `${color.red}✘${color.reset}`;
|
|
95
|
+
|
|
96
|
+
w(` ${icon} ${check.message}\n`);
|
|
97
|
+
|
|
98
|
+
// Print details if present
|
|
99
|
+
if (check.details && check.details.length > 0) {
|
|
100
|
+
for (const detail of check.details) {
|
|
101
|
+
w(` ${color.dim}→ ${detail}${color.reset}\n`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
w("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Summary
|
|
111
|
+
const pass = checks.filter((c) => c.status === "pass").length;
|
|
112
|
+
const warn = checks.filter((c) => c.status === "warn").length;
|
|
113
|
+
const fail = checks.filter((c) => c.status === "fail").length;
|
|
114
|
+
|
|
115
|
+
w(
|
|
116
|
+
`${color.bold}Summary:${color.reset} ${color.green}${pass} passed${color.reset}, ${color.yellow}${warn} warning${warn === 1 ? "" : "s"}${color.reset}, ${color.red}${fail} failure${fail === 1 ? "" : "s"}${color.reset}\n`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Format JSON output for doctor checks.
|
|
122
|
+
*/
|
|
123
|
+
function printJSON(checks: DoctorCheck[]): void {
|
|
124
|
+
const pass = checks.filter((c) => c.status === "pass").length;
|
|
125
|
+
const warn = checks.filter((c) => c.status === "warn").length;
|
|
126
|
+
const fail = checks.filter((c) => c.status === "fail").length;
|
|
127
|
+
|
|
128
|
+
const output = {
|
|
129
|
+
checks,
|
|
130
|
+
summary: { pass, warn, fail },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const DOCTOR_HELP = `overstory doctor -- Run health checks on overstory subsystems
|
|
137
|
+
|
|
138
|
+
Usage: overstory doctor [options]
|
|
139
|
+
|
|
140
|
+
Options:
|
|
141
|
+
--json Output as JSON
|
|
142
|
+
--verbose Show passing checks (default: only problems)
|
|
143
|
+
--category <name> Run only one category
|
|
144
|
+
--help, -h Show this help
|
|
145
|
+
|
|
146
|
+
Categories: dependencies, structure, config, databases, consistency, agents, merge, logs, version`;
|
|
147
|
+
|
|
148
|
+
/** Options for dependency injection in doctorCommand. */
|
|
149
|
+
export interface DoctorCommandOptions {
|
|
150
|
+
/** Override the check runners (defaults to ALL_CHECKS). Pass [] to skip all checks. */
|
|
151
|
+
checkRunners?: Array<{ category: DoctorCategory; fn: DoctorCheckFn }>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Entry point for `overstory doctor [--json] [--verbose] [--category <name>]`.
|
|
156
|
+
*
|
|
157
|
+
* @returns Exit code (1 if any check failed, undefined otherwise)
|
|
158
|
+
*/
|
|
159
|
+
export async function doctorCommand(
|
|
160
|
+
args: string[],
|
|
161
|
+
options?: DoctorCommandOptions,
|
|
162
|
+
): Promise<number | undefined> {
|
|
163
|
+
if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
|
|
164
|
+
process.stdout.write(`${DOCTOR_HELP}\n`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const json = hasFlag(args, "--json");
|
|
169
|
+
const verbose = hasFlag(args, "--verbose");
|
|
170
|
+
const categoryFilter = getFlag(args, "--category");
|
|
171
|
+
|
|
172
|
+
// Validate category filter if provided
|
|
173
|
+
if (categoryFilter !== undefined) {
|
|
174
|
+
const validCategories = ALL_CHECKS.map((c) => c.category);
|
|
175
|
+
if (!validCategories.includes(categoryFilter as DoctorCategory)) {
|
|
176
|
+
throw new ValidationError(
|
|
177
|
+
`Invalid category: ${categoryFilter}. Valid categories: ${validCategories.join(", ")}`,
|
|
178
|
+
{
|
|
179
|
+
field: "category",
|
|
180
|
+
value: categoryFilter,
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const cwd = process.cwd();
|
|
187
|
+
const config = await loadConfig(cwd);
|
|
188
|
+
const overstoryDir = join(config.project.root, ".overstory");
|
|
189
|
+
|
|
190
|
+
// Filter checks by category if specified
|
|
191
|
+
const allChecks = options?.checkRunners ?? ALL_CHECKS;
|
|
192
|
+
const checksToRun = categoryFilter
|
|
193
|
+
? allChecks.filter((c) => c.category === categoryFilter)
|
|
194
|
+
: allChecks;
|
|
195
|
+
|
|
196
|
+
// Run all checks sequentially
|
|
197
|
+
const results: DoctorCheck[] = [];
|
|
198
|
+
for (const { fn } of checksToRun) {
|
|
199
|
+
const checkResults = await fn(config, overstoryDir);
|
|
200
|
+
results.push(...checkResults);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Output results
|
|
204
|
+
if (json) {
|
|
205
|
+
printJSON(results);
|
|
206
|
+
} else {
|
|
207
|
+
printHumanReadable(results, verbose, allChecks);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Return exit code if any check failed
|
|
211
|
+
const hasFailures = results.some((c) => c.status === "fail");
|
|
212
|
+
return hasFailures ? 1 : undefined;
|
|
213
|
+
}
|