From f722affc2be7cd7117fb5923580dbd11f3eada13 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:17:09 +0000 Subject: [PATCH] fix(@angular/cli): gracefully handle package manager errors in command handler Catch `PackageManagerError` in the global `CommandModule` handler and gracefully log the installation failure message along with the process output (stdout/stderr) using `logger.fatal`. This ensures that any package manager error thrown during the execution of a command is reported cleanly to the terminal and sets the exit code to 1. --- .../cli/src/command-builder/command-module.ts | 15 ++-- packages/angular/cli/src/commands/add/cli.ts | 72 ++++++++----------- 2 files changed, 38 insertions(+), 49 deletions(-) diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts index b0d15d70d0b2..ae0eb82f0cb5 100644 --- a/packages/angular/cli/src/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -14,6 +14,7 @@ import { Parser as yargsParser } from 'yargs/helpers'; import { getAnalyticsUserId } from '../analytics/analytics'; import { AnalyticsCollector } from '../analytics/analytics-collector'; import { EventCustomDimension, EventCustomMetric } from '../analytics/analytics-parameters'; +import { PackageManagerError } from '../package-managers'; import { considerSettingUpAutocompletion } from '../utilities/completion'; import { AngularWorkspace } from '../utilities/config'; import { memoize } from '../utilities/memoize'; @@ -95,6 +96,7 @@ export abstract class CommandModule implements CommandModuleI async handler(args: ArgumentsCamelCase & OtherOptions): Promise { const { _, $0, ...options } = args; + const { logger } = this.context; // Camelize options as yargs will return the object in kebab-case when camel casing is disabled. const camelCasedOptions: Record = {}; @@ -103,10 +105,7 @@ export abstract class CommandModule implements CommandModuleI } // Set up autocompletion if appropriate. - const autocompletionExitCode = await considerSettingUpAutocompletion( - this.commandName, - this.context.logger, - ); + const autocompletionExitCode = await considerSettingUpAutocompletion(this.commandName, logger); if (autocompletionExitCode !== undefined) { process.exitCode = autocompletionExitCode; @@ -127,7 +126,13 @@ export abstract class CommandModule implements CommandModuleI exitCode = await this.run(camelCasedOptions as Options & OtherOptions); } catch (e) { if (e instanceof schema.SchemaValidationException) { - this.context.logger.fatal(`Error: ${e.message}`); + logger.fatal(`Error: ${e.message}`); + exitCode = 1; + } else if (e instanceof PackageManagerError) { + const output = e.stderr || e.stdout; + logger.fatal( + `Error: Package installation failed: ${e.message}${output ? `\nOutput: ${output}` : ''}`, + ); exitCode = 1; } else { throw e; diff --git a/packages/angular/cli/src/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts index dd78b7038120..a3a3d6f7ecd2 100644 --- a/packages/angular/cli/src/commands/add/cli.ts +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -25,12 +25,7 @@ import { SchematicsCommandArgs, SchematicsCommandModule, } from '../../command-builder/schematics-command-module'; -import { - NgAddSaveDependency, - PackageManagerError, - PackageManifest, - PackageMetadata, -} from '../../package-managers'; +import { NgAddSaveDependency, PackageManifest, PackageMetadata } from '../../package-managers'; import { assertIsError } from '../../utilities/error'; import { isTTY } from '../../utilities/tty'; import { VERSION } from '../../utilities/version'; @@ -639,47 +634,36 @@ export default class AddCommandModule // Only show if installation will actually occur task.title = 'Installing package'; - try { - if (context.savePackage === false) { - task.title += ' in temporary location'; - - // Temporary packages are located in a different directory - // Hence we need to resolve them using the temp path - const { workingDirectory } = await packageManager.acquireTempPackage( - packageIdentifier.toString(), - { - registry, - }, - ); + if (context.savePackage === false) { + task.title += ' in temporary location'; - const tempRequire = createRequire(workingDirectory + '/'); - assert(context.collectionName, 'Collection name should always be available'); - const resolvedCollectionPath = tempRequire.resolve( - join(context.collectionName, 'package.json'), - ); + // Temporary packages are located in a different directory + // Hence we need to resolve them using the temp path + const { workingDirectory } = await packageManager.acquireTempPackage( + packageIdentifier.toString(), + { + registry, + }, + ); - context.collectionName = dirname(resolvedCollectionPath); - } else { - await packageManager.add( - packageIdentifier.toString(), - 'none', - savePackage === 'devDependencies', - false, - true, - { - registry, - }, - ); - } - } catch (e) { - if (e instanceof PackageManagerError) { - const output = e.stderr || e.stdout; - if (output) { - throw new CommandError(`Package installation failed: ${e.message}\nOutput: ${output}`); - } - } + const tempRequire = createRequire(workingDirectory + '/'); + assert(context.collectionName, 'Collection name should always be available'); + const resolvedCollectionPath = tempRequire.resolve( + join(context.collectionName, 'package.json'), + ); - throw e; + context.collectionName = dirname(resolvedCollectionPath); + } else { + await packageManager.add( + packageIdentifier.toString(), + 'none', + savePackage === 'devDependencies', + false, + true, + { + registry, + }, + ); } }