fix: more pleasant error message
This commit is contained in:
+1
-1
@@ -15,7 +15,7 @@ import { up } from "./up";
|
|||||||
export const main = defineCommand({
|
export const main = defineCommand({
|
||||||
meta: {
|
meta: {
|
||||||
name: "kuber",
|
name: "kuber",
|
||||||
version: "1.2.1",
|
version: "1.2.2",
|
||||||
description: "Docker Compose -> K8s translation layer",
|
description: "Docker Compose -> K8s translation layer",
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
@@ -4,25 +4,11 @@ import { createMain } from "citty";
|
|||||||
import { initializeCompletion, main } from "./command/main";
|
import { initializeCompletion, main } from "./command/main";
|
||||||
import { provideContext } from "./lib/context";
|
import { provideContext } from "./lib/context";
|
||||||
import { extractConfigArgument } from "./lib/config";
|
import { extractConfigArgument } from "./lib/config";
|
||||||
import z, { ZodError } from "zod";
|
import { formatUnknownError, wrapCommandErrors } from "./lib/error";
|
||||||
|
|
||||||
function formatUnknownError(error: unknown): string {
|
|
||||||
if (error instanceof ZodError) return z.prettifyError(error);
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
const stack = error.stack?.split("\n").slice(0, 3).join("\n");
|
|
||||||
return stack && stack.trim().length > 0 ? stack : error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.stringify(error, null, 2);
|
|
||||||
} catch {
|
|
||||||
return String(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
await initializeCompletion();
|
await initializeCompletion();
|
||||||
|
wrapCommandErrors(main);
|
||||||
const cli = createMain(main);
|
const cli = createMain(main);
|
||||||
const { configPath, rawArgs } = extractConfigArgument(process.argv.slice(2));
|
const { configPath, rawArgs } = extractConfigArgument(process.argv.slice(2));
|
||||||
await provideContext(() => cli({ rawArgs }), configPath);
|
await provideContext(() => cli({ rawArgs }), configPath);
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import z, { ZodError } from "zod";
|
||||||
|
|
||||||
|
type RunnableCommand = {
|
||||||
|
run?: (...args: any[]) => unknown;
|
||||||
|
subCommands?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WrappedCommand = Symbol("wrappedCommand");
|
||||||
|
|
||||||
|
type WrappedCommand = RunnableCommand & {
|
||||||
|
[WrappedCommand]?: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ErrorHandler = (error: unknown) => void;
|
||||||
|
|
||||||
|
export function formatUnknownError(error: unknown): string {
|
||||||
|
if (error instanceof ZodError) return z.prettifyError(error);
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const stack = error.stack?.split("\n").slice(0, 3).join("\n");
|
||||||
|
return stack && stack.trim().length > 0 ? stack : error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.stringify(error, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportCommandError(error: unknown): void {
|
||||||
|
console.error(formatUnknownError(error));
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wrapCommandErrors(
|
||||||
|
command: RunnableCommand,
|
||||||
|
handleError: ErrorHandler = reportCommandError,
|
||||||
|
): void {
|
||||||
|
const wrapped = command as WrappedCommand;
|
||||||
|
if (wrapped[WrappedCommand]) return;
|
||||||
|
wrapped[WrappedCommand] = true;
|
||||||
|
|
||||||
|
if (command.run) {
|
||||||
|
const run = command.run;
|
||||||
|
command.run = async (...args) => {
|
||||||
|
try {
|
||||||
|
return await run(...args);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!command.subCommands || typeof command.subCommands !== "object") return;
|
||||||
|
for (const subCommand of Object.values(command.subCommands)) {
|
||||||
|
if (subCommand && typeof subCommand === "object") {
|
||||||
|
wrapCommandErrors(subCommand as RunnableCommand, handleError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dmgnr/kuber",
|
"name": "@dmgnr/kuber",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"description": "Docker Compose to Kubernetes translation layer",
|
"description": "Docker Compose to Kubernetes translation layer",
|
||||||
"bin": {
|
"bin": {
|
||||||
"kuber": "dist/index.js"
|
"kuber": "dist/index.js"
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
|
import z from "zod";
|
||||||
|
import { formatUnknownError, wrapCommandErrors } from "../../lib/error";
|
||||||
|
|
||||||
|
describe("CLI error handling", () => {
|
||||||
|
test("formats Zod errors without exposing internal parser details", () => {
|
||||||
|
const error = z.object({ name: z.string() }).safeParse({ name: 1 }).error;
|
||||||
|
expect(formatUnknownError(error)).toContain("name");
|
||||||
|
expect(formatUnknownError(error)).toContain("expected string");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("catches errors from nested command handlers before Citty sees them", async () => {
|
||||||
|
const handleError = mock((_error: unknown) => {});
|
||||||
|
const command = {
|
||||||
|
subCommands: {
|
||||||
|
child: {
|
||||||
|
async run() {
|
||||||
|
throw new Error("command failed");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapCommandErrors(command, handleError);
|
||||||
|
await expect(command.subCommands.child.run()).resolves.toBeUndefined();
|
||||||
|
expect(handleError).toHaveBeenCalledTimes(1);
|
||||||
|
expect(handleError.mock.calls[0]?.[0]).toBeInstanceOf(Error);
|
||||||
|
expect(String(handleError.mock.calls[0]?.[0])).toContain("command failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formats a dispatched command error once and exits with code 1", async () => {
|
||||||
|
const process = Bun.spawn(["bun", "index.ts", "exec"], {
|
||||||
|
cwd: `${import.meta.dir}/../..`,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
});
|
||||||
|
const [stderr, exitCode] = await Promise.all([
|
||||||
|
new Response(process.stderr).text(),
|
||||||
|
process.exited,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(exitCode).toBe(1);
|
||||||
|
expect(stderr.match(/Deployment name is required/g)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user