diff --git a/.projenrc.ts b/.projenrc.ts index bcd26b5d8..d381c2f66 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1185,7 +1185,6 @@ const cli = configureProject( '@types/fs-extra@^11', '@types/mockery', '@types/picomatch', - '@types/promptly', '@types/semver', '@types/sinon', '@types/yargs@^15', @@ -1243,13 +1242,12 @@ const cli = configureProject( 'chalk@^4', 'chokidar@^4', 'decamelize@^5', // Non-ESM - 'enquirer', + '@clack/core', 'fs-extra@^11', 'fast-glob', 'picomatch', 'p-limit@^3', 'p-queue@^6', - 'promptly', 'proxy-agent', 'semver', 'strip-ansi@^6', @@ -1286,6 +1284,11 @@ const cli = configureProject( }, jestOptions: jestOptionsForProject({ jestConfig: { + // @clack/core is ESM-only and cannot be loaded by Jest in CJS mode. + // Map it to a manual mock that provides safe defaults for all tests. + moduleNameMapper: { + '^@clack/core$': '/test/_helpers/mock-clack-core.js', + }, coverageThreshold: { // We want to improve our test coverage // DO NOT LOWER THESE VALUES! diff --git a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts index 436ee7633..da5056180 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts @@ -1,6 +1,5 @@ import type * as child_process from 'child_process'; import * as fs from 'fs'; -import * as os from 'os'; import * as path from 'path'; import type { TestContext } from './integ-test'; import { Process } from './process'; @@ -69,7 +68,10 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom // now write the input with a slight delay to ensure // the child process has already started reading. const sendInput = () => { - child.writeStdin(interaction.input + (interaction.end ?? os.EOL)); + // Use \r (carriage return) as default line ending — this is what real terminals + // send when Enter is pressed. Some prompt libraries (e.g. @clack/core) only + // recognize \r as the submit key, not \n. + child.writeStdin(interaction.input + (interaction.end ?? '\r')); }; if (interaction.beforeInput) { @@ -340,7 +342,11 @@ class LastLine { private lastLine: string = ''; public append(chunk: string): void { - const lines = chunk.split(os.EOL); + // Strip ANSI escape codes so prompt matching works regardless of terminal styling + const clean = chunk.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, ''); + // Split on \r, \n, or \r\n — interactive prompt libraries like @clack/prompts + // use \r and cursor movement instead of plain \n for re-rendering + const lines = clean.split(/\r?\n|\r/); if (lines.length === 1) { // chunk doesn't contain a new line so just append this.lastLine += lines[0]; diff --git a/packages/aws-cdk/.projen/deps.json b/packages/aws-cdk/.projen/deps.json index cbed4118f..539fd9d44 100644 --- a/packages/aws-cdk/.projen/deps.json +++ b/packages/aws-cdk/.projen/deps.json @@ -43,10 +43,6 @@ "name": "@types/picomatch", "type": "build" }, - { - "name": "@types/promptly", - "type": "build" - }, { "name": "@types/semver", "type": "build" @@ -302,6 +298,10 @@ "name": "@aws-sdk/lib-storage", "type": "runtime" }, + { + "name": "@clack/core", + "type": "runtime" + }, { "name": "@smithy/middleware-endpoint", "type": "runtime" @@ -358,10 +358,6 @@ "version": "^5", "type": "runtime" }, - { - "name": "enquirer", - "type": "runtime" - }, { "name": "fast-glob", "type": "runtime" @@ -385,10 +381,6 @@ "name": "picomatch", "type": "runtime" }, - { - "name": "promptly", - "type": "runtime" - }, { "name": "proxy-agent", "type": "runtime" diff --git a/packages/aws-cdk/.projen/tasks.json b/packages/aws-cdk/.projen/tasks.json index d5a3ea803..e4ce39079 100644 --- a/packages/aws-cdk/.projen/tasks.json +++ b/packages/aws-cdk/.projen/tasks.json @@ -55,7 +55,7 @@ }, "steps": [ { - "exec": "yarn dlx npm-check-updates@20 --upgrade --target=minor --cooldown=3 --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@cdklabs/eslint-plugin,@types/archiver,@types/jest,@types/mockery,@types/picomatch,@types/promptly,@types/semver,@types/sinon,aws-sdk-client-mock,aws-sdk-client-mock-jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-jest,eslint-plugin-jsdoc,eslint-plugin-prettier,fast-check,jest,jest-environment-node,jest-mock,license-checker,node-backpack,nx,projen,sinon,ts-jest,ts-mock-imports,tsx,@aws-sdk/client-appsync,@aws-sdk/client-bedrock-agentcore-control,@aws-sdk/client-cloudcontrol,@aws-sdk/client-cloudformation,@aws-sdk/client-cloudwatch-logs,@aws-sdk/client-codebuild,@aws-sdk/client-ec2,@aws-sdk/client-ecr,@aws-sdk/client-ecs,@aws-sdk/client-elastic-load-balancing-v2,@aws-sdk/client-iam,@aws-sdk/client-kms,@aws-sdk/client-lambda,@aws-sdk/client-route-53,@aws-sdk/client-s3,@aws-sdk/client-secrets-manager,@aws-sdk/client-sfn,@aws-sdk/client-ssm,@aws-sdk/client-sts,@aws-sdk/credential-providers,@aws-sdk/ec2-metadata-service,@aws-sdk/lib-storage,@smithy/middleware-endpoint,@smithy/property-provider,@smithy/shared-ini-file-loader,@smithy/smithy-client,@smithy/types,@smithy/util-retry,@smithy/util-waiter,archiver,cdk-from-cfn,enquirer,fast-glob,picomatch,promptly,proxy-agent,semver,uuid" + "exec": "yarn dlx npm-check-updates@20 --upgrade --target=minor --cooldown=3 --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@cdklabs/eslint-plugin,@types/archiver,@types/jest,@types/mockery,@types/picomatch,@types/semver,@types/sinon,aws-sdk-client-mock,aws-sdk-client-mock-jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-jest,eslint-plugin-jsdoc,eslint-plugin-prettier,fast-check,jest,jest-environment-node,jest-mock,license-checker,node-backpack,nx,projen,sinon,ts-jest,ts-mock-imports,tsx,@aws-sdk/client-appsync,@aws-sdk/client-bedrock-agentcore-control,@aws-sdk/client-cloudcontrol,@aws-sdk/client-cloudformation,@aws-sdk/client-cloudwatch-logs,@aws-sdk/client-codebuild,@aws-sdk/client-ec2,@aws-sdk/client-ecr,@aws-sdk/client-ecs,@aws-sdk/client-elastic-load-balancing-v2,@aws-sdk/client-iam,@aws-sdk/client-kms,@aws-sdk/client-lambda,@aws-sdk/client-route-53,@aws-sdk/client-s3,@aws-sdk/client-secrets-manager,@aws-sdk/client-sfn,@aws-sdk/client-ssm,@aws-sdk/client-sts,@aws-sdk/credential-providers,@aws-sdk/ec2-metadata-service,@aws-sdk/lib-storage,@clack/core,@smithy/middleware-endpoint,@smithy/property-provider,@smithy/shared-ini-file-loader,@smithy/smithy-client,@smithy/types,@smithy/util-retry,@smithy/util-waiter,archiver,cdk-from-cfn,fast-glob,picomatch,proxy-agent,semver,uuid" } ] }, diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index e49615257..fdda5ecec 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -11295,6 +11295,20 @@ Apache License of your accepting any such warranty or additional liability. +---------------- + +** @clack/core@1.2.0 - https://www.npmjs.com/package/@clack/core/v/1.2.0 | MIT +MIT License + +Copyright (c) Nate Moore + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** @nodelib/fs.scandir@2.1.5 - https://www.npmjs.com/package/@nodelib/fs.scandir/v/2.1.5 | MIT @@ -20923,32 +20937,6 @@ SOFTWARE. ----------------- - -** ansi-colors@4.1.3 - https://www.npmjs.com/package/ansi-colors/v/4.1.3 | MIT -The MIT License (MIT) - -Copyright (c) 2015-present, Brian Woodward. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - ---------------- ** ansi-regex@5.0.1 - https://www.npmjs.com/package/ansi-regex/v/5.0.1 | MIT @@ -21906,32 +21894,6 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- - -** enquirer@2.4.1 - https://www.npmjs.com/package/enquirer/v/2.4.1 | MIT -The MIT License (MIT) - -Copyright (c) 2016-present, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - ---------------- ** escodegen@2.1.0 - https://www.npmjs.com/package/escodegen/v/2.1.0 | BSD-2-Clause @@ -22369,6 +22331,86 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** fast-string-truncated-width@1.2.1 - https://www.npmjs.com/package/fast-string-truncated-width/v/1.2.1 | MIT +The MIT License (MIT) + +Copyright (c) 2024-present Fabio Spampinato + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +---------------- + +** fast-string-width@1.1.0 - https://www.npmjs.com/package/fast-string-width/v/1.1.0 | MIT +The MIT License (MIT) + +Copyright (c) 2024-present Fabio Spampinato + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +---------------- + +** fast-wrap-ansi@0.1.6 - https://www.npmjs.com/package/fast-wrap-ansi/v/0.1.6 | MIT +MIT License + +Copyright (c) 2025 James Garbutt + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** fast-xml-parser@5.5.8 - https://www.npmjs.com/package/fast-xml-parser/v/5.5.8 | MIT @@ -23079,26 +23121,6 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ** ms@2.1.3 - https://www.npmjs.com/package/ms/v/2.1.3 | MIT ----------------- - -** mute-stream@0.0.8 - https://www.npmjs.com/package/mute-stream/v/0.0.8 | ISC -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ---------------- ** netmask@2.1.1 - https://www.npmjs.com/package/netmask/v/2.1.1 | MIT @@ -23389,32 +23411,6 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- - -** promptly@3.2.0 - https://www.npmjs.com/package/promptly/v/3.2.0 | MIT -The MIT License (MIT) - -Copyright (c) 2018 Made With MOXY Lda - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - ---------------- ** proxy-agent@6.5.0 - https://www.npmjs.com/package/proxy-agent/v/6.5.0 | MIT @@ -23491,26 +23487,6 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- - -** read@1.0.7 - https://www.npmjs.com/package/read/v/1.0.7 | ISC -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ---------------- ** readable-stream@2.3.8 - https://www.npmjs.com/package/readable-stream/v/2.3.8 | MIT @@ -24009,6 +23985,32 @@ WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- + +** sisteransi@1.0.5 - https://www.npmjs.com/package/sisteransi/v/1.0.5 | MIT +MIT License + +Copyright (c) 2018 Terkel Gjervig Nielsen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** slice-ansi@4.0.0 - https://www.npmjs.com/package/slice-ansi/v/4.0.0 | MIT diff --git a/packages/aws-cdk/jest.config.json b/packages/aws-cdk/jest.config.json index 9a179fa6b..f5f96a606 100644 --- a/packages/aws-cdk/jest.config.json +++ b/packages/aws-cdk/jest.config.json @@ -52,6 +52,9 @@ ] ], "randomize": true, + "moduleNameMapper": { + "^@clack/core$": "/test/_helpers/mock-clack-core.js" + }, "testTimeout": 60000, "setupFilesAfterEnv": [ "/test/_helpers/jest-setup-after-env.ts" diff --git a/packages/aws-cdk/lib/cli/io-host/cli-io-host.ts b/packages/aws-cdk/lib/cli/io-host/cli-io-host.ts index 06cce0064..ccfe644f2 100644 --- a/packages/aws-cdk/lib/cli/io-host/cli-io-host.ts +++ b/packages/aws-cdk/lib/cli/io-host/cli-io-host.ts @@ -4,8 +4,8 @@ import { RequireApproval } from '@aws-cdk/cloud-assembly-schema'; import { ToolkitError } from '@aws-cdk/toolkit-lib'; import type { HotswapResult, IIoHost, IoMessage, IoMessageCode, IoMessageLevel, IoRequest, ToolkitAction } from '@aws-cdk/toolkit-lib'; import type { Context } from '@aws-cdk/toolkit-lib/lib/api'; +import { ConfirmPrompt, TextPrompt, isCancel } from '@clack/core'; import * as chalk from 'chalk'; -import * as promptly from 'promptly'; import type { IoHelper, ActivityPrinterProps, IActivityPrinter } from '../../../lib/api-private'; import { asIoHelper, IO, isMessageRelevantForLevel, CurrentActivityPrinter, HistoryActivityPrinter } from '../../../lib/api-private'; import { StackActivityProgress } from '../../commands/deploy'; @@ -451,6 +451,9 @@ export class CliIoHost implements IIoHost { const concurrency = data.concurrency ?? 0; const responseDescription = data.responseDescription; + // Output stream for all clack prompts and log messages + const output = this.selectStreamFromLevel(msg.level); + // Special approval prompt // Determine if the message needs approval. If it does, continue (it is a basic confirmation prompt) // If it does not, return success (true). We only check messages with codes that we are aware @@ -465,7 +468,7 @@ export class CliIoHost implements IIoHost { if (isConfirmationPrompt(msg)) { await this.notify({ ...msg, - message: `${chalk.cyan(msg.message)} (auto-confirmed)`, + message: `${chalk.cyan(msg.message)} ${chalk.dim('(auto-confirmed)')}`, }); return true; } @@ -474,7 +477,7 @@ export class CliIoHost implements IIoHost { if (msg.defaultResponse) { await this.notify({ ...msg, - message: `${chalk.cyan(msg.message)} (auto-responded with default: ${util.format(msg.defaultResponse)})`, + message: `${chalk.cyan(msg.message)} ${chalk.dim(`(auto-responded: ${util.format(msg.defaultResponse)})`)}`, }); return msg.defaultResponse; } @@ -493,8 +496,16 @@ export class CliIoHost implements IIoHost { // Basic confirmation prompt // We treat all requests with a boolean response as confirmation prompts if (isConfirmationPrompt(msg)) { - const confirmed = await promptly.confirm(`${chalk.cyan(msg.message)} (y/n)`); - if (!confirmed) { + const p = new ConfirmPrompt({ + active: 'y', + inactive: 'n', + output, + render() { + return `${chalk.cyan(msg.message)} (y/n)`; + }, + }); + const confirmed = await p.prompt(); + if (isCancel(confirmed) || !confirmed) { throw new ToolkitError('AbortedByUser', 'Aborted by user'); } return confirmed; @@ -503,11 +514,18 @@ export class CliIoHost implements IIoHost { // Asking for a specific value const prompt = extractPromptInfo(msg); const desc = responseDescription ?? prompt.default; - const answer = await promptly.prompt(`${chalk.cyan(msg.message)}${desc ? ` (${desc})` : ''}`, { - default: prompt.default, - trim: true, + const p = new TextPrompt({ + defaultValue: prompt.default, + output, + render() { + return `${chalk.cyan(msg.message)}${desc ? ` (${desc})` : ''} `; + }, }); - return prompt.convertAnswer(answer); + const answer = await p.prompt(); + if (isCancel(answer)) { + throw new ToolkitError('AbortedByUser', 'Aborted by user'); + } + return prompt.convertAnswer(String(answer).trim()); }); // We need to cast this because it is impossible to narrow the generic type @@ -576,7 +594,7 @@ function isConfirmationPrompt(msg: IoRequest): msg is IoRequest): { default: string; diff --git a/packages/aws-cdk/lib/commands/flags/interactive-handler.ts b/packages/aws-cdk/lib/commands/flags/interactive-handler.ts index f8ad2b134..24893a099 100644 --- a/packages/aws-cdk/lib/commands/flags/interactive-handler.ts +++ b/packages/aws-cdk/lib/commands/flags/interactive-handler.ts @@ -1,9 +1,21 @@ import type { FeatureFlag } from '@aws-cdk/toolkit-lib'; -// @ts-ignore -import { Select } from 'enquirer'; +import { SelectPrompt, isCancel } from '@clack/core'; import type { FlagOperations } from './operations'; import { FlagsMenuOptions, type FlagOperationsParams } from './types'; +async function promptSelect(message: string, choices: { value: T; label: string }[]): Promise { + const p = new SelectPrompt({ + options: choices, + render() { + const lines = choices.map((c, i) => + i === this.cursor ? `> ${c.label}` : ` ${c.label}`, + ); + return `${message}\n${lines.join('\n')}`; + }, + }); + return p.prompt() as Promise; +} + export class InteractiveHandler { constructor( private readonly flags: FeatureFlag[], @@ -30,13 +42,10 @@ export class InteractiveHandler { async handleInteractiveMode(): Promise { await this.displayFlagsWithDifferences(); - const prompt = new Select({ - name: 'option', - message: 'Menu', - choices: Object.values(FlagsMenuOptions), - }); + const choices = Object.values(FlagsMenuOptions).map(o => ({ value: o, label: o })); + const answer = await promptSelect('Menu', choices); - const answer = await prompt.run(); + if (isCancel(answer)) return null; switch (answer) { case FlagsMenuOptions.ALL_TO_RECOMMENDED: @@ -58,22 +67,23 @@ export class InteractiveHandler { private async handleSpecificFlagSelection(): Promise { const booleanFlags = this.flags.filter(flag => this.flagOperations.isBooleanFlag(flag)); - const flagPrompt = new Select({ - name: 'flag', - message: 'Select which flag you would like to modify:', - limit: 100, - choices: booleanFlags.map(flag => flag.name), - }); + const selectedFlagName = await promptSelect( + 'Select which flag you would like to modify:', + booleanFlags.map(flag => ({ value: flag.name, label: flag.name })), + ); - const selectedFlagName = await flagPrompt.run(); + if (isCancel(selectedFlagName)) { + return { set: false }; + } - const valuePrompt = new Select({ - name: 'value', - message: 'Select a value:', - choices: ['true', 'false'], - }); + const value = await promptSelect( + 'Select a value:', + [{ value: 'true', label: 'true' }, { value: 'false', label: 'false' }], + ); - const value = await valuePrompt.run(); + if (isCancel(value)) { + return { set: false }; + } return { FLAGNAME: [selectedFlagName], diff --git a/packages/aws-cdk/lib/index.ts b/packages/aws-cdk/lib/index.ts index 3fec2fa26..d35a9120f 100644 --- a/packages/aws-cdk/lib/index.ts +++ b/packages/aws-cdk/lib/index.ts @@ -1,2 +1,5 @@ +// Polyfill util.styleText for Node.js <20.12 (used by @clack/prompts) +import './util/style-text-polyfill'; + export * from './api'; export { cli, exec } from './cli/cli'; diff --git a/packages/aws-cdk/lib/util/style-text-polyfill.ts b/packages/aws-cdk/lib/util/style-text-polyfill.ts new file mode 100644 index 000000000..a187340ed --- /dev/null +++ b/packages/aws-cdk/lib/util/style-text-polyfill.ts @@ -0,0 +1,133 @@ +/** + * Polyfill for `util.styleText` which was added in Node.js 20.12.0. + * + * This patches `util.styleText` on older Node.js versions (e.g. 18.x) + * so that dependencies like @clack/prompts that rely on it can work. + * + * The implementation mirrors the Node.js upstream behavior: + * - Looks up ANSI codes from `util.inspect.colors` + * - Supports single format string or array of formats + * - `'none'` format is a passthrough + * - Replaces nested close codes so styles compose correctly + * - Supports hex colors (#RGB / #RRGGBB) via 24-bit ANSI + * - Supports `options.stream` / `options.validateStream` for color detection + * + * @see https://github.com/nodejs/node/blob/main/lib/util.js + */ +// eslint-disable-next-line @typescript-eslint/no-require-imports +const util = require('node:util'); + +if (typeof util.styleText !== 'function') { + const inspect = util.inspect; + const colors: Record = inspect.colors as any; + + const ESC = '\u001b['; + + // Codes that share close code 22 + const DIM_CODE = 2; + const BOLD_CODE = 1; + + /** + * When text contains a close sequence, replace it with close+reopen + * so the style continues after nested resets. If the format is bold or dim + * (which share close code 22), keep the close before reopening. + */ + function replaceCloseCode(str: string, closeSeq: string, openSeq: string, keepClose: boolean): string { + let index = str.indexOf(closeSeq); + if (index === -1) return str; + + const closeLen = closeSeq.length; + const replacement = keepClose ? closeSeq + openSeq : openSeq; + let result = ''; + let lastIndex = 0; + + do { + const afterClose = index + closeLen; + if (afterClose < str.length) { + result += str.slice(lastIndex, index) + replacement; + lastIndex = afterClose; + } else { + break; + } + index = str.indexOf(closeSeq, lastIndex); + } while (index !== -1); + + return result + str.slice(lastIndex); + } + + const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; + + function hexToRgb(hex: string): [number, number, number] { + let h: string; + if (hex.length === 4) { + h = hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]; + } else { + h = hex.slice(1); + } + const n = parseInt(h, 16); + return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]; // eslint-disable-line no-bitwise + } + + function shouldColorize(stream: any): boolean { + if (stream && typeof stream.hasColors === 'function') { + return stream.hasColors(); + } + // Fall back to checking isTTY + return !!(stream && stream.isTTY); + } + + util.styleText = function styleText( + format: string | string[], + text: string, + options?: { validateStream?: boolean; stream?: any }, + ): string { + if (typeof text !== 'string') { + throw new TypeError(`The "text" argument must be of type string. Received ${typeof text}`); + } + + const validateStream = options?.validateStream ?? true; + if (validateStream) { + const stream = options?.stream ?? process.stdout; + if (!shouldColorize(stream)) { + return text; + } + } + + const formats = Array.isArray(format) ? format : [format]; + let openCodes = ''; + let closeCodes = ''; + let processedText = text; + + for (const key of formats) { + if (key === 'none') continue; + + // Hex color support + if (typeof key === 'string' && HEX_RE.test(key)) { + const [r, g, b] = hexToRgb(key); + const openSeq = `${ESC}38;2;${r};${g};${b}m`; + const closeSeq = `${ESC}39m`; + openCodes += openSeq; + closeCodes = closeSeq + closeCodes; + processedText = replaceCloseCode(processedText, closeSeq, openSeq, false); + continue; + } + + const code = colors[key]; + if (!code) { + throw new TypeError( + `The argument 'format' must be one of: ${Object.keys(colors).join(', ')}. Received '${key}'`, + ); + } + + const openSeq = `${ESC}${code[0]}m`; + const closeSeq = `${ESC}${code[1]}m`; + const keepClose = code[0] === DIM_CODE || code[0] === BOLD_CODE; + + openCodes += openSeq; + closeCodes = closeSeq + closeCodes; + processedText = replaceCloseCode(processedText, closeSeq, openSeq, keepClose); + } + + return `${openCodes}${processedText}${closeCodes}`; + }; +} diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index f0d2d7477..3ffb663f2 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -46,7 +46,6 @@ "@types/mockery": "^1.4.33", "@types/node": "^20", "@types/picomatch": "^4.0.3", - "@types/promptly": "^3.0.5", "@types/semver": "^7.7.1", "@types/sinon": "^17.0.4", "@types/yargs": "^15", @@ -110,6 +109,7 @@ "@aws-sdk/credential-providers": "^3", "@aws-sdk/ec2-metadata-service": "^3", "@aws-sdk/lib-storage": "^3", + "@clack/core": "^1.2.0", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", @@ -123,13 +123,11 @@ "chalk": "^4", "chokidar": "^4", "decamelize": "^5", - "enquirer": "^2.4.1", "fast-glob": "^3.3.3", "fs-extra": "^11", "p-limit": "^3", "p-queue": "^6", "picomatch": "^4.0.4", - "promptly": "^3.2.0", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "strip-ansi": "^6", diff --git a/packages/aws-cdk/test/_helpers/mock-clack-core.js b/packages/aws-cdk/test/_helpers/mock-clack-core.js new file mode 100644 index 000000000..8fb5e6f02 --- /dev/null +++ b/packages/aws-cdk/test/_helpers/mock-clack-core.js @@ -0,0 +1,16 @@ +/** + * Manual Jest mock for @clack/core. + * + * @clack/core is an ESM-only package that cannot be loaded by Jest in CJS mode. + * Since prompt classes are interactive (stdin/stdout), they should always be mocked in tests. + * Tests that need specific behavior should override these mocks with jest.mock(). + */ + +const mockPrompt = jest.fn().mockResolvedValue(undefined); + +module.exports = { + ConfirmPrompt: jest.fn().mockImplementation(() => ({ prompt: jest.fn().mockResolvedValue(true) })), + TextPrompt: jest.fn().mockImplementation(() => ({ prompt: jest.fn().mockResolvedValue('') })), + SelectPrompt: jest.fn().mockImplementation(() => ({ prompt: mockPrompt })), + isCancel: jest.fn().mockReturnValue(false), +}; diff --git a/packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts b/packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts index 86aaa23e8..7f6d1d92b 100644 --- a/packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts +++ b/packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts @@ -2,6 +2,7 @@ import * as os from 'os'; import * as path from 'path'; import { PassThrough } from 'stream'; import { RequireApproval } from '@aws-cdk/cloud-assembly-schema'; +import { ConfirmPrompt, TextPrompt, isCancel } from '@clack/core'; import * as chalk from 'chalk'; import * as fs from 'fs-extra'; import { Context } from '../../../lib/api/context'; @@ -9,8 +10,6 @@ import type { IoMessage, IoMessageLevel, IoRequest } from '../../../lib/cli/io-h import { CliIoHost } from '../../../lib/cli/io-host'; import { CLI_PRIVATE_IO } from '../../../lib/cli/telemetry/messages'; -let passThrough: PassThrough; - // Store original process.on const originalProcessOn = process.on; @@ -40,7 +39,7 @@ describe('CliIoHost', () => { ioHost.isCI = false; ioHost.currentAction = 'synth'; ioHost.requireDeployApproval = RequireApproval.ANYCHANGE; - (process as any).stdin = passThrough = new PassThrough(); + (process as any).stdin = new PassThrough(); defaultMessage = { time: new Date('2024-01-01T12:00:00'), @@ -526,7 +525,11 @@ describe('CliIoHost', () => { describe('boolean', () => { test('respond "yes" to a confirmation prompt', async () => { - const response = await requestResponse('y', plainMessage({ + (ConfirmPrompt as unknown as jest.Mock).mockImplementationOnce(() => ({ + prompt: jest.fn().mockResolvedValue(true), + })); + + const response = await ioHost.requestResponse(plainMessage({ time: new Date(), level: 'info', action: 'synth', @@ -535,12 +538,16 @@ describe('CliIoHost', () => { defaultResponse: true, })); - expect(mockStdout).toHaveBeenCalledWith(chalk.cyan('Continue?') + ' (y/n) '); + expect(ConfirmPrompt).toHaveBeenCalledWith(expect.objectContaining({ output: expect.anything() })); expect(response).toBe(true); }); test('respond "no" to a confirmation prompt', async () => { - await expect(() => requestResponse('n', plainMessage({ + (ConfirmPrompt as unknown as jest.Mock).mockImplementationOnce(() => ({ + prompt: jest.fn().mockResolvedValue(false), + })); + + await expect(() => ioHost.requestResponse(plainMessage({ time: new Date(), level: 'info', action: 'synth', @@ -548,8 +555,23 @@ describe('CliIoHost', () => { message: 'Continue?', defaultResponse: true, }))).rejects.toThrow('Aborted by user'); + }); + + test('cancel a confirmation prompt', async () => { + const cancelSymbol = Symbol('cancel'); + (ConfirmPrompt as unknown as jest.Mock).mockImplementationOnce(() => ({ + prompt: jest.fn().mockResolvedValue(cancelSymbol), + })); + (isCancel as unknown as jest.Mock).mockReturnValueOnce(true); - expect(mockStdout).toHaveBeenCalledWith(chalk.cyan('Continue?') + ' (y/n) '); + await expect(() => ioHost.requestResponse(plainMessage({ + time: new Date(), + level: 'info', + action: 'synth', + code: 'CDK_TOOLKIT_I0001', + message: 'Continue?', + defaultResponse: true, + }))).rejects.toThrow('Aborted by user'); }); }); @@ -557,10 +579,14 @@ describe('CliIoHost', () => { test.each([ ['bear', 'bear'], ['giraffe', 'giraffe'], - // simulate the enter key - ['\x0A', 'cat'], + // empty input returns default + ['', 'cat'], ])('receives %p and returns %p', async (input, expectedResponse) => { - const response = await requestResponse(input, plainMessage({ + (TextPrompt as unknown as jest.Mock).mockImplementationOnce(() => ({ + prompt: jest.fn().mockResolvedValue(input || 'cat'), + })); + + const response = await ioHost.requestResponse(plainMessage({ time: new Date(), level: 'info', action: 'synth', @@ -569,7 +595,7 @@ describe('CliIoHost', () => { defaultResponse: 'cat', })); - expect(mockStdout).toHaveBeenCalledWith(chalk.cyan('Favorite animal') + ' (cat) '); + expect(TextPrompt).toHaveBeenCalledWith(expect.objectContaining({ output: expect.anything() })); expect(response).toBe(expectedResponse); }); }); @@ -577,10 +603,14 @@ describe('CliIoHost', () => { describe('number', () => { test.each([ ['3', 3], - // simulate the enter key - ['\x0A', 1], + // empty input returns default + ['1', 1], ])('receives %p and return %p', async (input, expectedResponse) => { - const response = await requestResponse(input, plainMessage({ + (TextPrompt as unknown as jest.Mock).mockImplementationOnce(() => ({ + prompt: jest.fn().mockResolvedValue(input), + })); + + const response = await ioHost.requestResponse(plainMessage({ time: new Date(), level: 'info', action: 'synth', @@ -589,7 +619,7 @@ describe('CliIoHost', () => { defaultResponse: 1, })); - expect(mockStdout).toHaveBeenCalledWith(chalk.cyan('How many would you like?') + ' (1) '); + expect(TextPrompt).toHaveBeenCalledWith(expect.objectContaining({ output: expect.anything() })); expect(response).toBe(expectedResponse); }); }); @@ -605,7 +635,6 @@ describe('CliIoHost', () => { test('it does not prompt the user and return true', async () => { const notifySpy = jest.spyOn(autoRespondingIoHost, 'notify'); - // WHEN const response = await autoRespondingIoHost.requestResponse(plainMessage({ time: new Date(), level: 'info', @@ -615,10 +644,9 @@ describe('CliIoHost', () => { defaultResponse: true, })); - // THEN - expect(mockStdout).not.toHaveBeenCalledWith(chalk.cyan('test message') + ' (y/n) '); + expect(ConfirmPrompt).not.toHaveBeenCalled(); expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - message: chalk.cyan('test message') + ' (auto-confirmed)', + message: expect.stringContaining('auto-confirmed'), })); expect(response).toBe(true); }); @@ -626,7 +654,6 @@ describe('CliIoHost', () => { test('messages with default are skipped', async () => { const notifySpy = jest.spyOn(autoRespondingIoHost, 'notify'); - // WHEN const response = await autoRespondingIoHost.requestResponse(plainMessage({ time: new Date(), level: 'info', @@ -636,10 +663,9 @@ describe('CliIoHost', () => { defaultResponse: 'foobar', })); - // THEN - expect(mockStdout).not.toHaveBeenCalledWith(chalk.cyan('test message') + ' (y/n) '); + expect(TextPrompt).not.toHaveBeenCalled(); expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - message: chalk.cyan('test message') + ' (auto-responded with default: foobar)', + message: expect.stringContaining('foobar'), })); expect(response).toBe('foobar'); }); @@ -709,7 +735,11 @@ describe('CliIoHost', () => { describe('requireApproval', () => { test('require approval by default - respond yes', async () => { - const response = await requestResponse('y', plainMessage({ + (ConfirmPrompt as unknown as jest.Mock).mockImplementationOnce(() => ({ + prompt: jest.fn().mockResolvedValue(true), + })); + + const response = await ioHost.requestResponse(plainMessage({ time: new Date(), level: 'info', action: 'synth', @@ -718,12 +748,16 @@ describe('CliIoHost', () => { defaultResponse: true, })); - expect(mockStdout).toHaveBeenCalledWith(chalk.cyan('test message') + ' (y/n) '); + expect(ConfirmPrompt).toHaveBeenCalledWith(expect.objectContaining({ output: expect.anything() })); expect(response).toEqual(true); }); test('require approval by default - respond no', async () => { - await expect(() => requestResponse('n', plainMessage({ + (ConfirmPrompt as unknown as jest.Mock).mockImplementationOnce(() => ({ + prompt: jest.fn().mockResolvedValue(false), + })); + + await expect(() => ioHost.requestResponse(plainMessage({ time: new Date(), level: 'info', action: 'synth', @@ -744,13 +778,17 @@ describe('CliIoHost', () => { defaultResponse: true, })); - expect(mockStdout).not.toHaveBeenCalledWith(chalk.cyan('test message') + ' (y/n) '); + expect(ConfirmPrompt).not.toHaveBeenCalled(); expect(response).toEqual(true); }); test('broadening - require approval on broadening changes', async () => { + (ConfirmPrompt as unknown as jest.Mock).mockImplementationOnce(() => ({ + prompt: jest.fn().mockResolvedValue(true), + })); + ioHost.requireDeployApproval = RequireApproval.BROADENING; - const response = await requestResponse('y', { + const response = await ioHost.requestResponse({ time: new Date(), level: 'info', action: 'synth', @@ -762,7 +800,7 @@ describe('CliIoHost', () => { defaultResponse: true, }); - expect(mockStdout).toHaveBeenCalledWith(chalk.cyan('test message') + ' (y/n) '); + expect(ConfirmPrompt).toHaveBeenCalledWith(expect.objectContaining({ output: expect.anything() })); expect(response).toEqual(true); }); @@ -787,15 +825,6 @@ describe('CliIoHost', () => { }); }); -/** - * Do a requestResponse cycle with the global ioHost, while sending input on the global fake input stream - */ -async function requestResponse(input: string, msg: IoRequest): Promise { - const promise = ioHost.requestResponse(msg); - passThrough.write(input + '\n'); - return promise; -} - function plainMessage | IoRequest, 'data'>>(m: A): A & { data: void } { return { ...m, diff --git a/packages/aws-cdk/test/commands/flag-operations.test.ts b/packages/aws-cdk/test/commands/flag-operations.test.ts index 163087778..7f09072b8 100644 --- a/packages/aws-cdk/test/commands/flag-operations.test.ts +++ b/packages/aws-cdk/test/commands/flag-operations.test.ts @@ -2,8 +2,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import type { FeatureFlag, Toolkit } from '@aws-cdk/toolkit-lib'; -// @ts-ignore -import { Select } from 'enquirer'; +import { SelectPrompt } from '@clack/core'; import type { IoHelper } from '../../lib/api-private'; import { asIoHelper } from '../../lib/api-private'; import { CliIoHost } from '../../lib/cli/io-host'; @@ -11,8 +10,9 @@ import type { FlagsOptions } from '../../lib/cli/user-input'; import { FlagCommandHandler } from '../../lib/commands/flags/flags'; import { FlagOperations } from '../../lib/commands/flags/operations'; -jest.mock('enquirer', () => ({ - Select: jest.fn(), +jest.mock('@clack/core', () => ({ + SelectPrompt: jest.fn(), + isCancel: jest.fn().mockReturnValue(false), })); let oldDir: string; @@ -1000,8 +1000,10 @@ describe('interactive prompts lead to the correct function calls', () => { '@aws-cdk/core:matchingFlag': true, }); - const mockRun = jest.fn().mockResolvedValue('Set all flags to recommended values'); - Select.mockImplementation(() => ({ run: mockRun })); + const mockSelect = SelectPrompt as unknown as jest.Mock; + mockSelect.mockImplementation(() => ({ + prompt: jest.fn().mockResolvedValue('Set all flags to recommended values'), + })); const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse'); requestResponseSpy.mockResolvedValue(true); @@ -1034,8 +1036,10 @@ describe('interactive prompts lead to the correct function calls', () => { '@aws-cdk/core:testFlag': false, }); - const mockRun = jest.fn().mockResolvedValue('Set unconfigured flags to recommended values'); - Select.mockImplementation(() => ({ run: mockRun })); + const mockSelect = SelectPrompt as unknown as jest.Mock; + mockSelect.mockImplementation(() => ({ + prompt: jest.fn().mockResolvedValue('Set unconfigured flags to recommended values'), + })); const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse'); requestResponseSpy.mockResolvedValue(true); @@ -1066,8 +1070,10 @@ describe('interactive prompts lead to the correct function calls', () => { '@aws-cdk/core:testFlag': false, }); - const mockRun = jest.fn().mockResolvedValue('Set unconfigured flags to their implied configuration (record current behavior)'); - Select.mockImplementation(() => ({ run: mockRun })); + const mockSelect = SelectPrompt as unknown as jest.Mock; + mockSelect.mockImplementation(() => ({ + prompt: jest.fn().mockResolvedValue('Set unconfigured flags to their implied configuration (record current behavior)'), + })); const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse'); requestResponseSpy.mockResolvedValue(true); @@ -1111,16 +1117,11 @@ describe('interactive prompts lead to the correct function calls', () => { '@aws-cdk/core:testFlag': false, }); - let promptNumber = 0; - const mockRun = jest.fn().mockImplementation(() => { - promptNumber++; - if (promptNumber === 1) return Promise.resolve('Modify a specific flag'); - if (promptNumber === 2) return Promise.resolve('@aws-cdk/core:testFlag'); - if (promptNumber === 3) return Promise.resolve('true'); - return Promise.resolve('Exit'); - }); - - Select.mockImplementation(() => ({ run: mockRun })); + const values = ['Modify a specific flag', '@aws-cdk/core:testFlag', 'true', 'Exit']; + let callIndex = 0; + (SelectPrompt as unknown as jest.Mock).mockImplementation(() => ({ + prompt: jest.fn().mockImplementation(() => Promise.resolve(values[callIndex++])), + })); const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse'); requestResponseSpy.mockResolvedValue(true); @@ -1150,8 +1151,10 @@ describe('interactive prompts lead to the correct function calls', () => { '@aws-cdk/core:testFlag': false, }); - const mockRun = jest.fn().mockResolvedValue('Exit'); - Select.mockImplementation(() => ({ run: mockRun })); + const mockSelect = SelectPrompt as unknown as jest.Mock; + mockSelect.mockImplementation(() => ({ + prompt: jest.fn().mockResolvedValue('Exit'), + })); const options: FlagsOptions = { interactive: true, @@ -1172,11 +1175,13 @@ describe('interactive prompts lead to the correct function calls', () => { await cleanupCdkJsonFile(cdkJsonPath); }); - test('enquirer prompts are called with correct options for main menu', async () => { + test('select prompts are called with correct options for main menu', async () => { const cdkJsonPath = await createCdkJsonFile(); - const mockRun = jest.fn().mockResolvedValue('Exit'); - Select.mockImplementation(() => ({ run: mockRun })); + const mockSelect = SelectPrompt as unknown as jest.Mock; + mockSelect.mockImplementation(() => ({ + prompt: jest.fn().mockResolvedValue('Exit'), + })); const options: FlagsOptions = { interactive: true, @@ -1185,17 +1190,15 @@ describe('interactive prompts lead to the correct function calls', () => { const flagOperations = new FlagCommandHandler(mockFlagsData, ioHelper, options, mockToolkit); await flagOperations.processFlagsCommand(); - expect(Select).toHaveBeenCalledWith({ - name: 'option', - message: 'Menu', - choices: [ - 'Set all flags to recommended values', - 'Set unconfigured flags to recommended values', - 'Set unconfigured flags to their implied configuration (record current behavior)', - 'Modify a specific flag', - 'Exit', + expect(SelectPrompt).toHaveBeenCalledWith(expect.objectContaining({ + options: [ + { value: 'Set all flags to recommended values', label: 'Set all flags to recommended values' }, + { value: 'Set unconfigured flags to recommended values', label: 'Set unconfigured flags to recommended values' }, + { value: 'Set unconfigured flags to their implied configuration (record current behavior)', label: 'Set unconfigured flags to their implied configuration (record current behavior)' }, + { value: 'Modify a specific flag', label: 'Modify a specific flag' }, + { value: 'Exit', label: 'Exit' }, ], - }); + })); await cleanupCdkJsonFile(cdkJsonPath); }); diff --git a/packages/aws-cdk/tsconfig.dev.json b/packages/aws-cdk/tsconfig.dev.json index 6e486b71a..1e0844cdf 100644 --- a/packages/aws-cdk/tsconfig.dev.json +++ b/packages/aws-cdk/tsconfig.dev.json @@ -33,7 +33,6 @@ "mockery", "node", "picomatch", - "promptly", "semver", "sinon", "yargs" diff --git a/packages/aws-cdk/tsconfig.json b/packages/aws-cdk/tsconfig.json index a5b18f85a..c42ab93db 100644 --- a/packages/aws-cdk/tsconfig.json +++ b/packages/aws-cdk/tsconfig.json @@ -34,7 +34,6 @@ "mockery", "node", "picomatch", - "promptly", "semver", "sinon", "yargs" diff --git a/yarn.lock b/yarn.lock index eeaa43dfb..d69e20795 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2916,6 +2916,16 @@ __metadata: languageName: node linkType: hard +"@clack/core@npm:^1.2.0": + version: 1.2.0 + resolution: "@clack/core@npm:1.2.0" + dependencies: + fast-wrap-ansi: "npm:^0.1.3" + sisteransi: "npm:^1.0.5" + checksum: 10c0/9ffd2d00a514b966ef28d91ef0b5ecd400e0378f78df352a5a98df2a773a91afa02b181322cb5cbd79d8d42e5c14c00874fe5f4e97d93f5f0d97bbddeb1c805a + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -5759,15 +5769,6 @@ __metadata: languageName: node linkType: hard -"@types/promptly@npm:^3.0.5": - version: 3.0.5 - resolution: "@types/promptly@npm:3.0.5" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/a25984e8688b1e78319d3b2cd976953692735987a2ef400dcf09990a7bd52c2dfd408948ef0782d26f6f99548871ee83828a7b045378366cf963bfc7e14bcc2e - languageName: node - linkType: hard - "@types/readdir-glob@npm:*": version: 1.1.5 resolution: "@types/readdir-glob@npm:1.1.5" @@ -6881,6 +6882,7 @@ __metadata: "@aws-sdk/ec2-metadata-service": "npm:^3" "@aws-sdk/lib-storage": "npm:^3" "@cdklabs/eslint-plugin": "npm:^2.0.0" + "@clack/core": "npm:^1.2.0" "@smithy/middleware-endpoint": "npm:^4.4.29" "@smithy/property-provider": "npm:^4.2.13" "@smithy/shared-ini-file-loader": "npm:^4.4.8" @@ -6895,7 +6897,6 @@ __metadata: "@types/mockery": "npm:^1.4.33" "@types/node": "npm:^20" "@types/picomatch": "npm:^4.0.3" - "@types/promptly": "npm:^3.0.5" "@types/semver": "npm:^7.7.1" "@types/sinon": "npm:^17.0.4" "@types/yargs": "npm:^15" @@ -6912,7 +6913,6 @@ __metadata: commit-and-tag-version: "npm:^12" constructs: "npm:^10.0.0" decamelize: "npm:^5" - enquirer: "npm:^2.4.1" eslint: "npm:^9" eslint-config-prettier: "npm:^10.1.8" eslint-import-resolver-typescript: "npm:^4.4.4" @@ -6936,7 +6936,6 @@ __metadata: picomatch: "npm:^4.0.4" prettier: "npm:^2.8" projen: "npm:^0.99.48" - promptly: "npm:^3.2.0" proxy-agent: "npm:^6.5.0" semver: "npm:^7.7.4" sinon: "npm:^19.0.5" @@ -8945,16 +8944,6 @@ __metadata: languageName: node linkType: hard -"enquirer@npm:^2.4.1": - version: 2.4.1 - resolution: "enquirer@npm:2.4.1" - dependencies: - ansi-colors: "npm:^4.1.1" - strip-ansi: "npm:^6.0.1" - checksum: 10c0/43850479d7a51d36a9c924b518dcdc6373b5a8ae3401097d336b7b7e258324749d0ad37a1fcaa5706f04799baa05585cd7af19ebdf7667673e7694435fcea918 - languageName: node - linkType: hard - "enquirer@npm:~2.3.6": version: 2.3.6 resolution: "enquirer@npm:2.3.6" @@ -9877,6 +9866,22 @@ __metadata: languageName: node linkType: hard +"fast-string-truncated-width@npm:^1.2.0": + version: 1.2.1 + resolution: "fast-string-truncated-width@npm:1.2.1" + checksum: 10c0/21ee31b5d1f62c48ba875081c726d31e464dfce7591ec13651baa4269316f6e10d70efa08cc511bc2de681c47791065a12b5cab596ea81a4afa5745180f92a20 + languageName: node + linkType: hard + +"fast-string-width@npm:^1.1.0": + version: 1.1.0 + resolution: "fast-string-width@npm:1.1.0" + dependencies: + fast-string-truncated-width: "npm:^1.2.0" + checksum: 10c0/7ed29610e8960fce477fde55a35f0687aeb14e844487dde6784409cdfe24aff4015c6eebcd872d00b76279325f9f707fdef9eae9aac9156a72cec1c9b38a2cab + languageName: node + linkType: hard + "fast-uri@npm:^3.0.1": version: 3.1.0 resolution: "fast-uri@npm:3.1.0" @@ -9884,6 +9889,15 @@ __metadata: languageName: node linkType: hard +"fast-wrap-ansi@npm:^0.1.3": + version: 0.1.6 + resolution: "fast-wrap-ansi@npm:0.1.6" + dependencies: + fast-string-width: "npm:^1.1.0" + checksum: 10c0/509e6b1c9f4eae9de9153d8f8020d3ea622c6be92a190963deafee70a3c83b6d58e41d8fd85c62a03c149b0659f79aaf76d2e042d29ee82ad35aab1cb07a46ef + languageName: node + linkType: hard + "fast-xml-builder@npm:^1.1.4": version: 1.1.4 resolution: "fast-xml-builder@npm:1.1.4" @@ -13333,13 +13347,6 @@ __metadata: languageName: node linkType: hard -"mute-stream@npm:~0.0.4": - version: 0.0.8 - resolution: "mute-stream@npm:0.0.8" - checksum: 10c0/18d06d92e5d6d45e2b63c0e1b8f25376af71748ac36f53c059baa8b76ffac31c5ab225480494e7d35d30215ecdb18fed26ec23cafcd2f7733f2f14406bcd19e2 - languageName: node - linkType: hard - "nanoid@npm:^3.3.11": version: 3.3.11 resolution: "nanoid@npm:3.3.11" @@ -14811,15 +14818,6 @@ __metadata: languageName: node linkType: hard -"promptly@npm:^3.2.0": - version: 3.2.0 - resolution: "promptly@npm:3.2.0" - dependencies: - read: "npm:^1.0.4" - checksum: 10c0/2d52204e9bbabd654a6ec61e3c64a6426df6eeeec0ade94d136de5c77af51d3d6a94a7408f2198b79396142b9afe10e34fdb8065867b4bc503b46dec6cf9eedf - languageName: node - linkType: hard - "prompts@npm:^2.0.1": version: 2.4.2 resolution: "prompts@npm:2.4.2" @@ -15109,15 +15107,6 @@ __metadata: languageName: node linkType: hard -"read@npm:^1.0.4": - version: 1.0.7 - resolution: "read@npm:1.0.7" - dependencies: - mute-stream: "npm:~0.0.4" - checksum: 10c0/443533f05d5bb11b36ef1c6d625aae4e2ced8967e93cf546f35aa77b4eb6bd157f4256619e446bae43467f8f6619c7bc5c76983348dffaf36afedf4224f46216 - languageName: node - linkType: hard - "read@npm:^5.0.0, read@npm:^5.0.1": version: 5.0.1 resolution: "read@npm:5.0.1"