cli: internal error handling refactor

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-08-17 21:15:05 +02:00
parent c62f43adeb
commit 2b8082a889
7 changed files with 47 additions and 54 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Internal refactor of error handling
+30 -16
View File
@@ -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);
}
}
-25
View File
@@ -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);
};
}
@@ -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 {
+1 -1
View File
@@ -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',
+7 -3
View File
@@ -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`,
);
},
});
},
+3 -8
View File
@@ -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);