From 95698f47383d4bd18f0c6fab9dc128de1477c0f9 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Wed, 14 Jan 2026 22:58:41 +0100 Subject: [PATCH 1/2] fix: proper error handling in CLI --- .../cli/src/brownfield/commands/packageIos.ts | 2 +- packages/cli/src/shared/utils/cli.ts | 28 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/brownfield/commands/packageIos.ts b/packages/cli/src/brownfield/commands/packageIos.ts index e2c42e60..2075a1f6 100644 --- a/packages/cli/src/brownfield/commands/packageIos.ts +++ b/packages/cli/src/brownfield/commands/packageIos.ts @@ -47,7 +47,7 @@ export const packageIosCommand = curryOptions( options.buildFolder ??= path.join(brownieCacheDir, 'build'); - packageIosAction( + await packageIosAction( options, { projectRoot, diff --git a/packages/cli/src/shared/utils/cli.ts b/packages/cli/src/shared/utils/cli.ts index 91f7ed00..aa1b2975 100644 --- a/packages/cli/src/shared/utils/cli.ts +++ b/packages/cli/src/shared/utils/cli.ts @@ -1,4 +1,4 @@ -import { logger, type RockCLIOptions } from '@rock-js/tools'; +import { logger, RockError, type RockCLIOptions } from '@rock-js/tools'; import type { Command } from 'commander'; @@ -23,11 +23,25 @@ export function curryOptions(programCommand: Command, options: RockCLIOptions) { return programCommand; } -function handleActionError(error: Error) { - logger.error(`Error running command: ${error.message}`); - process.exit(1); -} - export function actionRunner(fn: (...args: T[]) => Promise) { - return (...args: T[]) => fn(...args).catch(handleActionError); + return async function wrappedCLIAction(...args: T[]) { + try { + await fn(...args); + } catch (error) { + if (error instanceof RockError) { + if (logger.isVerbose()) { + logger.error(error); + } else { + logger.error(error.message); + if (error.cause) { + logger.error(`Cause: ${error.cause}`); + } + } + } else { + logger.error(`Unexpected error while running command:`, error); + } + + process.exit(1); + } + }; } From 67d696c86d962fea04ac963ffafc1aa0ce7581f7 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Wed, 14 Jan 2026 23:33:00 +0100 Subject: [PATCH 2/2] test: add CLI error handling tests --- .../src/shared/utils/__tests__/cli.test.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 packages/cli/src/shared/utils/__tests__/cli.test.ts diff --git a/packages/cli/src/shared/utils/__tests__/cli.test.ts b/packages/cli/src/shared/utils/__tests__/cli.test.ts new file mode 100644 index 00000000..b9d91cd3 --- /dev/null +++ b/packages/cli/src/shared/utils/__tests__/cli.test.ts @@ -0,0 +1,62 @@ +import * as rockTools from '@rock-js/tools'; + +import { expect, Mock, test, vi } from 'vitest'; + +import { actionRunner } from '../cli.js'; + +vi.mock('@rock-js/tools', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + logger: { + ...actual.logger, + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + success: vi.fn(), + }, + }; +}); + +// @ts-expect-error - override typings +const processExitMock = vi.spyOn(process, 'exit').mockImplementation(() => { + // no-op +}); + +const mockLoggerError = rockTools.logger.error as Mock; + +const FAILING_ACTION_ERROR_MESSAGE = 'Test error'; + +const createWrappedFailingAction = (ErrorCls: new (message: string) => Error) => + actionRunner(async (_a: number, _b: number) => { + throw new ErrorCls(FAILING_ACTION_ERROR_MESSAGE); + }); + +test('actionRunner should call the wrapped function', async () => { + const mockAction = vi.fn(async () => Promise.resolve()); + const wrappedAction = actionRunner(mockAction); + + await wrappedAction(); + + expect(mockAction).toHaveBeenCalledOnce(); +}); + +test('actionRunner should gracefully handle Errors', async () => { + const wrappedActionExpectation = expect( + createWrappedFailingAction(Error)(1, 2) + ); + + await wrappedActionExpectation.resolves.not.toThrowError(); + expect(processExitMock).toHaveBeenCalledExactlyOnceWith(1); + expect(mockLoggerError).toHaveBeenCalled(); +}); + +test('actionRunner should gracefully handle RockErrors', async () => { + const wrappedActionExpectation = expect( + createWrappedFailingAction(rockTools.RockError)(1, 2) + ); + + await wrappedActionExpectation.resolves.not.toThrowError(); + expect(processExitMock).toHaveBeenCalledExactlyOnceWith(1); + expect(mockLoggerError).toHaveBeenCalled(); +});