Skip to content

Commit 6e31132

Browse files
committed
fix: preserve toolcraft error output format
1 parent 411fff6 commit 6e31132

3 files changed

Lines changed: 143 additions & 71 deletions

File tree

packages/terminal-pilot/src/testing/testing.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,11 @@ describe("terminal-pilot CLI REPL runner", () => {
106106

107107
const missing = await repl.run(["get-session", "-s", "S1", "--output", "json"]);
108108
expect(missing.exitCode).toBe(1);
109-
expect(missing.stdout).toBe("");
110-
expect(missing.stderr).toContain('Session "S1" was not found.');
109+
expect(JSON.parse(missing.stdout)).toEqual({
110+
level: "error",
111+
message: 'Session "S1" was not found. No active sessions are available.'
112+
});
113+
expect(missing.stderr).toBe("");
111114
});
112115
});
113116

packages/toolcraft/src/cli.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const loggerState = {
1616
success: [] as string[],
1717
warn: [] as string[],
1818
error: [] as string[],
19+
errorOutputFormats: [] as Array<string | undefined>,
1920
resolved: [] as Array<{ label: string; value: string }>,
2021
errorResolved: [] as Array<{ label: string; value: string }>,
2122
message: [] as string[]
@@ -68,7 +69,10 @@ vi.mock("toolcraft-design", () => ({
6869
info: (message: string) => loggerState.info.push(message),
6970
success: (message: string) => loggerState.success.push(message),
7071
warn: (message: string) => loggerState.warn.push(message),
71-
error: (message: string) => loggerState.error.push(message),
72+
error: (message: string) => {
73+
loggerState.errorOutputFormats.push(process.env.OUTPUT_FORMAT);
74+
loggerState.error.push(message);
75+
},
7276
resolved: (label: string, value: string) => loggerState.resolved.push({ label, value }),
7377
errorResolved: (label: string, value: string) =>
7478
loggerState.errorResolved.push({ label, value }),
@@ -249,6 +253,7 @@ function resetLoggerState(): void {
249253
loggerState.success.length = 0;
250254
loggerState.warn.length = 0;
251255
loggerState.error.length = 0;
256+
loggerState.errorOutputFormats.length = 0;
252257
loggerState.resolved.length = 0;
253258
loggerState.errorResolved.length = 0;
254259
loggerState.message.length = 0;
@@ -2137,6 +2142,29 @@ describe("runCLI", () => {
21372142
expect(process.exitCode).toBe(1);
21382143
});
21392144

2145+
it("renders handler UserError messages inside the requested output format", async () => {
2146+
const deploy = defineCommand({
2147+
name: "deploy",
2148+
params: S.Object({}),
2149+
handler: async () => {
2150+
throw new UserError("Invalid input.");
2151+
}
2152+
});
2153+
2154+
const root = defineGroup({
2155+
name: "toolcraft",
2156+
children: [deploy]
2157+
});
2158+
2159+
process.argv = ["node", "toolcraft", "deploy", "--output", "json", "--yes"];
2160+
2161+
await runCLI(root);
2162+
2163+
expect(loggerState.error).toEqual(["Invalid input."]);
2164+
expect(loggerState.errorOutputFormats).toEqual(["json"]);
2165+
expect(process.exitCode).toBe(1);
2166+
});
2167+
21402168
it("reports missing required secrets before running the handler", async () => {
21412169
const handler = vi.fn(async () => null);
21422170

packages/toolcraft/src/cli.ts

Lines changed: 109 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2461,6 +2461,40 @@ function resolveOutput(resolvedFlags: ResolvedFlags): OutputMode {
24612461
return "rich";
24622462
}
24632463

2464+
function resolveOutputFromArgv(argv: readonly string[]): OutputMode {
2465+
for (let index = 0; index < argv.length; index += 1) {
2466+
const token = argv[index] ?? "";
2467+
2468+
if (token === "--json") {
2469+
return "json";
2470+
}
2471+
if (token === "--md" || token === "--markdown") {
2472+
return "md";
2473+
}
2474+
if (token === "--output") {
2475+
const value = argv[index + 1];
2476+
if (value === "rich" || value === "md" || value === "json") {
2477+
return value;
2478+
}
2479+
if (value === "markdown") {
2480+
return "md";
2481+
}
2482+
continue;
2483+
}
2484+
if (token.startsWith("--output=")) {
2485+
const value = token.slice("--output=".length);
2486+
if (value === "rich" || value === "md" || value === "json") {
2487+
return value;
2488+
}
2489+
if (value === "markdown") {
2490+
return "md";
2491+
}
2492+
}
2493+
}
2494+
2495+
return "rich";
2496+
}
2497+
24642498
const DESIGN_SYSTEM_OUTPUT_BY_MODE = {
24652499
rich: "terminal",
24662500
md: "markdown",
@@ -4545,100 +4579,103 @@ function renderHttpError(
45454579
}
45464580
}
45474581

4548-
function handleRunError(
4582+
async function handleRunError(
45494583
error: unknown,
45504584
options: {
45514585
debugStackMode: DebugStackMode | undefined;
4586+
output: OutputMode;
45524587
verbose: boolean;
45534588
program?: CommanderCommand;
45544589
argv?: readonly string[];
45554590
rootUsageName: string;
45564591
commandPath: string;
45574592
userErrorPattern: "runtime-user" | "usage";
45584593
}
4559-
): void {
4594+
): Promise<void> {
45604595
const logger = createLogger();
45614596

4562-
if (error instanceof UserError) {
4563-
renderCliErrorPattern(
4564-
options.userErrorPattern === "usage"
4565-
? {
4566-
kind: "usage",
4567-
message: error.message,
4568-
rootUsageName: options.rootUsageName,
4569-
commandPath: options.commandPath
4570-
}
4571-
: {
4572-
kind: "runtime-user",
4573-
message: error.message
4574-
}
4575-
);
4576-
return;
4577-
}
4578-
4579-
if (error instanceof Error && error.name === "ToolcraftBugError") {
4580-
renderCliErrorPattern({
4581-
kind: "toolcraft-bug",
4582-
error,
4583-
debugStackMode: options.debugStackMode
4584-
});
4585-
return;
4586-
}
4587-
4588-
if (error instanceof CommanderError) {
4589-
process.exitCode = error.exitCode;
4590-
if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
4597+
await withOutputFormat(options.output, async () => {
4598+
if (error instanceof UserError) {
4599+
renderCliErrorPattern(
4600+
options.userErrorPattern === "usage"
4601+
? {
4602+
kind: "usage",
4603+
message: error.message,
4604+
rootUsageName: options.rootUsageName,
4605+
commandPath: options.commandPath
4606+
}
4607+
: {
4608+
kind: "runtime-user",
4609+
message: error.message
4610+
}
4611+
);
45914612
return;
45924613
}
4593-
if (error.code === "commander.unknownCommand") {
4594-
logger.error(
4595-
appendUsagePointer(
4596-
formatUnknownCommandError(error, options.program, options.argv ?? process.argv),
4597-
{
4598-
rootUsageName: options.rootUsageName,
4599-
commandPath: options.commandPath
4600-
}
4601-
)
4602-
);
4614+
4615+
if (error instanceof Error && error.name === "ToolcraftBugError") {
4616+
renderCliErrorPattern({
4617+
kind: "toolcraft-bug",
4618+
error,
4619+
debugStackMode: options.debugStackMode
4620+
});
46034621
return;
46044622
}
4605-
if (error.code === "commander.unknownOption") {
4606-
const argv = options.argv ?? process.argv;
4623+
4624+
if (error instanceof CommanderError) {
4625+
process.exitCode = error.exitCode;
4626+
if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
4627+
return;
4628+
}
4629+
if (error.code === "commander.unknownCommand") {
4630+
logger.error(
4631+
appendUsagePointer(
4632+
formatUnknownCommandError(error, options.program, options.argv ?? process.argv),
4633+
{
4634+
rootUsageName: options.rootUsageName,
4635+
commandPath: options.commandPath
4636+
}
4637+
)
4638+
);
4639+
return;
4640+
}
4641+
if (error.code === "commander.unknownOption") {
4642+
const argv = options.argv ?? process.argv;
4643+
logger.error(
4644+
appendUsagePointer(formatUnknownOptionError(error, options.program, argv), {
4645+
rootUsageName: options.rootUsageName,
4646+
commandPath:
4647+
options.commandPath.length > 0
4648+
? options.commandPath
4649+
: findCurrentCommanderCommandPath(options.program, argv)
4650+
})
4651+
);
4652+
return;
4653+
}
46074654
logger.error(
4608-
appendUsagePointer(formatUnknownOptionError(error, options.program, argv), {
4655+
appendUsagePointer(formatCommanderErrorMessage(error), {
46094656
rootUsageName: options.rootUsageName,
46104657
commandPath:
46114658
options.commandPath.length > 0
46124659
? options.commandPath
4613-
: findCurrentCommanderCommandPath(options.program, argv)
4660+
: findCurrentCommanderCommandPath(options.program, options.argv ?? process.argv)
46144661
})
46154662
);
46164663
return;
46174664
}
4618-
logger.error(
4619-
appendUsagePointer(formatCommanderErrorMessage(error), {
4620-
rootUsageName: options.rootUsageName,
4621-
commandPath:
4622-
options.commandPath.length > 0
4623-
? options.commandPath
4624-
: findCurrentCommanderCommandPath(options.program, options.argv ?? process.argv)
4625-
})
4626-
);
4627-
return;
4628-
}
46294665

4630-
if (isHttpErrorLike(error)) {
4631-
renderHttpError(error, options);
4632-
process.exitCode = 1;
4633-
return;
4634-
}
4666+
if (isHttpErrorLike(error)) {
4667+
renderHttpError(error, options);
4668+
process.exitCode = 1;
4669+
return;
4670+
}
46354671

4636-
const message = error instanceof Error ? error.message : String(error);
4637-
renderCliErrorPattern({
4638-
kind: "unexpected",
4639-
message,
4640-
stack: error instanceof Error ? error.stack : undefined,
4641-
debugStackMode: options.debugStackMode
4672+
const message = error instanceof Error ? error.message : String(error);
4673+
renderCliErrorPattern({
4674+
kind: "unexpected",
4675+
message,
4676+
stack: error instanceof Error ? error.stack : undefined,
4677+
debugStackMode: options.debugStackMode
4678+
});
46424679
});
46434680
}
46444681

@@ -5087,11 +5124,15 @@ export async function runCLI<TServices extends object = Record<string, unknown>>
50875124
process.stderr.write(`Saved error report to ${report.displayPath}\n`);
50885125
}
50895126

5090-
handleRunError(error, {
5127+
await handleRunError(error, {
50915128
debugStackMode:
50925129
resolvedFlags !== undefined
50935130
? resolveDebugStackMode(resolvedFlags.debug)
50945131
: getDebugStackModeFromArgv(process.argv),
5132+
output:
5133+
resolvedFlags !== undefined
5134+
? resolveOutput(resolvedFlags)
5135+
: resolveOutputFromArgv(process.argv),
50955136
verbose: resolvedFlags ? Boolean(resolvedFlags.verbose) : process.argv.includes("--verbose"),
50965137
program,
50975138
argv: process.argv,

0 commit comments

Comments
 (0)