diff --git a/packages/angular/cli/commands/add-impl.ts b/packages/angular/cli/commands/add-impl.ts index d61d728159e7..95e49bd7ebb6 100644 --- a/packages/angular/cli/commands/add-impl.ts +++ b/packages/angular/cli/commands/add-impl.ts @@ -22,7 +22,9 @@ import { fetchPackageManifest, fetchPackageMetadata, } from '../utilities/package-metadata'; +import { askConfirmation } from '../utilities/prompt'; import { Spinner } from '../utilities/spinner'; +import { isTTY } from '../utilities/tty'; import { Schema as AddCommandSchema } from './add'; const npa = require('npm-package-arg'); @@ -179,6 +181,28 @@ export class AddCommand extends SchematicCommand { return 1; } + if (!options.skipConfirmation) { + const confirmationResponse = await askConfirmation( + `\nThe package ${colors.blue(packageIdentifier.raw)} will be installed and executed.\n` + + 'Would you like to proceed?', + true, + false, + ); + + if (!confirmationResponse) { + if (!isTTY) { + this.logger.error( + 'No terminal detected. ' + + `'--skip-confirmation' can be used to bypass installation confirmation. ` + + `Ensure package name is correct prior to '--skip-confirmation' option usage.`, + ); + } + this.logger.error('Command aborted.'); + + return 1; + } + } + try { spinner.start('Installing package...'); if (savePackage === false) { diff --git a/packages/angular/cli/commands/add.json b/packages/angular/cli/commands/add.json index 45679b0faf4d..bb3430bdeb33 100644 --- a/packages/angular/cli/commands/add.json +++ b/packages/angular/cli/commands/add.json @@ -35,6 +35,11 @@ "description": "Display additional details about internal operations during execution.", "type": "boolean", "default": false + }, + "skipConfirmation": { + "description": "Skip asking a confirmation prompt before installing and executing the package. Ensure package name is correct prior to using this option.", + "type": "boolean", + "default": false } }, "required": [ diff --git a/packages/angular/cli/utilities/prompt.ts b/packages/angular/cli/utilities/prompt.ts new file mode 100644 index 000000000000..fcbd0c2ea572 --- /dev/null +++ b/packages/angular/cli/utilities/prompt.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as inquirer from 'inquirer'; +import { isTTY } from './tty'; + +export async function askConfirmation( + message: string, + defaultResponse: boolean, + noTTYResponse?: boolean, +): Promise { + if (!isTTY()) { + return noTTYResponse ?? defaultResponse; + } + + const question: inquirer.Question = { + type: 'confirm', + name: 'confirmation', + prefix: '', + message, + default: defaultResponse, + }; + + const answers = await inquirer.prompt([question]); + + return answers['confirmation']; +} diff --git a/tests/legacy-cli/e2e/tests/build/material.ts b/tests/legacy-cli/e2e/tests/build/material.ts index ad31b4769341..5963d3411d89 100644 --- a/tests/legacy-cli/e2e/tests/build/material.ts +++ b/tests/legacy-cli/e2e/tests/build/material.ts @@ -8,7 +8,7 @@ const snapshots = require('../../ng-snapshot/package.json'); export default async function () { const tag = await isPrereleaseCli() ? '@next' : ''; - await ng('add', `@angular/material${tag}`); + await ng('add', `@angular/material${tag}`, '--skip-confirmation'); const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; if (isSnapshotBuild) { diff --git a/tests/legacy-cli/e2e/tests/commands/add/add-material.ts b/tests/legacy-cli/e2e/tests/commands/add/add-material.ts index 4f289095de13..39496611be33 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/add-material.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/add-material.ts @@ -11,14 +11,14 @@ export default async function () { const tag = await isPrereleaseCli() ? '@next' : ''; try { - await ng('add', `@angular/material${tag}`, '--unknown'); + await ng('add', `@angular/material${tag}`, '--unknown', '--skip-confirmation'); } catch (error) { if (!(error.message && error.message.includes(`Unknown option: '--unknown'`))) { throw error; } } - await ng('add', `@angular/material${tag}`, '--theme', 'custom', '--verbose'); + await ng('add', `@angular/material${tag}`, '--theme', 'custom', '--verbose', '--skip-confirmation'); await expectFileToMatch('package.json', /@angular\/material/); // Clean up existing cdk package diff --git a/tests/legacy-cli/e2e/tests/commands/add/add-pwa-yarn.ts b/tests/legacy-cli/e2e/tests/commands/add/add-pwa-yarn.ts index 1da9c3a105c8..60943b2dc600 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/add-pwa-yarn.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/add-pwa-yarn.ts @@ -13,7 +13,7 @@ export default async function () { // set yarn as package manager await ng('config', 'cli.packageManager', 'yarn'); - await ng('add', '@angular/pwa'); + await ng('add', '@angular/pwa', '--skip-confirmation'); await expectFileToExist(join(process.cwd(), 'src/manifest.webmanifest')); // Angular PWA doesn't install as a dependency diff --git a/tests/legacy-cli/e2e/tests/commands/add/add-pwa.ts b/tests/legacy-cli/e2e/tests/commands/add/add-pwa.ts index 416711c52d69..45672d13a94d 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/add-pwa.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/add-pwa.ts @@ -5,7 +5,7 @@ import { ng } from '../../../utils/process'; export default async function () { // forcibly remove in case another test doesn't clean itself up await rimraf('node_modules/@angular/pwa'); - await ng('add', '@angular/pwa'); + await ng('add', '@angular/pwa', '--skip-confirmation'); await expectFileToExist(join(process.cwd(), 'src/manifest.webmanifest')); // Angular PWA doesn't install as a dependency diff --git a/tests/legacy-cli/e2e/tests/commands/add/add-version.ts b/tests/legacy-cli/e2e/tests/commands/add/add-version.ts index 0fcdbc0c1fa3..bdbf9c96bd64 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/add-version.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/add-version.ts @@ -3,7 +3,7 @@ import { ng } from '../../../utils/process'; export default async function () { - await ng('add', '@angular-devkit-tests/ng-add-simple@^1.0.0'); + await ng('add', '@angular-devkit-tests/ng-add-simple@^1.0.0', '--skip-confirmation'); await expectFileToMatch('package.json', /\/ng-add-simple.*\^1\.0\.0/); await expectFileToExist('ng-add-test'); await rimraf('node_modules/@angular-devkit-tests/ng-add-simple'); diff --git a/tests/legacy-cli/e2e/tests/commands/add/add.ts b/tests/legacy-cli/e2e/tests/commands/add/add.ts index 87aee9c82441..bf3ae5e3d0b5 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/add.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/add.ts @@ -3,7 +3,7 @@ import { ng } from '../../../utils/process'; export default async function () { - await ng('add', '@angular-devkit-tests/ng-add-simple'); + await ng('add', '@angular-devkit-tests/ng-add-simple', '--skip-confirmation'); await expectFileToMatch('package.json', /@angular-devkit-tests\/ng-add-simple/); await expectFileToExist('ng-add-test'); await rimraf('node_modules/@angular-devkit-tests/ng-add-simple'); diff --git a/tests/legacy-cli/e2e/tests/commands/add/dir.ts b/tests/legacy-cli/e2e/tests/commands/add/dir.ts index d3c4b01d82e7..695c9369a43b 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/dir.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/dir.ts @@ -4,6 +4,6 @@ import { ng } from '../../../utils/process'; export default async function () { - await ng('add', assetDir('add-collection'), '--name=blah'); + await ng('add', assetDir('add-collection'), '--name=blah', '--skip-confirmation'); await expectFileToExist('blah'); } diff --git a/tests/legacy-cli/e2e/tests/commands/add/file.ts b/tests/legacy-cli/e2e/tests/commands/add/file.ts index 7cd61da0374b..d05abdb9e0f3 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/file.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/file.ts @@ -4,6 +4,6 @@ import { ng } from '../../../utils/process'; export default async function () { - await ng('add', assetDir('add-collection.tgz'), '--name=blah'); + await ng('add', assetDir('add-collection.tgz'), '--name=blah', '--skip-confirmation'); await expectFileToExist('blah'); } diff --git a/tests/legacy-cli/e2e/tests/commands/add/peer.ts b/tests/legacy-cli/e2e/tests/commands/add/peer.ts index ee1ece6dab6a..2f5147df0b03 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/peer.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/peer.ts @@ -4,17 +4,17 @@ import { ng } from '../../../utils/process'; const warning = 'Adding the package may not succeed.'; export default async function () { - const { stderr: bad } = await ng('add', assetDir('add-collection-peer-bad')); + const { stderr: bad } = await ng('add', assetDir('add-collection-peer-bad'), '--skip-confirmation'); if (!bad.includes(warning)) { throw new Error('peer warning not shown on bad package'); } - const { stderr: base } = await ng('add', assetDir('add-collection')); + const { stderr: base } = await ng('add', assetDir('add-collection'), '--skip-confirmation'); if (base.includes(warning)) { throw new Error('peer warning shown on base package'); } - const { stderr: good } = await ng('add', assetDir('add-collection-peer-good')); + const { stderr: good } = await ng('add', assetDir('add-collection-peer-good'), '--skip-confirmation'); if (good.includes(warning)) { throw new Error('peer warning shown on good package'); } diff --git a/tests/legacy-cli/e2e/tests/commands/add/registry-option.ts b/tests/legacy-cli/e2e/tests/commands/add/registry-option.ts index 650cee769bb3..8a9e31dcb4d6 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/registry-option.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/registry-option.ts @@ -15,9 +15,9 @@ export default async function () { process.env['NPM_CONFIG_REGISTRY'] = undefined; try { - await expectToFail(() => ng('add', '@angular/pwa')); + await expectToFail(() => ng('add', '@angular/pwa', '--skip-confirmation')); - await ng('add', `--registry=${testRegistry}`, '@angular/pwa'); + await ng('add', `--registry=${testRegistry}`, '@angular/pwa', '--skip-confirmation'); await expectFileToExist('src/manifest.webmanifest'); } finally { process.env['NPM_CONFIG_REGISTRY'] = originalRegistryVariable; diff --git a/tests/legacy-cli/e2e/tests/commands/add/version-specifier.ts b/tests/legacy-cli/e2e/tests/commands/add/version-specifier.ts index df4903d3c29d..f7c8cdd9f820 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/version-specifier.ts +++ b/tests/legacy-cli/e2e/tests/commands/add/version-specifier.ts @@ -10,25 +10,25 @@ export default async function () { const tag = await isPrereleaseCli() ? '@next' : ''; - await ng('add', `@angular/localize${tag}`); + await ng('add', `@angular/localize${tag}`, '--skip-confirmation'); await expectFileToMatch('package.json', /@angular\/localize/); - const output1 = await ng('add', '@angular/localize'); + const output1 = await ng('add', '@angular/localize', '--skip-confirmation'); if (!output1.stdout.includes('Skipping installation: Package already installed')) { throw new Error('Installation was not skipped'); } - const output2 = await ng('add', '@angular/localize@latest'); + const output2 = await ng('add', '@angular/localize@latest', '--skip-confirmation'); if (output2.stdout.includes('Skipping installation: Package already installed')) { throw new Error('Installation should not have been skipped'); } - const output3 = await ng('add', '@angular/localize@10.0.0'); + const output3 = await ng('add', '@angular/localize@10.0.0', '--skip-confirmation'); if (output3.stdout.includes('Skipping installation: Package already installed')) { throw new Error('Installation should not have been skipped'); } - const output4 = await ng('add', '@angular/localize@10'); + const output4 = await ng('add', '@angular/localize@10', '--skip-confirmation'); if (!output4.stdout.includes('Skipping installation: Package already installed')) { throw new Error('Installation was not skipped'); } diff --git a/tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts b/tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts index 29cc95795a92..4b8c05f04737 100644 --- a/tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts +++ b/tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts @@ -29,7 +29,7 @@ export default async function () { await installPackage('@schematics/angular@7'); const tag = (await isPrereleaseCli()) ? '@next' : ''; - await ng('add', `@angular/material${tag}`); + await ng('add', `@angular/material${tag}`, '--skip-confirmation'); await expectFileToMatch('package.json', /@angular\/material/); // Clean up existing cdk package