@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
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: ov ecosystem
|
|
3
|
+
*
|
|
4
|
+
* Shows a summary dashboard of all installed os-eco tools: version, update
|
|
5
|
+
* status (latest vs outdated), and doctor health (overstory only).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { jsonError, jsonOutput } from "../json.ts";
|
|
10
|
+
import { accent, brand, color, muted } from "../logging/color.ts";
|
|
11
|
+
|
|
12
|
+
const TOOLS = [
|
|
13
|
+
{ name: "overstory", cli: "ov", npm: "@os-eco/overstory-cli" },
|
|
14
|
+
{ name: "mulch", cli: "ml", npm: "@os-eco/mulch-cli" },
|
|
15
|
+
{ name: "seeds", cli: "sd", npm: "@os-eco/seeds-cli" },
|
|
16
|
+
{ name: "canopy", cli: "cn", npm: "@os-eco/canopy-cli" },
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export interface EcosystemOptions {
|
|
20
|
+
json?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DoctorSummary {
|
|
24
|
+
pass: number;
|
|
25
|
+
warn: number;
|
|
26
|
+
fail: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ToolResult {
|
|
30
|
+
name: string;
|
|
31
|
+
cli: string;
|
|
32
|
+
npm: string;
|
|
33
|
+
installed: boolean;
|
|
34
|
+
version?: string;
|
|
35
|
+
latest?: string;
|
|
36
|
+
upToDate?: boolean;
|
|
37
|
+
doctorSummary?: DoctorSummary;
|
|
38
|
+
latestError?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function getInstalledVersion(cli: string): Promise<string | null> {
|
|
42
|
+
// Try --version --json first
|
|
43
|
+
try {
|
|
44
|
+
const proc = Bun.spawn([cli, "--version", "--json"], {
|
|
45
|
+
stdout: "pipe",
|
|
46
|
+
stderr: "pipe",
|
|
47
|
+
});
|
|
48
|
+
const exitCode = await proc.exited;
|
|
49
|
+
if (exitCode === 0) {
|
|
50
|
+
const stdout = await new Response(proc.stdout).text();
|
|
51
|
+
try {
|
|
52
|
+
const data = JSON.parse(stdout.trim()) as { version?: string };
|
|
53
|
+
if (data.version) return data.version;
|
|
54
|
+
} catch {
|
|
55
|
+
// Not valid JSON, fall through to plain text
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// CLI not found — fall through to plain text fallback
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fallback: --version plain text
|
|
63
|
+
try {
|
|
64
|
+
const proc = Bun.spawn([cli, "--version"], {
|
|
65
|
+
stdout: "pipe",
|
|
66
|
+
stderr: "pipe",
|
|
67
|
+
});
|
|
68
|
+
const exitCode = await proc.exited;
|
|
69
|
+
if (exitCode === 0) {
|
|
70
|
+
const stdout = await new Response(proc.stdout).text();
|
|
71
|
+
const match = stdout.match(/(\d+\.\d+\.\d+)/);
|
|
72
|
+
if (match?.[1]) return match[1];
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// CLI not found
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function fetchLatestVersion(packageName: string): Promise<string> {
|
|
82
|
+
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
throw new Error(`npm registry error: ${res.status} ${res.statusText}`);
|
|
85
|
+
}
|
|
86
|
+
const data = (await res.json()) as { version: string };
|
|
87
|
+
return data.version;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function getDoctorSummary(): Promise<DoctorSummary | undefined> {
|
|
91
|
+
try {
|
|
92
|
+
const proc = Bun.spawn(["ov", "doctor", "--json"], {
|
|
93
|
+
stdout: "pipe",
|
|
94
|
+
stderr: "pipe",
|
|
95
|
+
});
|
|
96
|
+
await proc.exited;
|
|
97
|
+
const stdout = await new Response(proc.stdout).text();
|
|
98
|
+
const trimmed = stdout.trim();
|
|
99
|
+
if (trimmed) {
|
|
100
|
+
const data = JSON.parse(trimmed) as {
|
|
101
|
+
summary?: { pass?: number; warn?: number; fail?: number };
|
|
102
|
+
};
|
|
103
|
+
if (data.summary) {
|
|
104
|
+
return {
|
|
105
|
+
pass: data.summary.pass ?? 0,
|
|
106
|
+
warn: data.summary.warn ?? 0,
|
|
107
|
+
fail: data.summary.fail ?? 0,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Doctor failed — report nothing
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function checkTool(tool: { name: string; cli: string; npm: string }): Promise<ToolResult> {
|
|
118
|
+
const version = await getInstalledVersion(tool.cli);
|
|
119
|
+
|
|
120
|
+
if (version === null) {
|
|
121
|
+
return { name: tool.name, cli: tool.cli, npm: tool.npm, installed: false };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let latest: string | undefined;
|
|
125
|
+
let latestError: string | undefined;
|
|
126
|
+
let doctorSummary: DoctorSummary | undefined;
|
|
127
|
+
|
|
128
|
+
const latestPromise = fetchLatestVersion(tool.npm)
|
|
129
|
+
.then((v) => {
|
|
130
|
+
latest = v;
|
|
131
|
+
})
|
|
132
|
+
.catch((err) => {
|
|
133
|
+
latestError = err instanceof Error ? err.message : String(err);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const doctorPromise =
|
|
137
|
+
tool.name === "overstory"
|
|
138
|
+
? getDoctorSummary().then((d) => {
|
|
139
|
+
doctorSummary = d;
|
|
140
|
+
})
|
|
141
|
+
: Promise.resolve();
|
|
142
|
+
|
|
143
|
+
await Promise.all([latestPromise, doctorPromise]);
|
|
144
|
+
|
|
145
|
+
const upToDate = latest !== undefined ? version === latest : undefined;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
name: tool.name,
|
|
149
|
+
cli: tool.cli,
|
|
150
|
+
npm: tool.npm,
|
|
151
|
+
installed: true,
|
|
152
|
+
version,
|
|
153
|
+
latest,
|
|
154
|
+
upToDate,
|
|
155
|
+
doctorSummary,
|
|
156
|
+
latestError,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function formatDoctorLine(summary: DoctorSummary): string {
|
|
161
|
+
const parts: string[] = [];
|
|
162
|
+
if (summary.pass > 0) parts.push(color.green(`${summary.pass} passed`));
|
|
163
|
+
if (summary.warn > 0) parts.push(color.yellow(`${summary.warn} warn`));
|
|
164
|
+
if (summary.fail > 0) parts.push(color.red(`${summary.fail} fail`));
|
|
165
|
+
return parts.length > 0 ? parts.join(", ") : "no checks";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function printHumanOutput(results: ToolResult[]): void {
|
|
169
|
+
process.stdout.write(`${brand.bold("os-eco Ecosystem")}\n`);
|
|
170
|
+
process.stdout.write(`${"═".repeat(60)}\n`);
|
|
171
|
+
process.stdout.write("\n");
|
|
172
|
+
|
|
173
|
+
for (const tool of results) {
|
|
174
|
+
if (!tool.installed) {
|
|
175
|
+
process.stdout.write(
|
|
176
|
+
` ${color.red("x")} ${accent(tool.name)} ${muted(`(${tool.cli})`)} ${color.red("not installed")}\n`,
|
|
177
|
+
);
|
|
178
|
+
process.stdout.write(` ${muted(`npm i -g ${tool.npm}`)}\n`);
|
|
179
|
+
process.stdout.write("\n");
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Determine status icon
|
|
184
|
+
let icon: string;
|
|
185
|
+
if (tool.latestError !== undefined || tool.upToDate === undefined) {
|
|
186
|
+
icon = muted("-");
|
|
187
|
+
} else if (tool.upToDate) {
|
|
188
|
+
icon = color.green("-");
|
|
189
|
+
} else {
|
|
190
|
+
icon = color.yellow("!");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
process.stdout.write(` ${icon} ${accent(tool.name)} ${muted(`(${tool.cli})`)}\n`);
|
|
194
|
+
|
|
195
|
+
// Version line
|
|
196
|
+
let versionLine = `Version: ${tool.version}`;
|
|
197
|
+
if (tool.latestError !== undefined) {
|
|
198
|
+
versionLine += ` ${muted("(version check failed)")}`;
|
|
199
|
+
} else if (tool.upToDate === true) {
|
|
200
|
+
versionLine += ` ${color.green("(up to date)")}`;
|
|
201
|
+
} else if (tool.upToDate === false) {
|
|
202
|
+
versionLine += ` ${color.yellow(`(outdated, latest: ${tool.latest})`)}`;
|
|
203
|
+
}
|
|
204
|
+
process.stdout.write(` ${versionLine}\n`);
|
|
205
|
+
|
|
206
|
+
// Doctor summary (overstory only)
|
|
207
|
+
if (tool.doctorSummary !== undefined) {
|
|
208
|
+
process.stdout.write(` Doctor: ${formatDoctorLine(tool.doctorSummary)}\n`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
process.stdout.write("\n");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const installed = results.filter((t) => t.installed).length;
|
|
215
|
+
const missing = results.filter((t) => !t.installed).length;
|
|
216
|
+
const outdated = results.filter(
|
|
217
|
+
(t) => t.installed && t.upToDate === false && t.latestError === undefined,
|
|
218
|
+
).length;
|
|
219
|
+
|
|
220
|
+
let summary = `Summary: ${installed}/${results.length} installed`;
|
|
221
|
+
if (missing > 0) summary += `, ${missing} missing`;
|
|
222
|
+
if (outdated > 0) summary += `, ${outdated} outdated`;
|
|
223
|
+
process.stdout.write(`${summary}\n`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function executeEcosystem(opts: EcosystemOptions): Promise<void> {
|
|
227
|
+
const json = opts.json ?? false;
|
|
228
|
+
|
|
229
|
+
let results: ToolResult[];
|
|
230
|
+
try {
|
|
231
|
+
results = await Promise.all(TOOLS.map((tool) => checkTool(tool)));
|
|
232
|
+
} catch (err) {
|
|
233
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
234
|
+
if (json) {
|
|
235
|
+
jsonError("ecosystem", msg);
|
|
236
|
+
} else {
|
|
237
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
238
|
+
}
|
|
239
|
+
process.exitCode = 1;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (json) {
|
|
244
|
+
const installed = results.filter((t) => t.installed).length;
|
|
245
|
+
const missing = results.filter((t) => !t.installed).length;
|
|
246
|
+
const outdated = results.filter(
|
|
247
|
+
(t) => t.installed && t.upToDate === false && t.latestError === undefined,
|
|
248
|
+
).length;
|
|
249
|
+
|
|
250
|
+
jsonOutput("ecosystem", {
|
|
251
|
+
tools: results.map((t) => {
|
|
252
|
+
const entry: Record<string, unknown> = {
|
|
253
|
+
name: t.name,
|
|
254
|
+
cli: t.cli,
|
|
255
|
+
npm: t.npm,
|
|
256
|
+
installed: t.installed,
|
|
257
|
+
};
|
|
258
|
+
if (t.installed) {
|
|
259
|
+
entry.version = t.version;
|
|
260
|
+
entry.latest = t.latest;
|
|
261
|
+
entry.upToDate = t.upToDate;
|
|
262
|
+
if (t.doctorSummary !== undefined) {
|
|
263
|
+
entry.doctorSummary = t.doctorSummary;
|
|
264
|
+
}
|
|
265
|
+
if (t.latestError !== undefined) {
|
|
266
|
+
entry.latestError = t.latestError;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return entry;
|
|
270
|
+
}),
|
|
271
|
+
summary: {
|
|
272
|
+
total: results.length,
|
|
273
|
+
installed,
|
|
274
|
+
missing,
|
|
275
|
+
outdated,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
printHumanOutput(results);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function createEcosystemCommand(): Command {
|
|
285
|
+
return new Command("ecosystem")
|
|
286
|
+
.description("Show a summary dashboard of all installed os-eco tools")
|
|
287
|
+
.option("--json", "Output as JSON")
|
|
288
|
+
.action(async (opts: EcosystemOptions) => {
|
|
289
|
+
await executeEcosystem(opts);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
@@ -133,7 +133,14 @@ describe("errorsCommand", () => {
|
|
|
133
133
|
await errorsCommand(["--json"]);
|
|
134
134
|
const out = output();
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
137
|
+
success: boolean;
|
|
138
|
+
command: string;
|
|
139
|
+
events: unknown[];
|
|
140
|
+
};
|
|
141
|
+
expect(parsed.success).toBe(true);
|
|
142
|
+
expect(parsed.command).toBe("errors");
|
|
143
|
+
expect(parsed.events).toEqual([]);
|
|
137
144
|
});
|
|
138
145
|
});
|
|
139
146
|
|
|
@@ -157,9 +164,9 @@ describe("errorsCommand", () => {
|
|
|
157
164
|
await errorsCommand(["--json"]);
|
|
158
165
|
const out = output();
|
|
159
166
|
|
|
160
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
161
|
-
expect(parsed).toHaveLength(2);
|
|
162
|
-
expect(Array.isArray(parsed)).toBe(true);
|
|
167
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
168
|
+
expect(parsed.events).toHaveLength(2);
|
|
169
|
+
expect(Array.isArray(parsed.events)).toBe(true);
|
|
163
170
|
});
|
|
164
171
|
|
|
165
172
|
test("JSON output includes expected fields", async () => {
|
|
@@ -176,9 +183,9 @@ describe("errorsCommand", () => {
|
|
|
176
183
|
await errorsCommand(["--json"]);
|
|
177
184
|
const out = output();
|
|
178
185
|
|
|
179
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
180
|
-
expect(parsed).toHaveLength(1);
|
|
181
|
-
const event = parsed[0];
|
|
186
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
187
|
+
expect(parsed.events).toHaveLength(1);
|
|
188
|
+
const event = parsed.events[0];
|
|
182
189
|
expect(event).toBeDefined();
|
|
183
190
|
expect(event?.agentName).toBe("builder-1");
|
|
184
191
|
expect(event?.eventType).toBe("error");
|
|
@@ -201,8 +208,8 @@ describe("errorsCommand", () => {
|
|
|
201
208
|
await errorsCommand(["--json"]);
|
|
202
209
|
const out = output();
|
|
203
210
|
|
|
204
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
205
|
-
expect(parsed).toEqual([]);
|
|
211
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
212
|
+
expect(parsed.events).toEqual([]);
|
|
206
213
|
});
|
|
207
214
|
});
|
|
208
215
|
|
|
@@ -388,9 +395,9 @@ describe("errorsCommand", () => {
|
|
|
388
395
|
await errorsCommand(["--agent", "builder-1", "--json"]);
|
|
389
396
|
const out = output();
|
|
390
397
|
|
|
391
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
392
|
-
expect(parsed).toHaveLength(2);
|
|
393
|
-
for (const event of parsed) {
|
|
398
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
399
|
+
expect(parsed.events).toHaveLength(2);
|
|
400
|
+
for (const event of parsed.events) {
|
|
394
401
|
expect(event.agentName).toBe("builder-1");
|
|
395
402
|
}
|
|
396
403
|
});
|
|
@@ -404,8 +411,8 @@ describe("errorsCommand", () => {
|
|
|
404
411
|
await errorsCommand(["--agent", "nonexistent", "--json"]);
|
|
405
412
|
const out = output();
|
|
406
413
|
|
|
407
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
408
|
-
expect(parsed).toEqual([]);
|
|
414
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
415
|
+
expect(parsed.events).toEqual([]);
|
|
409
416
|
});
|
|
410
417
|
|
|
411
418
|
test("only returns error-level events for the agent", async () => {
|
|
@@ -431,9 +438,9 @@ describe("errorsCommand", () => {
|
|
|
431
438
|
await errorsCommand(["--agent", "builder-1", "--json"]);
|
|
432
439
|
const out = output();
|
|
433
440
|
|
|
434
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
435
|
-
expect(parsed).toHaveLength(1);
|
|
436
|
-
expect(parsed[0]?.level).toBe("error");
|
|
441
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
442
|
+
expect(parsed.events).toHaveLength(1);
|
|
443
|
+
expect(parsed.events[0]?.level).toBe("error");
|
|
437
444
|
});
|
|
438
445
|
});
|
|
439
446
|
|
|
@@ -451,9 +458,9 @@ describe("errorsCommand", () => {
|
|
|
451
458
|
await errorsCommand(["--run", "run-001", "--json"]);
|
|
452
459
|
const out = output();
|
|
453
460
|
|
|
454
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
455
|
-
expect(parsed).toHaveLength(2);
|
|
456
|
-
for (const event of parsed) {
|
|
461
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
462
|
+
expect(parsed.events).toHaveLength(2);
|
|
463
|
+
for (const event of parsed.events) {
|
|
457
464
|
expect(event.runId).toBe("run-001");
|
|
458
465
|
}
|
|
459
466
|
});
|
|
@@ -467,8 +474,8 @@ describe("errorsCommand", () => {
|
|
|
467
474
|
await errorsCommand(["--run", "run-999", "--json"]);
|
|
468
475
|
const out = output();
|
|
469
476
|
|
|
470
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
471
|
-
expect(parsed).toEqual([]);
|
|
477
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
478
|
+
expect(parsed.events).toEqual([]);
|
|
472
479
|
});
|
|
473
480
|
|
|
474
481
|
test("only returns error-level events for the run", async () => {
|
|
@@ -487,9 +494,9 @@ describe("errorsCommand", () => {
|
|
|
487
494
|
await errorsCommand(["--run", "run-001", "--json"]);
|
|
488
495
|
const out = output();
|
|
489
496
|
|
|
490
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
491
|
-
expect(parsed).toHaveLength(1);
|
|
492
|
-
expect(parsed[0]?.level).toBe("error");
|
|
497
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
498
|
+
expect(parsed.events).toHaveLength(1);
|
|
499
|
+
expect(parsed.events[0]?.level).toBe("error");
|
|
493
500
|
});
|
|
494
501
|
});
|
|
495
502
|
|
|
@@ -507,8 +514,8 @@ describe("errorsCommand", () => {
|
|
|
507
514
|
await errorsCommand(["--json", "--limit", "3"]);
|
|
508
515
|
const out = output();
|
|
509
516
|
|
|
510
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
511
|
-
expect(parsed).toHaveLength(3);
|
|
517
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
518
|
+
expect(parsed.events).toHaveLength(3);
|
|
512
519
|
});
|
|
513
520
|
|
|
514
521
|
test("default limit is 100", async () => {
|
|
@@ -522,8 +529,8 @@ describe("errorsCommand", () => {
|
|
|
522
529
|
await errorsCommand(["--json"]);
|
|
523
530
|
const out = output();
|
|
524
531
|
|
|
525
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
526
|
-
expect(parsed).toHaveLength(100);
|
|
532
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
533
|
+
expect(parsed.events).toHaveLength(100);
|
|
527
534
|
});
|
|
528
535
|
});
|
|
529
536
|
|
|
@@ -539,8 +546,8 @@ describe("errorsCommand", () => {
|
|
|
539
546
|
await errorsCommand(["--json", "--since", "2099-01-01T00:00:00Z"]);
|
|
540
547
|
const out = output();
|
|
541
548
|
|
|
542
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
543
|
-
expect(parsed).toEqual([]);
|
|
549
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
550
|
+
expect(parsed.events).toEqual([]);
|
|
544
551
|
});
|
|
545
552
|
|
|
546
553
|
test("--since with past timestamp returns all errors", async () => {
|
|
@@ -553,8 +560,8 @@ describe("errorsCommand", () => {
|
|
|
553
560
|
await errorsCommand(["--json", "--since", "2020-01-01T00:00:00Z"]);
|
|
554
561
|
const out = output();
|
|
555
562
|
|
|
556
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
557
|
-
expect(parsed).toHaveLength(2);
|
|
563
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
564
|
+
expect(parsed.events).toHaveLength(2);
|
|
558
565
|
});
|
|
559
566
|
|
|
560
567
|
test("--until with past timestamp returns no errors", async () => {
|
|
@@ -566,8 +573,8 @@ describe("errorsCommand", () => {
|
|
|
566
573
|
await errorsCommand(["--json", "--until", "2000-01-01T00:00:00Z"]);
|
|
567
574
|
const out = output();
|
|
568
575
|
|
|
569
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
570
|
-
expect(parsed).toEqual([]);
|
|
576
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
577
|
+
expect(parsed.events).toEqual([]);
|
|
571
578
|
});
|
|
572
579
|
});
|
|
573
580
|
|
|
@@ -608,8 +615,8 @@ describe("errorsCommand", () => {
|
|
|
608
615
|
await errorsCommand(["--json"]);
|
|
609
616
|
const out = output();
|
|
610
617
|
|
|
611
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
612
|
-
expect(parsed).toHaveLength(3);
|
|
618
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
619
|
+
expect(parsed.events).toHaveLength(3);
|
|
613
620
|
});
|
|
614
621
|
|
|
615
622
|
test("excludes non-error events from global view", async () => {
|
|
@@ -639,9 +646,9 @@ describe("errorsCommand", () => {
|
|
|
639
646
|
await errorsCommand(["--json"]);
|
|
640
647
|
const out = output();
|
|
641
648
|
|
|
642
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
643
|
-
expect(parsed).toHaveLength(1);
|
|
644
|
-
expect(parsed[0]?.level).toBe("error");
|
|
649
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
650
|
+
expect(parsed.events).toHaveLength(1);
|
|
651
|
+
expect(parsed.events[0]?.level).toBe("error");
|
|
645
652
|
});
|
|
646
653
|
});
|
|
647
654
|
});
|
package/src/commands/errors.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { Command } from "commander";
|
|
|
11
11
|
import { loadConfig } from "../config.ts";
|
|
12
12
|
import { ValidationError } from "../errors.ts";
|
|
13
13
|
import { createEventStore } from "../events/store.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { jsonOutput } from "../json.ts";
|
|
15
|
+
import { accent, color } from "../logging/color.ts";
|
|
15
16
|
import type { StoredEvent } from "../types.ts";
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -115,7 +116,7 @@ function printErrors(events: StoredEvent[]): void {
|
|
|
115
116
|
firstGroup = false;
|
|
116
117
|
|
|
117
118
|
w(
|
|
118
|
-
`${
|
|
119
|
+
`${accent(agentName)} ${color.dim(`(${agentEvents.length} error${agentEvents.length === 1 ? "" : "s"})`)}\n`,
|
|
119
120
|
);
|
|
120
121
|
|
|
121
122
|
for (const event of agentEvents) {
|
|
@@ -179,7 +180,7 @@ async function executeErrors(opts: ErrorsOpts): Promise<void> {
|
|
|
179
180
|
const eventsFile = Bun.file(eventsDbPath);
|
|
180
181
|
if (!(await eventsFile.exists())) {
|
|
181
182
|
if (json) {
|
|
182
|
-
|
|
183
|
+
jsonOutput("errors", { events: [] });
|
|
183
184
|
} else {
|
|
184
185
|
process.stdout.write("No events data yet.\n");
|
|
185
186
|
}
|
|
@@ -209,7 +210,7 @@ async function executeErrors(opts: ErrorsOpts): Promise<void> {
|
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
if (json) {
|
|
212
|
-
|
|
213
|
+
jsonOutput("errors", { events });
|
|
213
214
|
return;
|
|
214
215
|
}
|
|
215
216
|
|
|
@@ -138,7 +138,14 @@ describe("feedCommand", () => {
|
|
|
138
138
|
await feedCommand(["--json"]);
|
|
139
139
|
const out = output();
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
142
|
+
success: boolean;
|
|
143
|
+
command: string;
|
|
144
|
+
events: unknown[];
|
|
145
|
+
};
|
|
146
|
+
expect(parsed.success).toBe(true);
|
|
147
|
+
expect(parsed.command).toBe("feed");
|
|
148
|
+
expect(parsed.events).toEqual([]);
|
|
142
149
|
});
|
|
143
150
|
});
|
|
144
151
|
|
|
@@ -156,9 +163,9 @@ describe("feedCommand", () => {
|
|
|
156
163
|
await feedCommand(["--json"]);
|
|
157
164
|
const out = output();
|
|
158
165
|
|
|
159
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
160
|
-
expect(parsed).toHaveLength(3);
|
|
161
|
-
expect(Array.isArray(parsed)).toBe(true);
|
|
166
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
167
|
+
expect(parsed.events).toHaveLength(3);
|
|
168
|
+
expect(Array.isArray(parsed.events)).toBe(true);
|
|
162
169
|
});
|
|
163
170
|
|
|
164
171
|
test("JSON output includes expected fields", async () => {
|
|
@@ -177,9 +184,9 @@ describe("feedCommand", () => {
|
|
|
177
184
|
await feedCommand(["--json"]);
|
|
178
185
|
const out = output();
|
|
179
186
|
|
|
180
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
181
|
-
expect(parsed).toHaveLength(1);
|
|
182
|
-
const event = parsed[0];
|
|
187
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
188
|
+
expect(parsed.events).toHaveLength(1);
|
|
189
|
+
const event = parsed.events[0];
|
|
183
190
|
expect(event).toBeDefined();
|
|
184
191
|
expect(event?.agentName).toBe("builder-1");
|
|
185
192
|
expect(event?.eventType).toBe("tool_start");
|
|
@@ -198,8 +205,8 @@ describe("feedCommand", () => {
|
|
|
198
205
|
await feedCommand(["--json", "--since", "2099-01-01T00:00:00Z"]);
|
|
199
206
|
const out = output();
|
|
200
207
|
|
|
201
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
202
|
-
expect(parsed).toEqual([]);
|
|
208
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
209
|
+
expect(parsed.events).toEqual([]);
|
|
203
210
|
});
|
|
204
211
|
});
|
|
205
212
|
|
|
@@ -317,9 +324,9 @@ describe("feedCommand", () => {
|
|
|
317
324
|
await feedCommand(["--agent", "builder-1", "--json"]);
|
|
318
325
|
const out = output();
|
|
319
326
|
|
|
320
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
321
|
-
expect(parsed).toHaveLength(1);
|
|
322
|
-
expect(parsed[0]?.agentName).toBe("builder-1");
|
|
327
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
328
|
+
expect(parsed.events).toHaveLength(1);
|
|
329
|
+
expect(parsed.events[0]?.agentName).toBe("builder-1");
|
|
323
330
|
});
|
|
324
331
|
|
|
325
332
|
test("filters to multiple agents", async () => {
|
|
@@ -333,9 +340,9 @@ describe("feedCommand", () => {
|
|
|
333
340
|
await feedCommand(["--agent", "builder-1", "--agent", "scout-1", "--json"]);
|
|
334
341
|
const out = output();
|
|
335
342
|
|
|
336
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
337
|
-
expect(parsed).toHaveLength(2);
|
|
338
|
-
const agents = parsed.map((e) => e.agentName);
|
|
343
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
344
|
+
expect(parsed.events).toHaveLength(2);
|
|
345
|
+
const agents = parsed.events.map((e) => e.agentName);
|
|
339
346
|
expect(agents).toContain("builder-1");
|
|
340
347
|
expect(agents).toContain("scout-1");
|
|
341
348
|
expect(agents).not.toContain("builder-2");
|
|
@@ -356,9 +363,9 @@ describe("feedCommand", () => {
|
|
|
356
363
|
await feedCommand(["--run", "run-001", "--json"]);
|
|
357
364
|
const out = output();
|
|
358
365
|
|
|
359
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
360
|
-
expect(parsed).toHaveLength(2);
|
|
361
|
-
for (const event of parsed) {
|
|
366
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
367
|
+
expect(parsed.events).toHaveLength(2);
|
|
368
|
+
for (const event of parsed.events) {
|
|
362
369
|
expect(event.runId).toBe("run-001");
|
|
363
370
|
}
|
|
364
371
|
});
|
|
@@ -378,8 +385,8 @@ describe("feedCommand", () => {
|
|
|
378
385
|
await feedCommand(["--json", "--limit", "10"]);
|
|
379
386
|
const out = output();
|
|
380
387
|
|
|
381
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
382
|
-
expect(parsed).toHaveLength(10);
|
|
388
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
389
|
+
expect(parsed.events).toHaveLength(10);
|
|
383
390
|
});
|
|
384
391
|
|
|
385
392
|
test("default limit is 50", async () => {
|
|
@@ -393,8 +400,8 @@ describe("feedCommand", () => {
|
|
|
393
400
|
await feedCommand(["--json"]);
|
|
394
401
|
const out = output();
|
|
395
402
|
|
|
396
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
397
|
-
expect(parsed).toHaveLength(50);
|
|
403
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
404
|
+
expect(parsed.events).toHaveLength(50);
|
|
398
405
|
});
|
|
399
406
|
});
|
|
400
407
|
|
|
@@ -411,8 +418,8 @@ describe("feedCommand", () => {
|
|
|
411
418
|
await feedCommand(["--json", "--since", "2099-01-01T00:00:00Z"]);
|
|
412
419
|
const out = output();
|
|
413
420
|
|
|
414
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
415
|
-
expect(parsed).toEqual([]);
|
|
421
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
422
|
+
expect(parsed.events).toEqual([]);
|
|
416
423
|
});
|
|
417
424
|
|
|
418
425
|
test("--since with past timestamp returns all events", async () => {
|
|
@@ -425,8 +432,8 @@ describe("feedCommand", () => {
|
|
|
425
432
|
await feedCommand(["--json", "--since", "2020-01-01T00:00:00Z"]);
|
|
426
433
|
const out = output();
|
|
427
434
|
|
|
428
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
429
|
-
expect(parsed).toHaveLength(2);
|
|
435
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
436
|
+
expect(parsed.events).toHaveLength(2);
|
|
430
437
|
});
|
|
431
438
|
|
|
432
439
|
test("default since is 5 minutes ago", async () => {
|
|
@@ -440,8 +447,8 @@ describe("feedCommand", () => {
|
|
|
440
447
|
await feedCommand(["--json"]);
|
|
441
448
|
const out = output();
|
|
442
449
|
|
|
443
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
444
|
-
expect(parsed).toHaveLength(1);
|
|
450
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
451
|
+
expect(parsed.events).toHaveLength(1);
|
|
445
452
|
});
|
|
446
453
|
});
|
|
447
454
|
|
|
@@ -503,11 +510,11 @@ describe("feedCommand", () => {
|
|
|
503
510
|
await feedCommand(["--json"]);
|
|
504
511
|
const out = output();
|
|
505
512
|
|
|
506
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
507
|
-
expect(parsed).toHaveLength(3);
|
|
508
|
-
expect(parsed[0]?.eventType).toBe("session_start");
|
|
509
|
-
expect(parsed[1]?.eventType).toBe("tool_start");
|
|
510
|
-
expect(parsed[2]?.eventType).toBe("session_end");
|
|
513
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
514
|
+
expect(parsed.events).toHaveLength(3);
|
|
515
|
+
expect(parsed.events[0]?.eventType).toBe("session_start");
|
|
516
|
+
expect(parsed.events[1]?.eventType).toBe("tool_start");
|
|
517
|
+
expect(parsed.events[2]?.eventType).toBe("session_end");
|
|
511
518
|
});
|
|
512
519
|
|
|
513
520
|
test("handles event with all null optional fields", async () => {
|