Skip to content

Commit 985dc1a

Browse files
clydinalan-agius4
authored andcommitted
feat(@angular/cli): confirm ng add action before installation
BREAKING CHANGE: The `ng add` command will now ask the user to confirm the package and version prior to installing and executing an uninstalled package. This new behavior allows a user to abort the action if the version selected is not appropriate or if a typo occurred on the command line and an incorrect package would be installed. A `--skip-confirmation` option has been added to skip the prompt and directly install and execute the package. This option is useful in CI and non-TTY scenarios such as automated scripts.
1 parent 3c2f583 commit 985dc1a

File tree

15 files changed

+80
-20
lines changed

15 files changed

+80
-20
lines changed

packages/angular/cli/commands/add-impl.ts

+24
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import {
2222
fetchPackageManifest,
2323
fetchPackageMetadata,
2424
} from '../utilities/package-metadata';
25+
import { askConfirmation } from '../utilities/prompt';
2526
import { Spinner } from '../utilities/spinner';
27+
import { isTTY } from '../utilities/tty';
2628
import { Schema as AddCommandSchema } from './add';
2729

2830
const npa = require('npm-package-arg');
@@ -179,6 +181,28 @@ export class AddCommand extends SchematicCommand<AddCommandSchema> {
179181
return 1;
180182
}
181183

184+
if (!options.skipConfirmation) {
185+
const confirmationResponse = await askConfirmation(
186+
`\nThe package ${colors.blue(packageIdentifier.raw)} will be installed and executed.\n` +
187+
'Would you like to proceed?',
188+
true,
189+
false,
190+
);
191+
192+
if (!confirmationResponse) {
193+
if (!isTTY) {
194+
this.logger.error(
195+
'No terminal detected. ' +
196+
`'--skip-confirmation' can be used to bypass installation confirmation. ` +
197+
`Ensure package name is correct prior to '--skip-confirmation' option usage.`,
198+
);
199+
}
200+
this.logger.error('Command aborted.');
201+
202+
return 1;
203+
}
204+
}
205+
182206
try {
183207
spinner.start('Installing package...');
184208
if (savePackage === false) {

packages/angular/cli/commands/add.json

+5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
"description": "Display additional details about internal operations during execution.",
3636
"type": "boolean",
3737
"default": false
38+
},
39+
"skipConfirmation": {
40+
"description": "Skip asking a confirmation prompt before installing and executing the package. Ensure package name is correct prior to using this option.",
41+
"type": "boolean",
42+
"default": false
3843
}
3944
},
4045
"required": [
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import * as inquirer from 'inquirer';
9+
import { isTTY } from './tty';
10+
11+
export async function askConfirmation(
12+
message: string,
13+
defaultResponse: boolean,
14+
noTTYResponse?: boolean,
15+
): Promise<boolean> {
16+
if (!isTTY()) {
17+
return noTTYResponse ?? defaultResponse;
18+
}
19+
20+
const question: inquirer.Question = {
21+
type: 'confirm',
22+
name: 'confirmation',
23+
prefix: '',
24+
message,
25+
default: defaultResponse,
26+
};
27+
28+
const answers = await inquirer.prompt([question]);
29+
30+
return answers['confirmation'];
31+
}

tests/legacy-cli/e2e/tests/build/material.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const snapshots = require('../../ng-snapshot/package.json');
88

99
export default async function () {
1010
const tag = await isPrereleaseCli() ? '@next' : '';
11-
await ng('add', `@angular/material${tag}`);
11+
await ng('add', `@angular/material${tag}`, '--skip-confirmation');
1212

1313
const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots'];
1414
if (isSnapshotBuild) {

tests/legacy-cli/e2e/tests/commands/add/add-material.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ export default async function () {
1111
const tag = await isPrereleaseCli() ? '@next' : '';
1212

1313
try {
14-
await ng('add', `@angular/material${tag}`, '--unknown');
14+
await ng('add', `@angular/material${tag}`, '--unknown', '--skip-confirmation');
1515
} catch (error) {
1616
if (!(error.message && error.message.includes(`Unknown option: '--unknown'`))) {
1717
throw error;
1818
}
1919
}
2020

21-
await ng('add', `@angular/material${tag}`, '--theme', 'custom', '--verbose');
21+
await ng('add', `@angular/material${tag}`, '--theme', 'custom', '--verbose', '--skip-confirmation');
2222
await expectFileToMatch('package.json', /@angular\/material/);
2323

2424
// Clean up existing cdk package

tests/legacy-cli/e2e/tests/commands/add/add-pwa-yarn.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default async function () {
1313

1414
// set yarn as package manager
1515
await ng('config', 'cli.packageManager', 'yarn');
16-
await ng('add', '@angular/pwa');
16+
await ng('add', '@angular/pwa', '--skip-confirmation');
1717
await expectFileToExist(join(process.cwd(), 'src/manifest.webmanifest'));
1818

1919
// Angular PWA doesn't install as a dependency

tests/legacy-cli/e2e/tests/commands/add/add-pwa.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ng } from '../../../utils/process';
55
export default async function () {
66
// forcibly remove in case another test doesn't clean itself up
77
await rimraf('node_modules/@angular/pwa');
8-
await ng('add', '@angular/pwa');
8+
await ng('add', '@angular/pwa', '--skip-confirmation');
99
await expectFileToExist(join(process.cwd(), 'src/manifest.webmanifest'));
1010

1111
// Angular PWA doesn't install as a dependency

tests/legacy-cli/e2e/tests/commands/add/add-version.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ng } from '../../../utils/process';
33

44

55
export default async function () {
6-
await ng('add', '@angular-devkit-tests/ng-add-simple@^1.0.0');
6+
await ng('add', '@angular-devkit-tests/ng-add-simple@^1.0.0', '--skip-confirmation');
77
await expectFileToMatch('package.json', /\/ng-add-simple.*\^1\.0\.0/);
88
await expectFileToExist('ng-add-test');
99
await rimraf('node_modules/@angular-devkit-tests/ng-add-simple');

tests/legacy-cli/e2e/tests/commands/add/add.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ng } from '../../../utils/process';
33

44

55
export default async function () {
6-
await ng('add', '@angular-devkit-tests/ng-add-simple');
6+
await ng('add', '@angular-devkit-tests/ng-add-simple', '--skip-confirmation');
77
await expectFileToMatch('package.json', /@angular-devkit-tests\/ng-add-simple/);
88
await expectFileToExist('ng-add-test');
99
await rimraf('node_modules/@angular-devkit-tests/ng-add-simple');

tests/legacy-cli/e2e/tests/commands/add/dir.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ import { ng } from '../../../utils/process';
44

55

66
export default async function () {
7-
await ng('add', assetDir('add-collection'), '--name=blah');
7+
await ng('add', assetDir('add-collection'), '--name=blah', '--skip-confirmation');
88
await expectFileToExist('blah');
99
}

tests/legacy-cli/e2e/tests/commands/add/file.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ import { ng } from '../../../utils/process';
44

55

66
export default async function () {
7-
await ng('add', assetDir('add-collection.tgz'), '--name=blah');
7+
await ng('add', assetDir('add-collection.tgz'), '--name=blah', '--skip-confirmation');
88
await expectFileToExist('blah');
99
}

tests/legacy-cli/e2e/tests/commands/add/peer.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ import { ng } from '../../../utils/process';
44
const warning = 'Adding the package may not succeed.';
55

66
export default async function () {
7-
const { stderr: bad } = await ng('add', assetDir('add-collection-peer-bad'));
7+
const { stderr: bad } = await ng('add', assetDir('add-collection-peer-bad'), '--skip-confirmation');
88
if (!bad.includes(warning)) {
99
throw new Error('peer warning not shown on bad package');
1010
}
1111

12-
const { stderr: base } = await ng('add', assetDir('add-collection'));
12+
const { stderr: base } = await ng('add', assetDir('add-collection'), '--skip-confirmation');
1313
if (base.includes(warning)) {
1414
throw new Error('peer warning shown on base package');
1515
}
1616

17-
const { stderr: good } = await ng('add', assetDir('add-collection-peer-good'));
17+
const { stderr: good } = await ng('add', assetDir('add-collection-peer-good'), '--skip-confirmation');
1818
if (good.includes(warning)) {
1919
throw new Error('peer warning shown on good package');
2020
}

tests/legacy-cli/e2e/tests/commands/add/registry-option.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ export default async function () {
1515
process.env['NPM_CONFIG_REGISTRY'] = undefined;
1616

1717
try {
18-
await expectToFail(() => ng('add', '@angular/pwa'));
18+
await expectToFail(() => ng('add', '@angular/pwa', '--skip-confirmation'));
1919

20-
await ng('add', `--registry=${testRegistry}`, '@angular/pwa');
20+
await ng('add', `--registry=${testRegistry}`, '@angular/pwa', '--skip-confirmation');
2121
await expectFileToExist('src/manifest.webmanifest');
2222
} finally {
2323
process.env['NPM_CONFIG_REGISTRY'] = originalRegistryVariable;

tests/legacy-cli/e2e/tests/commands/add/version-specifier.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,25 @@ export default async function () {
1010

1111
const tag = await isPrereleaseCli() ? '@next' : '';
1212

13-
await ng('add', `@angular/localize${tag}`);
13+
await ng('add', `@angular/localize${tag}`, '--skip-confirmation');
1414
await expectFileToMatch('package.json', /@angular\/localize/);
1515

16-
const output1 = await ng('add', '@angular/localize');
16+
const output1 = await ng('add', '@angular/localize', '--skip-confirmation');
1717
if (!output1.stdout.includes('Skipping installation: Package already installed')) {
1818
throw new Error('Installation was not skipped');
1919
}
2020

21-
const output2 = await ng('add', '@angular/localize@latest');
21+
const output2 = await ng('add', '@angular/localize@latest', '--skip-confirmation');
2222
if (output2.stdout.includes('Skipping installation: Package already installed')) {
2323
throw new Error('Installation should not have been skipped');
2424
}
2525

26-
const output3 = await ng('add', '@angular/[email protected]');
26+
const output3 = await ng('add', '@angular/[email protected]', '--skip-confirmation');
2727
if (output3.stdout.includes('Skipping installation: Package already installed')) {
2828
throw new Error('Installation should not have been skipped');
2929
}
3030

31-
const output4 = await ng('add', '@angular/localize@10');
31+
const output4 = await ng('add', '@angular/localize@10', '--skip-confirmation');
3232
if (!output4.stdout.includes('Skipping installation: Package already installed')) {
3333
throw new Error('Installation was not skipped');
3434
}

tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default async function () {
2929
await installPackage('@schematics/angular@7');
3030

3131
const tag = (await isPrereleaseCli()) ? '@next' : '';
32-
await ng('add', `@angular/material${tag}`);
32+
await ng('add', `@angular/material${tag}`, '--skip-confirmation');
3333
await expectFileToMatch('package.json', /@angular\/material/);
3434

3535
// Clean up existing cdk package

0 commit comments

Comments
 (0)