@os-eco/overstory-cli 0.6.8 → 0.6.10
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 +19 -5
- package/agents/builder.md +6 -15
- package/agents/lead.md +4 -6
- package/agents/merger.md +5 -13
- package/agents/reviewer.md +2 -9
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +232 -0
- package/src/agents/hooks-deployer.ts +54 -8
- package/src/agents/overlay.test.ts +156 -1
- package/src/agents/overlay.ts +67 -7
- package/src/commands/agents.ts +9 -6
- package/src/commands/clean.ts +2 -1
- package/src/commands/completions.test.ts +8 -20
- package/src/commands/completions.ts +7 -6
- package/src/commands/coordinator.test.ts +8 -0
- package/src/commands/coordinator.ts +11 -8
- package/src/commands/costs.test.ts +48 -38
- package/src/commands/costs.ts +48 -38
- package/src/commands/dashboard.ts +7 -7
- package/src/commands/doctor.test.ts +8 -0
- package/src/commands/doctor.ts +96 -51
- package/src/commands/ecosystem.ts +291 -0
- package/src/commands/errors.test.ts +47 -40
- package/src/commands/errors.ts +5 -4
- package/src/commands/feed.test.ts +40 -33
- package/src/commands/feed.ts +5 -4
- package/src/commands/group.ts +23 -14
- package/src/commands/hooks.ts +2 -1
- package/src/commands/init.test.ts +104 -0
- package/src/commands/init.ts +11 -7
- package/src/commands/inspect.test.ts +2 -0
- package/src/commands/inspect.ts +9 -8
- package/src/commands/logs.test.ts +5 -6
- package/src/commands/logs.ts +2 -1
- package/src/commands/mail.test.ts +11 -10
- package/src/commands/mail.ts +11 -12
- package/src/commands/merge.ts +11 -12
- package/src/commands/metrics.test.ts +15 -2
- package/src/commands/metrics.ts +3 -2
- package/src/commands/monitor.ts +5 -4
- package/src/commands/nudge.ts +2 -3
- package/src/commands/prime.test.ts +1 -6
- package/src/commands/prime.ts +2 -3
- package/src/commands/replay.test.ts +62 -55
- package/src/commands/replay.ts +3 -2
- package/src/commands/run.ts +17 -20
- package/src/commands/sling.ts +3 -2
- package/src/commands/status.test.ts +2 -1
- package/src/commands/status.ts +7 -6
- package/src/commands/stop.test.ts +2 -0
- package/src/commands/stop.ts +10 -11
- package/src/commands/supervisor.ts +7 -6
- package/src/commands/trace.test.ts +52 -44
- package/src/commands/trace.ts +5 -4
- package/src/commands/upgrade.test.ts +46 -0
- package/src/commands/upgrade.ts +259 -0
- package/src/commands/watch.ts +8 -10
- package/src/commands/worktree.test.ts +21 -15
- package/src/commands/worktree.ts +10 -4
- package/src/doctor/databases.test.ts +38 -0
- package/src/doctor/databases.ts +7 -10
- package/src/doctor/ecosystem.test.ts +307 -0
- package/src/doctor/ecosystem.ts +155 -0
- package/src/doctor/merge-queue.test.ts +98 -0
- package/src/doctor/merge-queue.ts +23 -0
- package/src/doctor/structure.test.ts +130 -1
- package/src/doctor/structure.ts +87 -1
- package/src/doctor/types.ts +5 -2
- package/src/index.ts +25 -1
|
@@ -116,7 +116,14 @@ describe("costsCommand", () => {
|
|
|
116
116
|
await costsCommand(["--json"]);
|
|
117
117
|
const out = output();
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
120
|
+
success: boolean;
|
|
121
|
+
command: string;
|
|
122
|
+
sessions: unknown[];
|
|
123
|
+
};
|
|
124
|
+
expect(parsed.success).toBe(true);
|
|
125
|
+
expect(parsed.command).toBe("costs");
|
|
126
|
+
expect(parsed.sessions).toEqual([]);
|
|
120
127
|
});
|
|
121
128
|
});
|
|
122
129
|
|
|
@@ -149,9 +156,10 @@ describe("costsCommand", () => {
|
|
|
149
156
|
await costsCommand(["--json"]);
|
|
150
157
|
const out = output();
|
|
151
158
|
|
|
152
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
153
|
-
expect(
|
|
154
|
-
expect(parsed).
|
|
159
|
+
const parsed = JSON.parse(out.trim()) as { success: boolean; sessions: unknown[] };
|
|
160
|
+
expect(parsed.success).toBe(true);
|
|
161
|
+
expect(Array.isArray(parsed.sessions)).toBe(true);
|
|
162
|
+
expect(parsed.sessions).toHaveLength(2);
|
|
155
163
|
});
|
|
156
164
|
|
|
157
165
|
test("JSON output includes expected token fields", async () => {
|
|
@@ -173,9 +181,12 @@ describe("costsCommand", () => {
|
|
|
173
181
|
await costsCommand(["--json"]);
|
|
174
182
|
const out = output();
|
|
175
183
|
|
|
176
|
-
const parsed = JSON.parse(out.trim()) as
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
185
|
+
success: boolean;
|
|
186
|
+
sessions: Record<string, unknown>[];
|
|
187
|
+
};
|
|
188
|
+
expect(parsed.sessions).toHaveLength(1);
|
|
189
|
+
const session = parsed.sessions[0];
|
|
179
190
|
expect(session).toBeDefined();
|
|
180
191
|
expect(session?.inputTokens).toBe(100);
|
|
181
192
|
expect(session?.outputTokens).toBe(50);
|
|
@@ -193,8 +204,8 @@ describe("costsCommand", () => {
|
|
|
193
204
|
await costsCommand(["--json", "--agent", "nonexistent"]);
|
|
194
205
|
const out = output();
|
|
195
206
|
|
|
196
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
197
|
-
expect(parsed).toEqual([]);
|
|
207
|
+
const parsed = JSON.parse(out.trim()) as { success: boolean; sessions: unknown[] };
|
|
208
|
+
expect(parsed.sessions).toEqual([]);
|
|
198
209
|
});
|
|
199
210
|
|
|
200
211
|
test("JSON --by-capability outputs grouped object", async () => {
|
|
@@ -221,12 +232,12 @@ describe("costsCommand", () => {
|
|
|
221
232
|
await costsCommand(["--json", "--by-capability"]);
|
|
222
233
|
const out = output();
|
|
223
234
|
|
|
224
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown
|
|
225
|
-
expect(parsed).toBeDefined();
|
|
226
|
-
expect(parsed.builder).toBeDefined();
|
|
227
|
-
expect(parsed.scout).toBeDefined();
|
|
235
|
+
const parsed = JSON.parse(out.trim()) as { grouped: Record<string, unknown> };
|
|
236
|
+
expect(parsed.grouped).toBeDefined();
|
|
237
|
+
expect(parsed.grouped.builder).toBeDefined();
|
|
238
|
+
expect(parsed.grouped.scout).toBeDefined();
|
|
228
239
|
|
|
229
|
-
const builderGroup = parsed.builder as Record<string, unknown>;
|
|
240
|
+
const builderGroup = parsed.grouped.builder as Record<string, unknown>;
|
|
230
241
|
expect(builderGroup.sessions).toBeDefined();
|
|
231
242
|
expect(builderGroup.totals).toBeDefined();
|
|
232
243
|
});
|
|
@@ -415,9 +426,9 @@ describe("costsCommand", () => {
|
|
|
415
426
|
await costsCommand(["--json", "--agent", "builder-1"]);
|
|
416
427
|
const out = output();
|
|
417
428
|
|
|
418
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
419
|
-
expect(parsed).toHaveLength(1);
|
|
420
|
-
expect(parsed[0]?.agentName).toBe("builder-1");
|
|
429
|
+
const parsed = JSON.parse(out.trim()) as { sessions: Record<string, unknown>[] };
|
|
430
|
+
expect(parsed.sessions).toHaveLength(1);
|
|
431
|
+
expect(parsed.sessions[0]?.agentName).toBe("builder-1");
|
|
421
432
|
});
|
|
422
433
|
|
|
423
434
|
test("returns empty for non-existent agent", async () => {
|
|
@@ -429,8 +440,8 @@ describe("costsCommand", () => {
|
|
|
429
440
|
await costsCommand(["--json", "--agent", "nonexistent"]);
|
|
430
441
|
const out = output();
|
|
431
442
|
|
|
432
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
433
|
-
expect(parsed).toEqual([]);
|
|
443
|
+
const parsed = JSON.parse(out.trim()) as { sessions: unknown[] };
|
|
444
|
+
expect(parsed.sessions).toEqual([]);
|
|
434
445
|
});
|
|
435
446
|
});
|
|
436
447
|
|
|
@@ -456,9 +467,9 @@ describe("costsCommand", () => {
|
|
|
456
467
|
await costsCommand(["--json", "--run", "run-2026-01-01"]);
|
|
457
468
|
const out = output();
|
|
458
469
|
|
|
459
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
460
|
-
expect(parsed).toHaveLength(1);
|
|
461
|
-
expect(parsed[0]?.agentName).toBe("builder-1");
|
|
470
|
+
const parsed = JSON.parse(out.trim()) as { sessions: Record<string, unknown>[] };
|
|
471
|
+
expect(parsed.sessions).toHaveLength(1);
|
|
472
|
+
expect(parsed.sessions[0]?.agentName).toBe("builder-1");
|
|
462
473
|
});
|
|
463
474
|
|
|
464
475
|
test("returns empty when no sessions match run ID", async () => {
|
|
@@ -472,8 +483,8 @@ describe("costsCommand", () => {
|
|
|
472
483
|
await costsCommand(["--json", "--run", "run-nonexistent"]);
|
|
473
484
|
const out = output();
|
|
474
485
|
|
|
475
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
476
|
-
expect(parsed).toEqual([]);
|
|
486
|
+
const parsed = JSON.parse(out.trim()) as { sessions: unknown[] };
|
|
487
|
+
expect(parsed.sessions).toEqual([]);
|
|
477
488
|
});
|
|
478
489
|
});
|
|
479
490
|
|
|
@@ -557,12 +568,11 @@ describe("costsCommand", () => {
|
|
|
557
568
|
await costsCommand(["--json", "--by-capability"]);
|
|
558
569
|
const out = output();
|
|
559
570
|
|
|
560
|
-
const parsed = JSON.parse(out.trim()) as
|
|
561
|
-
string,
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
expect(parsed.
|
|
565
|
-
expect(parsed.scout?.sessions).toHaveLength(1);
|
|
571
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
572
|
+
grouped: Record<string, { sessions: unknown[]; totals: Record<string, unknown> }>;
|
|
573
|
+
};
|
|
574
|
+
expect(parsed.grouped.builder?.sessions).toHaveLength(3);
|
|
575
|
+
expect(parsed.grouped.scout?.sessions).toHaveLength(1);
|
|
566
576
|
});
|
|
567
577
|
|
|
568
578
|
test("empty data shows no session data message", async () => {
|
|
@@ -591,8 +601,8 @@ describe("costsCommand", () => {
|
|
|
591
601
|
await costsCommand(["--json", "--last", "3"]);
|
|
592
602
|
const out = output();
|
|
593
603
|
|
|
594
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
595
|
-
expect(parsed).toHaveLength(3);
|
|
604
|
+
const parsed = JSON.parse(out.trim()) as { sessions: unknown[] };
|
|
605
|
+
expect(parsed.sessions).toHaveLength(3);
|
|
596
606
|
});
|
|
597
607
|
|
|
598
608
|
test("default limit is 20", async () => {
|
|
@@ -606,8 +616,8 @@ describe("costsCommand", () => {
|
|
|
606
616
|
await costsCommand(["--json"]);
|
|
607
617
|
const out = output();
|
|
608
618
|
|
|
609
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
610
|
-
expect(parsed).toHaveLength(20);
|
|
619
|
+
const parsed = JSON.parse(out.trim()) as { sessions: unknown[] };
|
|
620
|
+
expect(parsed.sessions).toHaveLength(20);
|
|
611
621
|
});
|
|
612
622
|
});
|
|
613
623
|
|
|
@@ -705,9 +715,9 @@ describe("costsCommand", () => {
|
|
|
705
715
|
await costsCommand(["--json"]);
|
|
706
716
|
const out = output();
|
|
707
717
|
|
|
708
|
-
const parsed = JSON.parse(out.trim()) as SessionMetrics[];
|
|
709
|
-
const totalInput = parsed.reduce((sum, s) => sum + s.inputTokens, 0);
|
|
710
|
-
const totalOutput = parsed.reduce((sum, s) => sum + s.outputTokens, 0);
|
|
718
|
+
const parsed = JSON.parse(out.trim()) as { sessions: SessionMetrics[] };
|
|
719
|
+
const totalInput = parsed.sessions.reduce((sum, s) => sum + s.inputTokens, 0);
|
|
720
|
+
const totalOutput = parsed.sessions.reduce((sum, s) => sum + s.outputTokens, 0);
|
|
711
721
|
expect(totalInput).toBe(300);
|
|
712
722
|
expect(totalOutput).toBe(150);
|
|
713
723
|
});
|
|
@@ -1106,7 +1116,7 @@ describe("costsCommand", () => {
|
|
|
1106
1116
|
const out = output();
|
|
1107
1117
|
|
|
1108
1118
|
const parsed = JSON.parse(out.trim()) as Record<string, unknown>;
|
|
1109
|
-
expect(parsed.error).toBe("
|
|
1119
|
+
expect(parsed.error).toBe("No orchestrator transcript found");
|
|
1110
1120
|
});
|
|
1111
1121
|
|
|
1112
1122
|
test("--self in help text", async () => {
|
package/src/commands/costs.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { join } from "node:path";
|
|
|
11
11
|
import { Command } from "commander";
|
|
12
12
|
import { loadConfig } from "../config.ts";
|
|
13
13
|
import { ValidationError } from "../errors.ts";
|
|
14
|
+
import { jsonError, jsonOutput } from "../json.ts";
|
|
14
15
|
import { color } from "../logging/color.ts";
|
|
15
16
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
16
17
|
import { estimateCost, parseTranscriptUsage } from "../metrics/transcript.ts";
|
|
@@ -272,10 +273,7 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
272
273
|
const transcriptPath = await discoverOrchestratorTranscript(config.project.root);
|
|
273
274
|
if (!transcriptPath) {
|
|
274
275
|
if (json) {
|
|
275
|
-
|
|
276
|
-
JSON.stringify({ error: "no_transcript", message: "No orchestrator transcript found" }) +
|
|
277
|
-
"\n",
|
|
278
|
-
);
|
|
276
|
+
jsonError("costs", "No orchestrator transcript found");
|
|
279
277
|
} else {
|
|
280
278
|
process.stdout.write(
|
|
281
279
|
"No orchestrator transcript found.\nExpected at: ~/.claude/projects/{project-key}/*.jsonl\n",
|
|
@@ -289,18 +287,16 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
289
287
|
const cacheTotal = usage.cacheReadTokens + usage.cacheCreationTokens;
|
|
290
288
|
|
|
291
289
|
if (json) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
})}\n`,
|
|
303
|
-
);
|
|
290
|
+
jsonOutput("costs", {
|
|
291
|
+
source: "self",
|
|
292
|
+
transcriptPath,
|
|
293
|
+
model: usage.modelUsed,
|
|
294
|
+
inputTokens: usage.inputTokens,
|
|
295
|
+
outputTokens: usage.outputTokens,
|
|
296
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
297
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
298
|
+
estimatedCostUsd: cost,
|
|
299
|
+
});
|
|
304
300
|
} else {
|
|
305
301
|
const w = process.stdout.write.bind(process.stdout);
|
|
306
302
|
const separator = "\u2500".repeat(70);
|
|
@@ -327,9 +323,17 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
327
323
|
const metricsFile = Bun.file(metricsDbPath);
|
|
328
324
|
if (!(await metricsFile.exists())) {
|
|
329
325
|
if (json) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
326
|
+
jsonOutput("costs", {
|
|
327
|
+
agents: [],
|
|
328
|
+
totals: {
|
|
329
|
+
inputTokens: 0,
|
|
330
|
+
outputTokens: 0,
|
|
331
|
+
cacheTokens: 0,
|
|
332
|
+
costUsd: 0,
|
|
333
|
+
burnRatePerMin: 0,
|
|
334
|
+
tokensPerMin: 0,
|
|
335
|
+
},
|
|
336
|
+
});
|
|
333
337
|
} else {
|
|
334
338
|
process.stdout.write(
|
|
335
339
|
"No live data available. Token snapshots begin after first tool call.\n",
|
|
@@ -345,9 +349,17 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
345
349
|
const snapshots = metricsStore.getLatestSnapshots();
|
|
346
350
|
if (snapshots.length === 0) {
|
|
347
351
|
if (json) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
352
|
+
jsonOutput("costs", {
|
|
353
|
+
agents: [],
|
|
354
|
+
totals: {
|
|
355
|
+
inputTokens: 0,
|
|
356
|
+
outputTokens: 0,
|
|
357
|
+
cacheTokens: 0,
|
|
358
|
+
costUsd: 0,
|
|
359
|
+
burnRatePerMin: 0,
|
|
360
|
+
tokensPerMin: 0,
|
|
361
|
+
},
|
|
362
|
+
});
|
|
351
363
|
} else {
|
|
352
364
|
process.stdout.write(
|
|
353
365
|
"No live data available. Token snapshots begin after first tool call.\n",
|
|
@@ -428,19 +440,17 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
428
440
|
const tokensPerMin = avgElapsedMs > 0 ? totalTokens / (avgElapsedMs / 60_000) : 0;
|
|
429
441
|
|
|
430
442
|
if (json) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
})}\n`,
|
|
443
|
-
);
|
|
443
|
+
jsonOutput("costs", {
|
|
444
|
+
agents: agentData,
|
|
445
|
+
totals: {
|
|
446
|
+
inputTokens: totalInput,
|
|
447
|
+
outputTokens: totalOutput,
|
|
448
|
+
cacheTokens: totalCacheTokens,
|
|
449
|
+
costUsd: totalCost,
|
|
450
|
+
burnRatePerMin,
|
|
451
|
+
tokensPerMin,
|
|
452
|
+
},
|
|
453
|
+
});
|
|
444
454
|
} else {
|
|
445
455
|
const w = process.stdout.write.bind(process.stdout);
|
|
446
456
|
const separator = "\u2500".repeat(70);
|
|
@@ -502,7 +512,7 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
502
512
|
const metricsFile = Bun.file(metricsDbPath);
|
|
503
513
|
if (!(await metricsFile.exists())) {
|
|
504
514
|
if (json) {
|
|
505
|
-
|
|
515
|
+
jsonOutput("costs", { sessions: [] });
|
|
506
516
|
} else {
|
|
507
517
|
process.stdout.write("No metrics data yet.\n");
|
|
508
518
|
}
|
|
@@ -532,9 +542,9 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
532
542
|
totals: group.totals,
|
|
533
543
|
};
|
|
534
544
|
}
|
|
535
|
-
|
|
545
|
+
jsonOutput("costs", { grouped });
|
|
536
546
|
} else {
|
|
537
|
-
|
|
547
|
+
jsonOutput("costs", { sessions });
|
|
538
548
|
}
|
|
539
549
|
return;
|
|
540
550
|
}
|
|
@@ -15,7 +15,7 @@ import { Command } from "commander";
|
|
|
15
15
|
import { loadConfig } from "../config.ts";
|
|
16
16
|
import { ValidationError } from "../errors.ts";
|
|
17
17
|
import type { ColorFn } from "../logging/color.ts";
|
|
18
|
-
import { color, noColor, visibleLength } from "../logging/color.ts";
|
|
18
|
+
import { accent, color, noColor, visibleLength } from "../logging/color.ts";
|
|
19
19
|
import { createMailStore, type MailStore } from "../mail/store.ts";
|
|
20
20
|
import { createMergeQueue, type MergeQueue } from "../merge/queue.ts";
|
|
21
21
|
import { createMetricsStore, type MetricsStore } from "../metrics/store.ts";
|
|
@@ -404,7 +404,7 @@ async function loadDashboardData(
|
|
|
404
404
|
function renderHeader(width: number, interval: number, currentRunId?: string | null): string {
|
|
405
405
|
const left = color.bold(`ov dashboard v${PKG_VERSION}`);
|
|
406
406
|
const now = new Date().toLocaleTimeString();
|
|
407
|
-
const scope = currentRunId ? ` [run: ${currentRunId.slice(0, 8)}]` : " [all runs]";
|
|
407
|
+
const scope = currentRunId ? ` [run: ${accent(currentRunId.slice(0, 8))}]` : " [all runs]";
|
|
408
408
|
const right = `${now}${scope} | refresh: ${interval}ms`;
|
|
409
409
|
const padding = width - visibleLength(left) - right.length;
|
|
410
410
|
const line = left + " ".repeat(Math.max(0, padding)) + right;
|
|
@@ -497,10 +497,10 @@ function renderAgentPanel(
|
|
|
497
497
|
|
|
498
498
|
const icon = getStateIcon(agent.state);
|
|
499
499
|
const stateColor = getStateColor(agent.state);
|
|
500
|
-
const name = pad(truncate(agent.agentName, 15), 15);
|
|
500
|
+
const name = accent(pad(truncate(agent.agentName, 15), 15));
|
|
501
501
|
const capability = pad(truncate(agent.capability, 12), 12);
|
|
502
502
|
const state = pad(agent.state, 10);
|
|
503
|
-
const taskId = pad(truncate(agent.taskId, 16), 16);
|
|
503
|
+
const taskId = accent(pad(truncate(agent.taskId, 16), 16));
|
|
504
504
|
const endTime =
|
|
505
505
|
agent.state === "completed" || agent.state === "zombie"
|
|
506
506
|
? new Date(agent.lastActivity).getTime()
|
|
@@ -575,8 +575,8 @@ function renderMailPanel(
|
|
|
575
575
|
|
|
576
576
|
const priorityColorFn = getPriorityColor(msg.priority);
|
|
577
577
|
const priority = msg.priority === "normal" ? "" : `[${msg.priority}] `;
|
|
578
|
-
const from = truncate(msg.from, 12);
|
|
579
|
-
const to = truncate(msg.to, 12);
|
|
578
|
+
const from = accent(truncate(msg.from, 12));
|
|
579
|
+
const to = accent(truncate(msg.to, 12));
|
|
580
580
|
const subject = truncate(msg.subject, panelWidth - 40);
|
|
581
581
|
const time = timeAgo(msg.createdAt);
|
|
582
582
|
|
|
@@ -643,7 +643,7 @@ function renderMergeQueuePanel(
|
|
|
643
643
|
|
|
644
644
|
const statusColorFn = getMergeStatusColor(entry.status);
|
|
645
645
|
const status = pad(entry.status, 10);
|
|
646
|
-
const agent = truncate(entry.agentName, 15);
|
|
646
|
+
const agent = accent(truncate(entry.agentName, 15));
|
|
647
647
|
const branch = truncate(entry.branchName, panelWidth - 30);
|
|
648
648
|
|
|
649
649
|
const line = `${BOX.vertical} ${statusColorFn(status)} ${agent} ${branch}`;
|
|
@@ -85,10 +85,14 @@ describe("doctorCommand", () => {
|
|
|
85
85
|
const out = output();
|
|
86
86
|
|
|
87
87
|
const parsed = JSON.parse(out.trim()) as {
|
|
88
|
+
success: boolean;
|
|
89
|
+
command: string;
|
|
88
90
|
checks: unknown[];
|
|
89
91
|
summary: { pass: number; warn: number; fail: number };
|
|
90
92
|
};
|
|
91
93
|
expect(parsed).toBeDefined();
|
|
94
|
+
expect(parsed.success).toBe(true);
|
|
95
|
+
expect(parsed.command).toBe("doctor");
|
|
92
96
|
expect(Array.isArray(parsed.checks)).toBe(true);
|
|
93
97
|
expect(parsed.summary).toBeDefined();
|
|
94
98
|
expect(typeof parsed.summary.pass).toBe("number");
|
|
@@ -101,9 +105,13 @@ describe("doctorCommand", () => {
|
|
|
101
105
|
const out = output();
|
|
102
106
|
|
|
103
107
|
const parsed = JSON.parse(out.trim()) as {
|
|
108
|
+
success: boolean;
|
|
109
|
+
command: string;
|
|
104
110
|
checks: unknown[];
|
|
105
111
|
summary: { pass: number; warn: number; fail: number };
|
|
106
112
|
};
|
|
113
|
+
expect(parsed.success).toBe(true);
|
|
114
|
+
expect(parsed.command).toBe("doctor");
|
|
107
115
|
expect(parsed.checks).toEqual([]);
|
|
108
116
|
expect(parsed.summary.pass).toBe(0);
|
|
109
117
|
expect(parsed.summary.warn).toBe(0);
|
package/src/commands/doctor.ts
CHANGED
|
@@ -12,12 +12,14 @@ import { checkConfig } from "../doctor/config-check.ts";
|
|
|
12
12
|
import { checkConsistency } from "../doctor/consistency.ts";
|
|
13
13
|
import { checkDatabases } from "../doctor/databases.ts";
|
|
14
14
|
import { checkDependencies } from "../doctor/dependencies.ts";
|
|
15
|
+
import { checkEcosystem } from "../doctor/ecosystem.ts";
|
|
15
16
|
import { checkLogs } from "../doctor/logs.ts";
|
|
16
17
|
import { checkMergeQueue } from "../doctor/merge-queue.ts";
|
|
17
18
|
import { checkStructure } from "../doctor/structure.ts";
|
|
18
19
|
import type { DoctorCategory, DoctorCheck, DoctorCheckFn } from "../doctor/types.ts";
|
|
19
20
|
import { checkVersion } from "../doctor/version.ts";
|
|
20
21
|
import { ValidationError } from "../errors.ts";
|
|
22
|
+
import { jsonOutput } from "../json.ts";
|
|
21
23
|
import { color } from "../logging/color.ts";
|
|
22
24
|
|
|
23
25
|
/** Registry of all check modules in execution order. */
|
|
@@ -31,8 +33,25 @@ const ALL_CHECKS: Array<{ category: DoctorCategory; fn: DoctorCheckFn }> = [
|
|
|
31
33
|
{ category: "merge", fn: checkMergeQueue },
|
|
32
34
|
{ category: "logs", fn: checkLogs },
|
|
33
35
|
{ category: "version", fn: checkVersion },
|
|
36
|
+
{ category: "ecosystem", fn: checkEcosystem },
|
|
34
37
|
];
|
|
35
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Execute all fix functions on non-passing fixable checks.
|
|
41
|
+
* Returns a list of human-readable actions taken.
|
|
42
|
+
*/
|
|
43
|
+
async function applyFixes(checks: DoctorCheck[]): Promise<string[]> {
|
|
44
|
+
const fixable = checks.filter((c) => c.fixable && c.status !== "pass" && c.fix);
|
|
45
|
+
const fixed: string[] = [];
|
|
46
|
+
for (const check of fixable) {
|
|
47
|
+
if (check.fix) {
|
|
48
|
+
const actions = await check.fix();
|
|
49
|
+
fixed.push(...actions);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return fixed;
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
/**
|
|
37
56
|
* Format human-readable output for doctor checks.
|
|
38
57
|
*/
|
|
@@ -40,6 +59,7 @@ function printHumanReadable(
|
|
|
40
59
|
checks: DoctorCheck[],
|
|
41
60
|
verbose: boolean,
|
|
42
61
|
checkRegistry: Array<{ category: DoctorCategory; fn: DoctorCheckFn }>,
|
|
62
|
+
fixedItems?: string[],
|
|
43
63
|
): void {
|
|
44
64
|
const w = process.stdout.write.bind(process.stdout);
|
|
45
65
|
|
|
@@ -104,22 +124,28 @@ function printHumanReadable(
|
|
|
104
124
|
w(
|
|
105
125
|
`${color.bold("Summary:")} ${color.green(`${pass} passed`)}, ${color.yellow(`${warn} warning${warn === 1 ? "" : "s"}`)}, ${color.red(`${fail} failure${fail === 1 ? "" : "s"}`)}\n`,
|
|
106
126
|
);
|
|
127
|
+
|
|
128
|
+
if (fixedItems && fixedItems.length > 0) {
|
|
129
|
+
w(`\n${color.bold("Fixed:")}\n`);
|
|
130
|
+
for (const item of fixedItems) {
|
|
131
|
+
w(` ${color.green("-")} ${item}\n`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
107
134
|
}
|
|
108
135
|
|
|
109
136
|
/**
|
|
110
137
|
* Format JSON output for doctor checks.
|
|
111
138
|
*/
|
|
112
|
-
function printJSON(checks: DoctorCheck[]): void {
|
|
139
|
+
function printJSON(checks: DoctorCheck[], fixed?: string[]): void {
|
|
113
140
|
const pass = checks.filter((c) => c.status === "pass").length;
|
|
114
141
|
const warn = checks.filter((c) => c.status === "warn").length;
|
|
115
142
|
const fail = checks.filter((c) => c.status === "fail").length;
|
|
116
143
|
|
|
117
|
-
|
|
144
|
+
jsonOutput("doctor", {
|
|
118
145
|
checks,
|
|
119
146
|
summary: { pass, warn, fail },
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
|
147
|
+
...(fixed && fixed.length > 0 ? { fixed } : {}),
|
|
148
|
+
});
|
|
123
149
|
}
|
|
124
150
|
|
|
125
151
|
/** Options for dependency injection in doctorCommand. */
|
|
@@ -137,59 +163,78 @@ export function createDoctorCommand(options?: DoctorCommandOptions): Command {
|
|
|
137
163
|
.option("--json", "Output as JSON")
|
|
138
164
|
.option("--verbose", "Show passing checks (default: only problems)")
|
|
139
165
|
.option("--category <name>", "Run only one category")
|
|
166
|
+
.option("--fix", "Attempt to auto-fix issues")
|
|
140
167
|
.addHelpText(
|
|
141
168
|
"after",
|
|
142
|
-
"\nCategories: dependencies, structure, config, databases, consistency, agents, merge, logs, version",
|
|
169
|
+
"\nCategories: dependencies, structure, config, databases, consistency, agents, merge, logs, version, ecosystem",
|
|
143
170
|
)
|
|
144
|
-
.action(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
171
|
+
.action(
|
|
172
|
+
async (opts: { json?: boolean; verbose?: boolean; category?: string; fix?: boolean }) => {
|
|
173
|
+
const json = opts.json ?? false;
|
|
174
|
+
const verbose = opts.verbose ?? false;
|
|
175
|
+
const categoryFilter = opts.category;
|
|
176
|
+
const fix = opts.fix ?? false;
|
|
177
|
+
|
|
178
|
+
// Validate category filter if provided
|
|
179
|
+
if (categoryFilter !== undefined) {
|
|
180
|
+
const validCategories = ALL_CHECKS.map((c) => c.category);
|
|
181
|
+
if (!validCategories.includes(categoryFilter as DoctorCategory)) {
|
|
182
|
+
throw new ValidationError(
|
|
183
|
+
`Invalid category: ${categoryFilter}. Valid categories: ${validCategories.join(", ")}`,
|
|
184
|
+
{
|
|
185
|
+
field: "category",
|
|
186
|
+
value: categoryFilter,
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
}
|
|
160
190
|
}
|
|
161
|
-
}
|
|
162
191
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// Filter checks by category if specified
|
|
168
|
-
const allChecks = options?.checkRunners ?? ALL_CHECKS;
|
|
169
|
-
const checksToRun = categoryFilter
|
|
170
|
-
? allChecks.filter((c) => c.category === categoryFilter)
|
|
171
|
-
: allChecks;
|
|
172
|
-
|
|
173
|
-
// Run all checks sequentially
|
|
174
|
-
const results: DoctorCheck[] = [];
|
|
175
|
-
for (const { fn } of checksToRun) {
|
|
176
|
-
const checkResults = await fn(config, overstoryDir);
|
|
177
|
-
results.push(...checkResults);
|
|
178
|
-
}
|
|
192
|
+
const cwd = process.cwd();
|
|
193
|
+
const config = await loadConfig(cwd);
|
|
194
|
+
const overstoryDir = join(config.project.root, ".overstory");
|
|
179
195
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
196
|
+
// Filter checks by category if specified
|
|
197
|
+
const allChecks = options?.checkRunners ?? ALL_CHECKS;
|
|
198
|
+
const checksToRun = categoryFilter
|
|
199
|
+
? allChecks.filter((c) => c.category === categoryFilter)
|
|
200
|
+
: allChecks;
|
|
186
201
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
202
|
+
// Run all checks sequentially
|
|
203
|
+
let results: DoctorCheck[] = [];
|
|
204
|
+
for (const { fn } of checksToRun) {
|
|
205
|
+
const checkResults = await fn(config, overstoryDir);
|
|
206
|
+
results.push(...checkResults);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Apply fixes if requested
|
|
210
|
+
let fixedItems: string[] | undefined;
|
|
211
|
+
if (fix) {
|
|
212
|
+
const applied = await applyFixes(results);
|
|
213
|
+
if (applied.length > 0) {
|
|
214
|
+
fixedItems = applied;
|
|
215
|
+
// Re-run all checks to get fresh results after fixes
|
|
216
|
+
results = [];
|
|
217
|
+
for (const { fn } of checksToRun) {
|
|
218
|
+
const checkResults = await fn(config, overstoryDir);
|
|
219
|
+
results.push(...checkResults);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Output results
|
|
225
|
+
if (json) {
|
|
226
|
+
printJSON(results, fixedItems);
|
|
227
|
+
} else {
|
|
228
|
+
printHumanReadable(results, verbose, allChecks, fixedItems);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Set exit code if any check failed
|
|
232
|
+
const hasFailures = results.some((c) => c.status === "fail");
|
|
233
|
+
if (hasFailures) {
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
);
|
|
193
238
|
}
|
|
194
239
|
|
|
195
240
|
/**
|