Skip to content

Commit 17658d2

Browse files
gnapseclaude
andauthored
feat: add structured error formatting with codes and hints (#18)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b635e5c commit 17658d2

6 files changed

Lines changed: 105 additions & 12 deletions

File tree

src/__tests__/output.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2-
import { getOutputOptions, outputItem, outputList } from "../lib/output.js";
2+
import {
3+
formatError,
4+
getOutputOptions,
5+
outputItem,
6+
outputList,
7+
} from "../lib/output.js";
38

49
describe("output", () => {
510
let logs: string[];
@@ -52,4 +57,37 @@ describe("output", () => {
5257
},
5358
);
5459
});
60+
61+
describe("formatError", () => {
62+
it("formats error with code and message", () => {
63+
const result = formatError("TEST_ERROR", "Something went wrong");
64+
expect(result).toContain("Error: TEST_ERROR");
65+
expect(result).toContain("Something went wrong");
66+
});
67+
68+
it("formats error with hints", () => {
69+
const result = formatError("TEST_ERROR", "Something went wrong", [
70+
"Try this",
71+
"Or try that",
72+
]);
73+
expect(result).toContain("Error: TEST_ERROR");
74+
expect(result).toContain("Something went wrong");
75+
expect(result).toContain(" - Try this");
76+
expect(result).toContain(" - Or try that");
77+
});
78+
79+
it("formats error with empty hints array", () => {
80+
const result = formatError("TEST_ERROR", "Something went wrong", []);
81+
expect(result).toContain("Error: TEST_ERROR");
82+
expect(result).toContain("Something went wrong");
83+
expect(result).not.toContain(" - ");
84+
});
85+
86+
it("formats error without hints", () => {
87+
const result = formatError("NO_HINTS", "No hints provided");
88+
expect(result).toContain("Error: NO_HINTS");
89+
expect(result).toContain("No hints provided");
90+
expect(result).not.toContain(" - ");
91+
});
92+
});
5593
});

src/commands/auth.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getTokenSource,
1010
saveConfig,
1111
} from "../lib/auth.js";
12+
import { formatError } from "../lib/output.js";
1213

1314
interface TeamInfo {
1415
name: string;
@@ -59,7 +60,12 @@ export function registerAuthCommand(program: Command): void {
5960
.action(async () => {
6061
const token = await promptSecret("API token: ");
6162
if (!token.trim()) {
62-
console.error(chalk.red("Token is required."));
63+
console.error(
64+
formatError("AUTH_TOKEN_REQUIRED", "API token is required.", [
65+
"Enter your API token from Outline settings",
66+
"Find it at Settings → API Tokens",
67+
]),
68+
);
6369
process.exit(1);
6470
}
6571

@@ -104,8 +110,15 @@ export function registerAuthCommand(program: Command): void {
104110
console.log(`User: ${data.user.name} (${data.user.email})`);
105111
} catch (err) {
106112
console.error(
107-
chalk.red("Could not fetch auth info:"),
108-
(err as Error).message,
113+
formatError(
114+
"AUTH_VERIFICATION_FAILED",
115+
`Could not fetch auth info: ${(err as Error).message}`,
116+
[
117+
"Check that your API token is valid",
118+
"Verify the base URL is correct",
119+
"Run 'ol auth login' to re-authenticate",
120+
],
121+
),
109122
);
110123
process.exit(1);
111124
}

src/commands/collection.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import chalk from "chalk";
22
import type { Command } from "commander";
33
import { apiRequest } from "../lib/api.js";
4-
import { getOutputOptions, outputItem, outputList } from "../lib/output.js";
4+
import {
5+
formatError,
6+
getOutputOptions,
7+
outputItem,
8+
outputList,
9+
} from "../lib/output.js";
510

611
interface Collection {
712
id: string;
@@ -122,7 +127,13 @@ export function registerCollectionCommand(program: Command): void {
122127
.option("--confirm", "Skip confirmation")
123128
.action(async (id: string, opts) => {
124129
if (!opts.confirm) {
125-
console.error(chalk.red("Use --confirm to delete."));
130+
console.error(
131+
formatError(
132+
"CONFIRMATION_REQUIRED",
133+
"Delete operation requires confirmation.",
134+
["Use --confirm flag to proceed with deletion"],
135+
),
136+
);
126137
process.exit(1);
127138
}
128139
await apiRequest("collections.delete", { id });

src/commands/document.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import type { Command } from "commander";
55
import { apiRequest } from "../lib/api.js";
66
import { getBaseUrl } from "../lib/auth.js";
77
import { renderMarkdown } from "../lib/markdown.js";
8-
import { getOutputOptions, outputItem, outputList } from "../lib/output.js";
8+
import {
9+
formatError,
10+
getOutputOptions,
11+
outputItem,
12+
outputList,
13+
} from "../lib/output.js";
914

1015
interface Document {
1116
id: string;
@@ -226,7 +231,13 @@ export function registerDocumentCommand(program: Command): void {
226231
.option("--confirm", "Skip confirmation")
227232
.action(async (id: string, opts) => {
228233
if (!opts.confirm) {
229-
console.error(chalk.red("Use --confirm to delete."));
234+
console.error(
235+
formatError(
236+
"CONFIRMATION_REQUIRED",
237+
"Delete operation requires confirmation.",
238+
["Use --confirm flag to proceed with deletion"],
239+
),
240+
);
230241
process.exit(1);
231242
}
232243
await apiRequest("documents.delete", { id: resolveId(id) });

src/commands/skill.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import chalk from "chalk";
22
import type { Command } from "commander";
3+
import { formatError } from "../lib/output.js";
34
import {
45
getInstaller,
56
listAgents,
@@ -26,8 +27,10 @@ export function registerSkillCommand(program: Command): void {
2627
if (!installer) {
2728
const available = listAgents().join(", ");
2829
console.error(
29-
chalk.red(`Unknown agent: ${agent}`),
30-
chalk.dim(`\nAvailable: ${available}`),
30+
formatError("UNKNOWN_AGENT", `Unknown agent: ${agent}`, [
31+
`Available agents: ${available}`,
32+
"Run 'ol skill list' to see all agents",
33+
]),
3134
);
3235
process.exit(1);
3336
}
@@ -56,8 +59,10 @@ export function registerSkillCommand(program: Command): void {
5659
if (!installer) {
5760
const available = listAgents().join(", ");
5861
console.error(
59-
chalk.red(`Unknown agent: ${agent}`),
60-
chalk.dim(`\nAvailable: ${available}`),
62+
formatError("UNKNOWN_AGENT", `Unknown agent: ${agent}`, [
63+
`Available agents: ${available}`,
64+
"Run 'ol skill list' to see all agents",
65+
]),
6166
);
6267
process.exit(1);
6368
}

src/lib/output.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,18 @@ function pick<T extends object>(obj: T, keys: (keyof T)[]): Partial<T> {
7474
}
7575
return result;
7676
}
77+
78+
export function formatError(
79+
code: string,
80+
message: string,
81+
hints?: string[],
82+
): string {
83+
const lines = [`Error: ${code}`, message];
84+
if (hints && hints.length > 0) {
85+
lines.push("");
86+
for (const hint of hints) {
87+
lines.push(` - ${hint}`);
88+
}
89+
}
90+
return chalk.red(lines.join("\n"));
91+
}

0 commit comments

Comments
 (0)