diff --git a/.changeset/fresh-turtles-stand.md b/.changeset/fresh-turtles-stand.md new file mode 100644 index 0000000000..b33b0f4a24 --- /dev/null +++ b/.changeset/fresh-turtles-stand.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Internal refactor of error handling diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts index ef97270569..aee6d3f575 100644 --- a/packages/cli/src/lib/errors.ts +++ b/packages/cli/src/lib/errors.ts @@ -14,15 +14,10 @@ * limitations under the License. */ +import { CustomErrorBase, isError, stringifyError } from '@backstage/errors'; import chalk from 'chalk'; -export class CustomError extends Error { - get name(): string { - return this.constructor.name; - } -} - -export class ExitCodeError extends CustomError { +export class ExitCodeError extends CustomErrorBase { readonly code: number; constructor(code: number, command?: string) { @@ -35,14 +30,33 @@ export class ExitCodeError extends CustomError { } } -export function exitWithError(error: Error): never { - if (error instanceof ExitCodeError) { - process.stderr.write(`\n${chalk.red(error.message)}\n\n`); - process.exit(error.code); - } else { - process.stderr.write(`\n${chalk.red(`${error.stack}`)}\n\n`); - process.exit(1); - } +function exit(message: string, code: number = 1): never { + process.stderr.write(`\n${chalk.red(message)}\n\n`); + process.exit(code); } -export class NotFoundError extends CustomError {} +export function exitWithError(error: unknown): never { + if (!isError(error)) { + process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); + process.exit(1); + } + + switch (error.name) { + case 'InputError': + return exit(error.message, 74 /* input/output error */); + case 'NotFoundError': + return exit(error.message, 127 /* command not found */); + case 'NotImplementedError': + return exit(error.message, 64 /* command line usage error */); + case 'AuthenticationError': + case 'NotAllowedError': + return exit(error.message, 77 /* permissino denied */); + case 'ExitCodeError': + return exit( + error.message, + 'code' in error && typeof error.code === 'number' ? error.code : 1, + ); + default: + return exit(stringifyError(error), 1); + } +} diff --git a/packages/cli/src/lib/removed.ts b/packages/cli/src/lib/removed.ts deleted file mode 100644 index 063882be18..0000000000 --- a/packages/cli/src/lib/removed.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export function removed(message?: string) { - return () => { - console.error( - message - ? `This command has been removed, ${message}` - : 'This command has been removed', - ); - process.exit(1); - }; -} diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli/src/lib/versioning/packages.test.ts index 78fb1d1f5e..de6e7e1ed2 100644 --- a/packages/cli/src/lib/versioning/packages.test.ts +++ b/packages/cli/src/lib/versioning/packages.test.ts @@ -17,8 +17,8 @@ import * as runObj from '../run'; import * as yarn from './yarn'; import { fetchPackageInfo, mapDependencies } from './packages'; -import { NotFoundError } from '../errors'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { NotFoundError } from '@backstage/errors'; jest.mock('../run', () => { return { diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli/src/lib/versioning/packages.ts index 7a75189a5f..223f6b8aab 100644 --- a/packages/cli/src/lib/versioning/packages.ts +++ b/packages/cli/src/lib/versioning/packages.ts @@ -16,9 +16,9 @@ import { minimatch } from 'minimatch'; import { getPackages } from '@manypkg/get-packages'; -import { NotFoundError } from '../errors'; import { detectYarnVersion } from './yarn'; import { execFile } from '../run'; +import { NotFoundError } from '@backstage/errors'; const DEP_TYPES = [ 'dependencies', diff --git a/packages/cli/src/modules/new/index.ts b/packages/cli/src/modules/new/index.ts index 6802077507..120e77c7c0 100644 --- a/packages/cli/src/modules/new/index.ts +++ b/packages/cli/src/modules/new/index.ts @@ -16,7 +16,7 @@ import { createCliPlugin } from '../../wiring/factory'; import { Command } from 'commander'; import { lazy } from '../../lib/lazy'; -import { removed } from '../../lib/removed'; +import { NotImplementedError } from '@backstage/errors'; export default createCliPlugin({ pluginId: 'new', @@ -71,7 +71,9 @@ export default createCliPlugin({ description: 'Create a new Backstage app', deprecated: true, execute: async () => { - removed("use 'backstage-cli new' instead")(); + throw new NotImplementedError( + `This command has been removed, use 'backstage-cli new' instead`, + ); }, }); reg.addCommand({ @@ -79,7 +81,9 @@ export default createCliPlugin({ description: 'Create a new Backstage plugin', deprecated: true, execute: async () => { - removed("use 'backstage-cli new' instead")(); + throw new NotImplementedError( + `This command has been removed, use 'backstage-cli new' instead`, + ); }, }); }, diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index 778700fe6b..15ac9837d0 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -21,7 +21,7 @@ import { Command } from 'commander'; import { version } from '../lib/version'; import chalk from 'chalk'; import { exitWithError } from '../lib/errors'; -import { assertError } from '@backstage/errors'; +import { ForwardedError } from '@backstage/errors'; import { isPromise } from 'util/types'; type UninitializedFeature = CliFeature | Promise<{ default: CliFeature }>; @@ -126,8 +126,7 @@ export class CliInitializer { }, }); process.exit(0); - } catch (error) { - assertError(error); + } catch (error: unknown) { exitWithError(error); } }); @@ -142,11 +141,7 @@ export class CliInitializer { }); process.on('unhandledRejection', rejection => { - if (rejection instanceof Error) { - exitWithError(rejection); - } else { - exitWithError(new Error(`Unknown rejection: '${rejection}'`)); - } + exitWithError(new ForwardedError('Unhandled rejection', rejection)); }); program.parse(process.argv);