From 8b984e730bb607ebf74fcff07bef36a0e88aa2bd Mon Sep 17 00:00:00 2001 From: corentin Date: Fri, 10 Apr 2026 13:21:12 +0200 Subject: [PATCH 1/4] Migrate prompts to modern Inquirer and drop checkbox-plus --- LICENSE-3rdparty.csv | 4 +- packages/base/package.json | 3 +- .../base/src/helpers/__tests__/prompt.test.ts | 31 +- packages/base/src/helpers/inquirer.ts | 60 +++ packages/base/src/helpers/prompt.ts | 23 +- packages/plugin-cloud-run/package.json | 2 - packages/plugin-cloud-run/src/prompt.ts | 52 +-- packages/plugin-lambda/package.json | 2 - .../src/__tests__/prompt.test.ts | 94 ++-- .../plugin-lambda/src/functions/commons.ts | 6 +- packages/plugin-lambda/src/prompt.ts | 229 +++++----- yarn.lock | 428 ++++++++++++------ 12 files changed, 560 insertions(+), 374 deletions(-) create mode 100644 packages/base/src/helpers/inquirer.ts diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 02a64f8732..cf0ae17311 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -18,7 +18,6 @@ Component,Origin,Licence,Copyright @types/debug,dev,MIT,Copyright (c) Microsoft Corporation @types/tiny-async-pool,dev,MIT,Copyright (c) Microsoft Corporation @types/deep-extend,dev,MIT,Copyright Microsoft Corporation -@types/inquirer,dev,MIT,Copyright Microsoft Corporation @types/jest,dev,MIT,Copyright Microsoft Corporation @types/js-yaml,dev,MIT,Bart van der Schoor @types/node,dev,MIT,Copyright Microsoft Corporation @@ -45,8 +44,6 @@ form-data,import,MIT,Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.co fuzzy,import,MIT,Copyright (c) 2012 Matt York get-value,import,MIT,"Copyright (c) 2014-present, Jon Schlinkert." glob,import,ISC,Copyright (c) Isaac Z. Schlueter and Contributors -inquirer,import,MIT,Copyright (c) 2012 Simon Boudrias -inquirer-checkbox-plus-prompt,import,MIT,Copyright (c) 2018 Mohammad Anas Fares is-docker,import,MIT,Copyright (c) Sindre Sorhus (sindresorhus.com) jest,dev,MIT,"Copyright (c) Facebook, Inc. and its affiliates." js-yaml,import,MIT,Copyright (C) 2011-2015 by Vitaly Puzrin @@ -88,6 +85,7 @@ jszip,import,MIT,Copyright (c) 2009-2016 Stuart Knightley and other contributors @google-cloud/run,import,Apache-2.0,Copyright (c) 2023 Google LLC and other contributors google-auth-library,import,Apache-2.0,Copyright (c) 2023 Google LLC and other contributors @google-cloud/logging,import,Apache-2.0,Copyright (c) 2023 Google LLC and other contributors +@inquirer/prompts,import,MIT,Copyright (c) 2025 Simon Boudrias jest-diff,import,MIT,"Copyright (c) Meta Platforms, Inc. and other contributors" tsx,dev,MIT,Copyright (c) Hiroki Osame typescript-eslint,dev,MIT,Copyright (c) 2019 typescript-eslint and other contributors diff --git a/packages/base/package.json b/packages/base/package.json index 12928f1ba5..4b44e9bba9 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -117,6 +117,7 @@ }, "dependencies": { "@antfu/install-pkg": "1.1.0", + "@inquirer/prompts": "8.4.1", "@types/datadog-metrics": "0.6.1", "async-retry": "1.3.3", "chalk": "3.0.0", @@ -127,7 +128,6 @@ "fast-xml-parser": "5.5.9", "form-data": "4.0.4", "glob": "13.0.6", - "inquirer": "8.2.7", "is-docker": "4.0.0", "jest-diff": "30.2.0", "js-yaml": "4.1.1", @@ -145,7 +145,6 @@ "@types/async-retry": "1.4.8", "@types/debug": "4.1.12", "@types/deep-extend": "0.4.31", - "@types/inquirer": "8.2.6", "@types/js-yaml": "4.0.9", "@types/semver": "7.7.1", "@types/tiny-async-pool": "2.0.3" diff --git a/packages/base/src/helpers/__tests__/prompt.test.ts b/packages/base/src/helpers/__tests__/prompt.test.ts index 99c47bbee0..de9f602958 100644 --- a/packages/base/src/helpers/__tests__/prompt.test.ts +++ b/packages/base/src/helpers/__tests__/prompt.test.ts @@ -1,31 +1,40 @@ -jest.mock('inquirer') -import {prompt} from 'inquirer' +jest.mock('../inquirer', () => ({ + loadPrompts: jest.fn(), +})) +import {loadPrompts} from '../inquirer' import {confirmationQuestion, requestConfirmation, requestFilePath} from '../prompt' describe('prompt', () => { + const mockConfirm = jest.fn() + const mockInput = jest.fn() + + beforeEach(() => { + jest.resetAllMocks() + ;(loadPrompts as jest.Mock).mockResolvedValue({ + confirm: mockConfirm, + input: mockInput, + }) + }) + describe('confirmationQuestion', () => { test('returns question with provided message', async () => { const message = 'Do you want to continue?' const question = confirmationQuestion(message) - expect(await question.message).toBe(message) + expect(question.message).toBe(message) }) }) describe('requestConfirmation', () => { test('returns boolean when users responds to confirmation question', async () => { - ;(prompt as any).mockImplementation(() => - Promise.resolve({ - confirmation: true, - }) - ) + mockConfirm.mockResolvedValue(true) const confirmation = await requestConfirmation('Do you want to continue?') expect(confirmation).toBe(true) }) test('throws error when something unexpected happens while prompting', async () => { - ;(prompt as any).mockImplementation(() => Promise.reject(new Error('Unexpected error'))) + mockConfirm.mockRejectedValue(new Error('Unexpected error')) let error try { await requestConfirmation('Do you wanna continue?') @@ -42,14 +51,14 @@ describe('prompt', () => { const mockFilePath = '/Users/username/project/test.ts' test('returns the selected file path', async () => { - ;(prompt as any).mockImplementation(() => Promise.resolve({filePath: mockFilePath})) + mockInput.mockResolvedValue(mockFilePath) const selectedPath = await requestFilePath() expect(mockFilePath).toBe(selectedPath) }) test('throws error when something unexpected happens while prompting', async () => { - ;(prompt as any).mockImplementation(() => Promise.reject(new Error('Unexpected error'))) + mockInput.mockRejectedValue(new Error('Unexpected error')) let error try { await requestFilePath() diff --git a/packages/base/src/helpers/inquirer.ts b/packages/base/src/helpers/inquirer.ts new file mode 100644 index 0000000000..ddeb4507a8 --- /dev/null +++ b/packages/base/src/helpers/inquirer.ts @@ -0,0 +1,60 @@ +type PromptValidation = (value: Value) => boolean | string | Promise + +type Choice = + | Value + | { + checked?: boolean + description?: string + disabled?: boolean | string + name: string + short?: string + value: Value + } + +export type CheckboxConfig = { + choices: readonly Choice[] + default?: readonly Value[] + message: string + pageSize?: number + validate?: PromptValidation +} + +export type ConfirmConfig = { + default?: boolean + message: string +} + +export type InputConfig = { + default?: string + message: string + validate?: PromptValidation +} + +export type PasswordConfig = { + default?: string + mask?: boolean | string + message: string + validate?: PromptValidation +} + +export type SelectConfig = { + choices: readonly Choice[] + default?: Value + message: string +} + +export type InquirerPrompts = { + checkbox: (config: CheckboxConfig) => Promise + confirm: (config: ConfirmConfig) => Promise + input: (config: InputConfig) => Promise + password: (config: PasswordConfig) => Promise + select: (config: SelectConfig) => Promise +} + +// eslint-disable-next-line @typescript-eslint/no-implied-eval -- TypeScript rewrites plain `import()` to `require()` in our CommonJS emit. +const importInquirerPrompts = new Function('specifier', 'return import(specifier)') as ( + specifier: string +) => Promise + +// Preserve a real runtime dynamic import so Node can load the ESM-only prompt package from CommonJS output. +export const loadPrompts = () => importInquirerPrompts('@inquirer/prompts') diff --git a/packages/base/src/helpers/prompt.ts b/packages/base/src/helpers/prompt.ts index a5e0c75839..f275ec96a1 100644 --- a/packages/base/src/helpers/prompt.ts +++ b/packages/base/src/helpers/prompt.ts @@ -2,23 +2,20 @@ * @file Functions used to prompt the user for input. */ -import inquirer from 'inquirer' +import type {ConfirmConfig, InputConfig} from './inquirer' -export const confirmationQuestion = ( - message: string, - defaultValue = true -): inquirer.ConfirmQuestion<{confirmation: boolean}> => ({ +import {loadPrompts} from './inquirer' + +export const confirmationQuestion = (message: string, defaultValue = true): ConfirmConfig => ({ message, - name: 'confirmation', - type: 'confirm', default: defaultValue, }) export const requestConfirmation = async (message: string, defaultValue = true) => { try { - const confirmationAnswer = await inquirer.prompt(confirmationQuestion(message, defaultValue)) + const {confirm} = await loadPrompts() - return confirmationAnswer.confirmation + return await confirm(confirmationQuestion(message, defaultValue)) } catch (err) { if (err instanceof Error) { throw Error(`Couldn't receive confirmation. ${err.message}`) @@ -29,14 +26,12 @@ export const requestConfirmation = async (message: string, defaultValue = true) export const requestFilePath = async () => { try { - const question: inquirer.InputQuestion<{filePath: string}> = { + const question: InputConfig = { message: 'Please enter a file path, or press Enter to finish:', - name: 'filePath', - type: 'input', } - const filePathAnswer = await inquirer.prompt([question]) + const {input} = await loadPrompts() - return filePathAnswer.filePath + return await input(question) } catch (err) { if (err instanceof Error) { throw Error(`Couldn't receive file path. ${err.message}`) diff --git a/packages/plugin-cloud-run/package.json b/packages/plugin-cloud-run/package.json index 9d6bb04dc3..afd72503f0 100644 --- a/packages/plugin-cloud-run/package.json +++ b/packages/plugin-cloud-run/package.json @@ -46,8 +46,6 @@ "@google-cloud/run": "3.0.0", "chalk": "3.0.0", "google-auth-library": "10.2.1", - "inquirer": "8.2.7", - "inquirer-checkbox-plus-prompt": "1.4.2", "ora": "5.4.1", "upath": "2.0.1" } diff --git a/packages/plugin-cloud-run/src/prompt.ts b/packages/plugin-cloud-run/src/prompt.ts index 4172207428..0fa661c204 100644 --- a/packages/plugin-cloud-run/src/prompt.ts +++ b/packages/plugin-cloud-run/src/prompt.ts @@ -1,14 +1,12 @@ -import {DATADOG_SITES} from '@datadog/datadog-ci-base/constants' -import inquirer from 'inquirer' +import type {ConfirmConfig, InputConfig, SelectConfig} from '@datadog/datadog-ci-base/helpers/inquirer' -const checkboxPlusPrompt = require('inquirer-checkbox-plus-prompt') -inquirer.registerPrompt('checkbox-plus', checkboxPlusPrompt) +import {DATADOG_SITES} from '@datadog/datadog-ci-base/constants' +import {loadPrompts} from '@datadog/datadog-ci-base/helpers/inquirer' export const requestGCPProject = async (): Promise => { - const answer = await inquirer.prompt({ + const {input} = await loadPrompts() + const question: InputConfig = { message: 'Enter GCP Project ID:', - name: 'project', - type: 'input', validate: (value: string) => { if (!value || value.trim().length === 0) { return 'Project ID is required.' @@ -16,17 +14,16 @@ export const requestGCPProject = async (): Promise => { return true }, - }) + } - return answer.project + return input(question) } export const requestGCPRegion = async (defaultRegion?: string): Promise => { - const answer = await inquirer.prompt({ + const {input} = await loadPrompts() + const question: InputConfig = { default: defaultRegion || 'us-central1', message: 'Enter GCP Region:', - name: 'region', - type: 'input', validate: (value: string) => { if (!value || value.trim().length === 0) { return 'Region is required.' @@ -34,16 +31,15 @@ export const requestGCPRegion = async (defaultRegion?: string): Promise return true }, - }) + } - return answer.region + return input(question) } export const requestServiceName = async (): Promise => { - const answer = await inquirer.prompt({ + const {input} = await loadPrompts() + const question: InputConfig = { message: 'Enter Cloud Run service name:', - name: 'serviceName', - type: 'input', validate: (value: string) => { if (!value || value.trim().length === 0) { return 'Service name is required.' @@ -51,29 +47,27 @@ export const requestServiceName = async (): Promise => { return true }, - }) + } - return answer.serviceName + return input(question) } export const requestSite = async (): Promise => { - const answer = await inquirer.prompt({ + const {select} = await loadPrompts() + const question: SelectConfig = { choices: DATADOG_SITES, message: 'Select a Datadog Site:', - name: 'site', - type: 'list', - }) + } - return answer.site + return select(question) } export const requestConfirmation = async (message: string, defaultValue = true) => { - const confirmationAnswer = await inquirer.prompt({ + const {confirm} = await loadPrompts() + const question: ConfirmConfig = { message, - name: 'confirmation', - type: 'confirm', default: defaultValue, - }) + } - return confirmationAnswer.confirmation !== false + return confirm(question) } diff --git a/packages/plugin-lambda/package.json b/packages/plugin-lambda/package.json index c5ba673f42..4745186aa3 100644 --- a/packages/plugin-lambda/package.json +++ b/packages/plugin-lambda/package.json @@ -59,8 +59,6 @@ "chalk": "3.0.0", "clipanion": "3.2.1", "fuzzy": "0.1.3", - "inquirer": "8.2.7", - "inquirer-checkbox-plus-prompt": "1.4.2", "ora": "5.4.1", "upath": "2.0.1" } diff --git a/packages/plugin-lambda/src/__tests__/prompt.test.ts b/packages/plugin-lambda/src/__tests__/prompt.test.ts index e421fb05bc..2e443ac63b 100644 --- a/packages/plugin-lambda/src/__tests__/prompt.test.ts +++ b/packages/plugin-lambda/src/__tests__/prompt.test.ts @@ -1,7 +1,9 @@ -jest.mock('inquirer') +jest.mock('@datadog/datadog-ci-base/helpers/inquirer', () => ({ + loadPrompts: jest.fn(), +})) import {MOCK_DATADOG_API_KEY} from '@datadog/datadog-ci-base/helpers/__tests__/testing-tools' +import {loadPrompts} from '@datadog/datadog-ci-base/helpers/inquirer' import {CI_API_KEY_ENV_VAR, CI_SITE_ENV_VAR} from '@datadog/datadog-ci-base/helpers/serverless/constants' -import {prompt} from 'inquirer' import { AWS_ACCESS_KEY_ID_ENV_VAR, @@ -22,11 +24,28 @@ import { import {mockAwsAccessKeyId, mockAwsSecretAccessKey} from './fixtures' describe('prompt', () => { + const mockCheckbox = jest.fn() + const mockConfirm = jest.fn() + const mockInput = jest.fn() + const mockPassword = jest.fn() + const mockSelect = jest.fn() + + beforeEach(() => { + jest.resetAllMocks() + ;(loadPrompts as jest.Mock).mockResolvedValue({ + checkbox: mockCheckbox, + confirm: mockConfirm, + input: mockInput, + password: mockPassword, + select: mockSelect, + }) + }) + describe('datadogApiKeyTypeQuestion', () => { test('returns question with message pointing to the correct given site', async () => { const site = 'datadoghq.com' const question = datadogApiKeyTypeQuestion(site) - expect(await question.message).toBe( + expect(question.message).toBe( `Which type of Datadog API Key you want to set? \nLearn more at https://app.${site}/organization-settings/api-keys` ) }) @@ -39,8 +58,7 @@ describe('prompt', () => { message: 'API Key:', } const question = datadogEnvVarsQuestions(datadogApiKeyType) - expect(await question.message).toBe('API Key:') - expect(question.name).toBe(CI_API_KEY_ENV_VAR) + expect(question.message).toBe('API Key:') }) test('validates DATADOG_API_KEY correctly', () => { @@ -63,8 +81,7 @@ describe('prompt', () => { message: 'KMS API Key:', } const question = datadogEnvVarsQuestions(datadogApiKeyType) - expect(await question.message).toBe('KMS API Key:') - expect(question.name).toBe(CI_KMS_API_KEY_ENV_VAR) + expect(question.message).toBe('KMS API Key:') }) test('returns correct message when user selects DATADOG_API_KEY_SECRET_ARN', async () => { @@ -73,8 +90,7 @@ describe('prompt', () => { message: 'API Key Secret ARN:', } const question = datadogEnvVarsQuestions(datadogApiKeyType) - expect(await question.message).toBe('API Key Secret ARN:') - expect(question.name).toBe(CI_API_KEY_SECRET_ARN_ENV_VAR) + expect(question.message).toBe('API Key Secret ARN:') }) test('validates DATADOG_API_KEY_SECRET_ARN correctly', () => { @@ -98,7 +114,13 @@ describe('prompt', () => { test('returns question with the provided function names being its choices', () => { const functionNames = ['my-func', 'my-func-2', 'my-third-func'] const question = functionSelectionQuestion(functionNames) - expect(question.choices).toBe(functionNames) + expect(question.choices).toEqual( + functionNames.map((functionName) => ({ + checked: false, + name: functionName, + value: functionName, + })) + ) }) }) @@ -113,12 +135,8 @@ describe('prompt', () => { }) test('sets the AWS credentials as environment variables', async () => { - ;(prompt as any).mockImplementation(() => - Promise.resolve({ - [AWS_ACCESS_KEY_ID_ENV_VAR]: mockAwsAccessKeyId, - [AWS_SECRET_ACCESS_KEY_ENV_VAR]: mockAwsSecretAccessKey, - }) - ) + mockInput.mockResolvedValue(mockAwsAccessKeyId) + mockPassword.mockResolvedValueOnce(mockAwsSecretAccessKey).mockResolvedValueOnce(undefined) await requestAWSCredentials() @@ -127,13 +145,8 @@ describe('prompt', () => { }) test('sets the AWS credentials with session token as environment variables', async () => { - ;(prompt as any).mockImplementation(() => - Promise.resolve({ - [AWS_ACCESS_KEY_ID_ENV_VAR]: mockAwsAccessKeyId, - [AWS_SECRET_ACCESS_KEY_ENV_VAR]: mockAwsSecretAccessKey, - [AWS_SESSION_TOKEN_ENV_VAR]: 'some-session-token', - }) - ) + mockInput.mockResolvedValue(mockAwsAccessKeyId) + mockPassword.mockResolvedValueOnce(mockAwsSecretAccessKey).mockResolvedValueOnce('some-session-token') await requestAWSCredentials() @@ -143,7 +156,7 @@ describe('prompt', () => { }) test('throws error when something unexpected happens while prompting', async () => { - ;(prompt as any).mockImplementation(() => Promise.reject(new Error('Unexpected error'))) + mockInput.mockRejectedValue(new Error('Unexpected error')) let error try { await requestAWSCredentials() @@ -168,26 +181,11 @@ describe('prompt', () => { test('sets the Datadog Environment Variables as provided/selected by user', async () => { const site = 'datadoghq.com' - ;(prompt as any).mockImplementation((question: any) => { - switch (question.name) { - case CI_API_KEY_ENV_VAR: - return Promise.resolve({ - [CI_API_KEY_ENV_VAR]: MOCK_DATADOG_API_KEY, - }) - case CI_SITE_ENV_VAR: - return Promise.resolve({ - [CI_SITE_ENV_VAR]: 'datadoghq.com', - }) - case 'type': - return Promise.resolve({ - type: { - envVar: CI_API_KEY_ENV_VAR, - message: 'API Key:', - }, - }) - default: - } + mockSelect.mockResolvedValueOnce(site).mockResolvedValueOnce({ + envVar: CI_API_KEY_ENV_VAR, + message: 'API Key:', }) + mockInput.mockResolvedValue(MOCK_DATADOG_API_KEY) await requestDatadogEnvVars() @@ -196,7 +194,7 @@ describe('prompt', () => { }) test('throws error when something unexpected happens while prompting', async () => { - ;(prompt as any).mockImplementation(() => Promise.reject(new Error('Unexpected error'))) + mockSelect.mockRejectedValue(new Error('Unexpected error')) let error try { await requestDatadogEnvVars() @@ -213,14 +211,16 @@ describe('prompt', () => { const selectedFunctions = ['my-func', 'my-func-2', 'my-third-func'] test('returns the selected functions', async () => { - ;(prompt as any).mockImplementation(() => Promise.resolve({functions: selectedFunctions})) + mockInput.mockResolvedValue('') + mockCheckbox.mockResolvedValue(selectedFunctions) + mockConfirm.mockResolvedValue(false) const functions = await requestFunctionSelection(selectedFunctions) - expect(functions).toBe(selectedFunctions) + expect(functions).toEqual(selectedFunctions) }) test('throws error when something unexpected happens while prompting', async () => { - ;(prompt as any).mockImplementation(() => Promise.reject(new Error('Unexpected error'))) + mockInput.mockRejectedValue(new Error('Unexpected error')) let error try { await requestFunctionSelection(selectedFunctions) diff --git a/packages/plugin-lambda/src/functions/commons.ts b/packages/plugin-lambda/src/functions/commons.ts index c7784fd41c..5a99b16fd3 100644 --- a/packages/plugin-lambda/src/functions/commons.ts +++ b/packages/plugin-lambda/src/functions/commons.ts @@ -15,6 +15,7 @@ import type {Writable} from 'stream' import {ListFunctionsCommand, GetFunctionCommand, UpdateFunctionConfigurationCommand} from '@aws-sdk/client-lambda' import {fromIni, fromNodeProviderChain} from '@aws-sdk/credential-providers' +import {loadPrompts} from '@datadog/datadog-ci-base/helpers/inquirer' import * as helpersRenderer from '@datadog/datadog-ci-base/helpers/renderer' import { API_KEY_ENV_VAR, @@ -25,7 +26,6 @@ import {LAMBDA_LAYER_VERSIONS} from '@datadog/datadog-ci-base/helpers/serverless import {maskString} from '@datadog/datadog-ci-base/helpers/utils' import {isValidDatadogSite} from '@datadog/datadog-ci-base/helpers/validation' import {CredentialsProviderError} from '@smithy/property-provider' -import inquirer from 'inquirer' import { ARM64_ARCHITECTURE, @@ -164,9 +164,9 @@ export const getAWSFileCredentialsParams = (profile: string): FromIniInit => { // If provided profile is enforced by MFA and a session // token is not set we must request for the MFA token. init.mfaCodeProvider = async (mfaSerial) => { - const answer = await inquirer.prompt(awsProfileQuestion(mfaSerial)) + const {input} = await loadPrompts() - return answer.AWS_MFA + return input(awsProfileQuestion(mfaSerial)) } return init diff --git a/packages/plugin-lambda/src/prompt.ts b/packages/plugin-lambda/src/prompt.ts index bc94d71872..e34c4a46d4 100644 --- a/packages/plugin-lambda/src/prompt.ts +++ b/packages/plugin-lambda/src/prompt.ts @@ -1,4 +1,7 @@ +import type {CheckboxConfig, InputConfig, PasswordConfig, SelectConfig} from '@datadog/datadog-ci-base/helpers/inquirer' + import {DATADOG_SITES} from '@datadog/datadog-ci-base/constants' +import {loadPrompts} from '@datadog/datadog-ci-base/helpers/inquirer' import { CI_API_KEY_ENV_VAR, CI_SITE_ENV_VAR, @@ -9,7 +12,6 @@ import { import {isValidDatadogSite} from '@datadog/datadog-ci-base/helpers/validation' import chalk from 'chalk' import {filter} from 'fuzzy' -import inquirer from 'inquirer' import { AWS_ACCESS_KEY_ID_ENV_VAR, @@ -27,14 +29,14 @@ import { } from './constants' import {isMissingAnyDatadogApiKeyEnvVar, sentenceMatchesRegEx} from './functions/commons' -const checkboxPlusPrompt = require('inquirer-checkbox-plus-prompt') -inquirer.registerPrompt('checkbox-plus', checkboxPlusPrompt) +type DatadogApiKeyType = { + envVar: string + message: string +} -export const awsProfileQuestion = (mfaSerial: string): inquirer.InputQuestion => ({ +export const awsProfileQuestion = (mfaSerial: string): InputConfig => ({ default: undefined, message: `Enter MFA code for ${mfaSerial}: `, - name: 'AWS_MFA', - type: 'input', validate: (value) => { if (!value || value === undefined || value.length < 6) { return 'Enter a valid MFA token. Length must be greater than or equal to 6.' @@ -44,51 +46,40 @@ export const awsProfileQuestion = (mfaSerial: string): inquirer.InputQuestion => }, }) -const awsCredentialsQuestions: inquirer.QuestionCollection = [ - { - // AWS_ACCESS_KEY_ID question - message: 'Enter AWS Access Key ID:', - name: AWS_ACCESS_KEY_ID_ENV_VAR, - type: 'input', - validate: (value) => { - if (!value || !sentenceMatchesRegEx(value, AWS_ACCESS_KEY_ID_REG_EXP)) { - return 'Enter a valid AWS Access Key ID.' - } +const awsAccessKeyIdQuestion: InputConfig = { + message: 'Enter AWS Access Key ID:', + validate: (value) => { + if (!value || !sentenceMatchesRegEx(value, AWS_ACCESS_KEY_ID_REG_EXP)) { + return 'Enter a valid AWS Access Key ID.' + } - return true - }, + return true }, - { - // AWS_SECRET_ACCESS_KEY_ENV_VAR question - mask: true, - message: 'Enter AWS Secret Access Key:', - name: AWS_SECRET_ACCESS_KEY_ENV_VAR, - type: 'password', - validate: (value) => { - if (!value || !sentenceMatchesRegEx(value, AWS_SECRET_ACCESS_KEY_REG_EXP)) { - return 'Enter a valid AWS Secret Access Key.' - } +} - return true - }, - }, - { - // AWS_SESSION_TOKEN - mask: true, - message: 'Enter AWS Session Token (optional):', - name: AWS_SESSION_TOKEN_ENV_VAR, - type: 'password', +const awsSecretAccessKeyQuestion: PasswordConfig = { + mask: true, + message: 'Enter AWS Secret Access Key:', + validate: (value) => { + if (!value || !sentenceMatchesRegEx(value, AWS_SECRET_ACCESS_KEY_REG_EXP)) { + return 'Enter a valid AWS Secret Access Key.' + } + + return true }, -] +} -const awsRegionQuestion = (defaultRegion?: string): inquirer.InputQuestion => ({ +const awsSessionTokenQuestion: PasswordConfig = { + mask: true, + message: 'Enter AWS Session Token (optional):', +} + +const awsRegionQuestion = (defaultRegion?: string): InputConfig => ({ default: defaultRegion, message: 'Which AWS region (e.g., us-east-1) your Lambda functions are deployed?', - name: AWS_DEFAULT_REGION_ENV_VAR, - type: 'input', }) -export const datadogApiKeyTypeQuestion = (datadogSite: string): inquirer.ListQuestion => ({ +export const datadogApiKeyTypeQuestion = (datadogSite: string): SelectConfig => ({ choices: [ { name: `Plain text ${chalk.bold('API Key')} (Recommended for trial users) `, @@ -97,7 +88,6 @@ export const datadogApiKeyTypeQuestion = (datadogSite: string): inquirer.ListQue message: 'API Key:', }, }, - new inquirer.Separator(), { name: `API key encrypted with AWS Key Management Service ${chalk.bold('(KMS) API Key')}`, value: { @@ -123,52 +113,37 @@ export const datadogApiKeyTypeQuestion = (datadogSite: string): inquirer.ListQue message: `Which type of Datadog API Key you want to set? \nLearn more at ${chalk.blueBright( `https://app.${datadogSite}/organization-settings/api-keys` )}`, - name: 'type', - type: 'list', }) -const datadogSiteQuestion: inquirer.ListQuestion = { +const datadogSiteQuestion: SelectConfig = { // DATADOG SITE choices: DATADOG_SITES, message: `Select the Datadog site to send data. \nLearn more at ${chalk.blueBright( 'https://docs.datadoghq.com/getting_started/site/' )}`, - name: CI_SITE_ENV_VAR, - type: 'list', } -const envQuestion: inquirer.InputQuestion = { +const envQuestion: InputConfig = { default: undefined, - message: 'Enter a value for the environment variable DD_ENV', - suffix: chalk.dim(' (recommended)'), - name: ENVIRONMENT_ENV_VAR, - type: 'input', + message: `Enter a value for the environment variable DD_ENV${chalk.dim(' (recommended)')}`, } -const serviceQuestion: inquirer.InputQuestion = { +const serviceQuestion: InputConfig = { default: undefined, - message: 'Enter a value for the environment variable DD_SERVICE', - suffix: chalk.dim(' (recommended)'), - name: SERVICE_ENV_VAR, - type: 'input', + message: `Enter a value for the environment variable DD_SERVICE${chalk.dim(' (recommended)')}`, } -const versionQuestion: inquirer.InputQuestion = { +const versionQuestion: InputConfig = { default: undefined, - message: 'Enter a value for the environment variable DD_VERSION', - suffix: chalk.dim(' (recommended)'), - name: VERSION_ENV_VAR, - type: 'input', + message: `Enter a value for the environment variable DD_VERSION${chalk.dim(' (recommended)')}`, } const INVALID_KEY_MESSAGE = 'Enter a valid Datadog API Key.' -export const datadogEnvVarsQuestions = (datadogApiKeyType: Record): inquirer.InputQuestion => ({ +export const datadogEnvVarsQuestions = (datadogApiKeyType: DatadogApiKeyType): InputConfig => ({ // DATADOG API KEY given type default: process.env[datadogApiKeyType.envVar], message: datadogApiKeyType.message, - name: datadogApiKeyType.envVar, - type: 'input', validate: (value) => { if (!value) { return INVALID_KEY_MESSAGE @@ -193,26 +168,39 @@ export const datadogEnvVarsQuestions = (datadogApiKeyType: Record): }, }) -export const functionSelectionQuestion = (functionNames: string[]): typeof checkboxPlusPrompt => ({ - choices: functionNames, - highlight: true, - message: - 'Select the functions to modify (Press to select, p.s. start typing the name instead of manually scrolling)', - name: 'functions', - pageSize: 10, - searchable: true, - source: (answersSoFar: unknown, input: string) => { - input = input || '' - - return new Promise((resolve) => { - const fuzzyResult = filter(input, functionNames) - const data = fuzzyResult.map((element) => element.original) - resolve(data) - }) +const getFilteredFunctionNames = (functionNames: string[], searchTerm?: string) => { + if (!searchTerm) { + return functionNames + } + + return filter(searchTerm, functionNames).map((element) => element.original) +} + +const functionSearchQuestion = (functionNames: string[]): InputConfig => ({ + default: '', + message: 'Filter functions to modify (press Enter to show all functions):', + validate: (value) => { + if (getFilteredFunctionNames(functionNames, value).length < 1) { + return 'No functions matched that filter. Try another search term.' + } + + return true }, - type: 'checkbox-plus', - validate: (selectedFunctions: string | string[]) => { - if (selectedFunctions.length < 1) { +}) + +export const functionSelectionQuestion = ( + functionNames: string[], + selectedFunctions: string[] = [] +): CheckboxConfig => ({ + choices: functionNames.map((functionName) => ({ + checked: selectedFunctions.includes(functionName), + name: functionName, + value: functionName, + })), + pageSize: 10, + message: 'Select the functions to modify (Press to select and to continue)', + validate: (selectedFunctionNames) => { + if (selectedFunctionNames.length < 1) { return 'You must choose at least one function.' } @@ -222,11 +210,13 @@ export const functionSelectionQuestion = (functionNames: string[]): typeof check export const requestAWSCredentials = async (): Promise => { try { - const awsCredentialsAnswers = await inquirer.prompt(awsCredentialsQuestions) - process.env[AWS_ACCESS_KEY_ID_ENV_VAR] = awsCredentialsAnswers[AWS_ACCESS_KEY_ID_ENV_VAR] - process.env[AWS_SECRET_ACCESS_KEY_ENV_VAR] = awsCredentialsAnswers[AWS_SECRET_ACCESS_KEY_ENV_VAR] - if (awsCredentialsAnswers[AWS_SESSION_TOKEN_ENV_VAR] !== undefined) { - process.env[AWS_SESSION_TOKEN_ENV_VAR] = awsCredentialsAnswers[AWS_SESSION_TOKEN_ENV_VAR] + const {input, password} = await loadPrompts() + process.env[AWS_ACCESS_KEY_ID_ENV_VAR] = await input(awsAccessKeyIdQuestion) + process.env[AWS_SECRET_ACCESS_KEY_ENV_VAR] = await password(awsSecretAccessKeyQuestion) + + const awsSessionToken = await password(awsSessionTokenQuestion) + if (awsSessionToken !== undefined) { + process.env[AWS_SESSION_TOKEN_ENV_VAR] = awsSessionToken } } catch (e) { if (e instanceof Error) { @@ -237,8 +227,8 @@ export const requestAWSCredentials = async (): Promise => { export const requestAWSRegion = async (defaultRegion?: string): Promise => { try { - const awsRegionAnswer = await inquirer.prompt(awsRegionQuestion(defaultRegion)) - process.env[AWS_DEFAULT_REGION_ENV_VAR] = awsRegionAnswer[AWS_DEFAULT_REGION_ENV_VAR] + const {input} = await loadPrompts() + process.env[AWS_DEFAULT_REGION_ENV_VAR] = await input(awsRegionQuestion(defaultRegion)) } catch (e) { if (e instanceof Error) { throw Error(`Couldn't set AWS region. ${e.message}`) @@ -248,20 +238,19 @@ export const requestAWSRegion = async (defaultRegion?: string): Promise => export const requestDatadogEnvVars = async (): Promise => { try { + const {input, select} = await loadPrompts() const envSite = process.env[CI_SITE_ENV_VAR] let selectedDatadogSite = envSite if (!isValidDatadogSite(envSite)) { - const datadogSiteAnswer = await inquirer.prompt(datadogSiteQuestion) - selectedDatadogSite = datadogSiteAnswer[CI_SITE_ENV_VAR] + selectedDatadogSite = await select(datadogSiteQuestion) process.env[CI_SITE_ENV_VAR] = selectedDatadogSite } if (isMissingAnyDatadogApiKeyEnvVar()) { - const datadogApiKeyTypeAnswer = await inquirer.prompt(datadogApiKeyTypeQuestion(selectedDatadogSite!)) - const datadogApiKeyType = datadogApiKeyTypeAnswer.type - const datadogEnvVars = await inquirer.prompt(datadogEnvVarsQuestions(datadogApiKeyType)) + const datadogApiKeyType = await select(datadogApiKeyTypeQuestion(selectedDatadogSite!)) + const datadogEnvVar = await input(datadogEnvVarsQuestions(datadogApiKeyType)) const selectedDatadogApiKeyEnvVar = datadogApiKeyType.envVar - process.env[selectedDatadogApiKeyEnvVar] = datadogEnvVars[selectedDatadogApiKeyEnvVar] + process.env[selectedDatadogApiKeyEnvVar] = datadogEnvVar } } catch (e) { if (e instanceof Error) { @@ -272,17 +261,10 @@ export const requestDatadogEnvVars = async (): Promise => { export const requestEnvServiceVersion = async (): Promise => { try { - const envQuestionAnswer = await inquirer.prompt(envQuestion) - const inputedEnvQuestionAnswer = envQuestionAnswer[ENVIRONMENT_ENV_VAR] - process.env[ENVIRONMENT_ENV_VAR] = inputedEnvQuestionAnswer - - const serviceQuestionAnswer = await inquirer.prompt(serviceQuestion) - const inputedServiceQuestionAnswer = serviceQuestionAnswer[SERVICE_ENV_VAR] - process.env[SERVICE_ENV_VAR] = inputedServiceQuestionAnswer - - const versionQuestionAnswer = await inquirer.prompt(versionQuestion) - const inputedVersionQuestionAnswer = versionQuestionAnswer[VERSION_ENV_VAR] - process.env[VERSION_ENV_VAR] = inputedVersionQuestionAnswer + const {input} = await loadPrompts() + process.env[ENVIRONMENT_ENV_VAR] = await input(envQuestion) + process.env[SERVICE_ENV_VAR] = await input(serviceQuestion) + process.env[VERSION_ENV_VAR] = await input(versionQuestion) } catch (e) { if (e instanceof Error) { throw Error(`Couldn't set user defined env, service, and version environment variables. ${e.message}`) @@ -290,14 +272,37 @@ export const requestEnvServiceVersion = async (): Promise => { } } -export const requestFunctionSelection = async (functionNames: string[]): Promise => { +export const requestFunctionSelection = async (functionNames: string[]): Promise => { try { - const selectedFunctionsAnswer: any = await inquirer.prompt(functionSelectionQuestion(functionNames)) + const {checkbox, confirm, input} = await loadPrompts() + const selectedFunctions = new Set() + let continueFiltering = true + + while (continueFiltering) { + const searchTerm = await input(functionSearchQuestion(functionNames)) + const filteredFunctionNames = getFilteredFunctionNames(functionNames, searchTerm) + const filteredSelection = await checkbox(functionSelectionQuestion(filteredFunctionNames, [...selectedFunctions])) + + for (const functionName of filteredFunctionNames) { + selectedFunctions.delete(functionName) + } - return selectedFunctionsAnswer.functions + for (const functionName of filteredSelection) { + selectedFunctions.add(functionName) + } + + continueFiltering = await confirm({ + default: false, + message: 'Would you like to apply another filter to add or remove more functions?', + }) + } + + return [...selectedFunctions] } catch (e) { if (e instanceof Error) { throw Error(`Couldn't receive selected functions. ${e.message}`) } + + throw e } } diff --git a/yarn.lock b/yarn.lock index 903108c1e0..7d8f3cc49e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1955,11 +1955,11 @@ __metadata: resolution: "@datadog/datadog-ci-base@workspace:packages/base" dependencies: "@antfu/install-pkg": "npm:1.1.0" + "@inquirer/prompts": "npm:8.4.1" "@types/async-retry": "npm:1.4.8" "@types/datadog-metrics": "npm:0.6.1" "@types/debug": "npm:4.1.12" "@types/deep-extend": "npm:0.4.31" - "@types/inquirer": "npm:8.2.6" "@types/js-yaml": "npm:4.0.9" "@types/semver": "npm:7.7.1" "@types/tiny-async-pool": "npm:2.0.3" @@ -1972,7 +1972,6 @@ __metadata: fast-xml-parser: "npm:5.5.9" form-data: "npm:4.0.4" glob: "npm:13.0.6" - inquirer: "npm:8.2.7" is-docker: "npm:4.0.0" jest-diff: "npm:30.2.0" js-yaml: "npm:4.1.1" @@ -2053,8 +2052,6 @@ __metadata: "@google-cloud/run": "npm:3.0.0" chalk: "npm:3.0.0" google-auth-library: "npm:10.2.1" - inquirer: "npm:8.2.7" - inquirer-checkbox-plus-prompt: "npm:1.4.2" ora: "npm:5.4.1" upath: "npm:2.0.1" languageName: unknown @@ -2147,8 +2144,6 @@ __metadata: chalk: "npm:3.0.0" clipanion: "npm:3.2.1" fuzzy: "npm:0.1.3" - inquirer: "npm:8.2.7" - inquirer-checkbox-plus-prompt: "npm:1.4.2" ora: "npm:5.4.1" upath: "npm:2.0.1" languageName: unknown @@ -2848,18 +2843,244 @@ __metadata: languageName: node linkType: hard -"@inquirer/external-editor@npm:^1.0.0": - version: 1.0.3 - resolution: "@inquirer/external-editor@npm:1.0.3" +"@inquirer/ansi@npm:^2.0.5": + version: 2.0.5 + resolution: "@inquirer/ansi@npm:2.0.5" + checksum: 10/482f8a606885ee0377a60eb5e9b303ae75fcfb2c6250819be348047c89e4e01a25feef369d3646dec7ba17e38cd5cc08271db6db21c401be315b3ada749e6b53 + languageName: node + linkType: hard + +"@inquirer/checkbox@npm:^5.1.3": + version: 5.1.3 + resolution: "@inquirer/checkbox@npm:5.1.3" + dependencies: + "@inquirer/ansi": "npm:^2.0.5" + "@inquirer/core": "npm:^11.1.8" + "@inquirer/figures": "npm:^2.0.5" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/f2c16ec308910552d4ab2cb063b66566de7bef7795fc4e246f430d6c12874be0e60015e5b2706e7543a4795252d9e42cc38857c687dc4fbfd584f1a4ad3095c5 + languageName: node + linkType: hard + +"@inquirer/confirm@npm:^6.0.11": + version: 6.0.11 + resolution: "@inquirer/confirm@npm:6.0.11" + dependencies: + "@inquirer/core": "npm:^11.1.8" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/f51ead4a6a68ac585257e66bbe8196a6b7aec1956b12038827a2d03a509b9db8e0ece97d4b92033259090de33d9aefd0cff288cd4dce6f472d927ef8fe9302f5 + languageName: node + linkType: hard + +"@inquirer/core@npm:^11.1.8": + version: 11.1.8 + resolution: "@inquirer/core@npm:11.1.8" + dependencies: + "@inquirer/ansi": "npm:^2.0.5" + "@inquirer/figures": "npm:^2.0.5" + "@inquirer/type": "npm:^4.0.5" + cli-width: "npm:^4.1.0" + fast-wrap-ansi: "npm:^0.2.0" + mute-stream: "npm:^3.0.0" + signal-exit: "npm:^4.1.0" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/e034f637ea9c12c2aaf8f5b128611f9d72976b50cf387f1207e0459342924c64f2de7e675e2b86616c44daf05700c4764f83e7ca1417aa41ed3d29d458062218 + languageName: node + linkType: hard + +"@inquirer/editor@npm:^5.1.0": + version: 5.1.0 + resolution: "@inquirer/editor@npm:5.1.0" + dependencies: + "@inquirer/core": "npm:^11.1.8" + "@inquirer/external-editor": "npm:^3.0.0" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/91918966cff21b7cbf28fefc1faa29ecc89ab13d4e9311ba7f25df6eef18180387b04c45cb237ec4b1c529229fb4c9a587ddaa1619a80b4b3d41dbea24c1fc84 + languageName: node + linkType: hard + +"@inquirer/expand@npm:^5.0.12": + version: 5.0.12 + resolution: "@inquirer/expand@npm:5.0.12" + dependencies: + "@inquirer/core": "npm:^11.1.8" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/286c8592c63cb9ed647b30729ad43036859a32a66f3c18c84d4ea7694f60b3aaa53e968a46ad7ac0b09017c593067e2dded19514bd2303f088c5b5984130e579 + languageName: node + linkType: hard + +"@inquirer/external-editor@npm:^3.0.0": + version: 3.0.0 + resolution: "@inquirer/external-editor@npm:3.0.0" dependencies: chardet: "npm:^2.1.1" - iconv-lite: "npm:^0.7.0" + iconv-lite: "npm:^0.7.2" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/a2b0a255601f563317c21547778fb081d0356de478ffa70eb29a9e2247761a76b97fb7f50dcc5e1e3cafb2f888f3ac684374c35f929d1f8b280361c6c66c97d0 + languageName: node + linkType: hard + +"@inquirer/figures@npm:^2.0.5": + version: 2.0.5 + resolution: "@inquirer/figures@npm:2.0.5" + checksum: 10/e4d09c11a75206578abcfd8fc69b0f54cff7a853826696df5b3a45ed24ebc5c82e8998f1e9fa42119de848e6a0a526a6ac476053800413637bf6d21c2116cc60 + languageName: node + linkType: hard + +"@inquirer/input@npm:^5.0.11": + version: 5.0.11 + resolution: "@inquirer/input@npm:5.0.11" + dependencies: + "@inquirer/core": "npm:^11.1.8" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/b7e7fa15b9e57ccc6078bd43766700b2f416f9aaf801195379434b8ed8a3df0cb7ea9d4ba7e468a3440ff61df872c85d481e529341d45cf9e8287caf621dc79d + languageName: node + linkType: hard + +"@inquirer/number@npm:^4.0.11": + version: 4.0.11 + resolution: "@inquirer/number@npm:4.0.11" + dependencies: + "@inquirer/core": "npm:^11.1.8" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/3f0c39e1d4de63acfa97431bc92a0c03c83852adac34e2f60c6b7efb5b03ac39ed3961375246561083bb4d7b84ab1314b22f3161568ac4cf9058fc5bf3d34370 + languageName: node + linkType: hard + +"@inquirer/password@npm:^5.0.11": + version: 5.0.11 + resolution: "@inquirer/password@npm:5.0.11" + dependencies: + "@inquirer/ansi": "npm:^2.0.5" + "@inquirer/core": "npm:^11.1.8" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/9863177e5515ac6f49f1ed5bf780e20ffe39f1044b6f3c19c085b39bdf27365c4c7e16b909af2a6d11a0ef7d5e166360689cdd80b62272f16618245c81afd350 + languageName: node + linkType: hard + +"@inquirer/prompts@npm:8.4.1": + version: 8.4.1 + resolution: "@inquirer/prompts@npm:8.4.1" + dependencies: + "@inquirer/checkbox": "npm:^5.1.3" + "@inquirer/confirm": "npm:^6.0.11" + "@inquirer/editor": "npm:^5.1.0" + "@inquirer/expand": "npm:^5.0.12" + "@inquirer/input": "npm:^5.0.11" + "@inquirer/number": "npm:^4.0.11" + "@inquirer/password": "npm:^5.0.11" + "@inquirer/rawlist": "npm:^5.2.7" + "@inquirer/search": "npm:^4.1.7" + "@inquirer/select": "npm:^5.1.3" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/791e43eabd38879ebcc6c4768fcd8497d6a5625e0d6f667ec4a474bd2818fef3957d3538d0afdd07e4463fc514060ad797802266bbc0abe3e9dda667384a9233 + languageName: node + linkType: hard + +"@inquirer/rawlist@npm:^5.2.7": + version: 5.2.7 + resolution: "@inquirer/rawlist@npm:5.2.7" + dependencies: + "@inquirer/core": "npm:^11.1.8" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/1828c53bb54bd18fb9a2ea680977a0b0365a7509d6e47f59bd4641c859d51a0a0582b3671a2c8796073c4aea9eee90b57bc90570c73cc4379a54362585be519a + languageName: node + linkType: hard + +"@inquirer/search@npm:^4.1.7": + version: 4.1.7 + resolution: "@inquirer/search@npm:4.1.7" + dependencies: + "@inquirer/core": "npm:^11.1.8" + "@inquirer/figures": "npm:^2.0.5" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/6378ce146bcc1b45ed14bae37e4b6f215bca4b1cf900fcaee83d8ea01623c93f70c731bdcaabbab1bf7026e31f9baa254a5c45ecdada977162de0b850d2829e1 + languageName: node + linkType: hard + +"@inquirer/select@npm:^5.1.3": + version: 5.1.3 + resolution: "@inquirer/select@npm:5.1.3" + dependencies: + "@inquirer/ansi": "npm:^2.0.5" + "@inquirer/core": "npm:^11.1.8" + "@inquirer/figures": "npm:^2.0.5" + "@inquirer/type": "npm:^4.0.5" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/d5d330544dce6348764dee6373ef0f2e6377678fa75cd4d562aaf2cdfed90905689c0ee810dc45f15482e67395e527ceecfeea52dc86af274b15212a72c600bb + languageName: node + linkType: hard + +"@inquirer/type@npm:^4.0.5": + version: 4.0.5 + resolution: "@inquirer/type@npm:4.0.5" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/c95d7237a885b32031715089f92820525731d4d3c2bd7afdb826307dc296cc2b39e7a644b0bb265441963348cca42e7785feb29c3aaf18fd2b63131769bf6587 + checksum: 10/83d15e11cc0586373070e8c262f69b1d1e4a6c72f58b3afb3d163479309f5a9bb584320eec2d85474506fb845a114e2c50010758fcf3af56c93293d579f76333 languageName: node linkType: hard @@ -4771,16 +4992,6 @@ __metadata: languageName: node linkType: hard -"@types/inquirer@npm:8.2.6": - version: 8.2.6 - resolution: "@types/inquirer@npm:8.2.6" - dependencies: - "@types/through": "npm:*" - rxjs: "npm:^7.2.0" - checksum: 10/7f93b12b4da7a3a8bca270b6adca761e88a6c8a05b6ea61553ced2d92f26795143ff886792057028b68ea94ed00b610b2cbed317d13e96e3e520a09bc48f03a7 - languageName: node - linkType: hard - "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -4959,15 +5170,6 @@ __metadata: languageName: node linkType: hard -"@types/through@npm:*": - version: 0.0.33 - resolution: "@types/through@npm:0.0.33" - dependencies: - "@types/node": "npm:*" - checksum: 10/fd0b73f873a64ed5366d1d757c42e5dbbb2201002667c8958eda7ca02fff09d73de91360572db465ee00240c32d50c6039ea736d8eca374300f9664f93e8da39 - languageName: node - linkType: hard - "@types/tiny-async-pool@npm:2.0.3": version: 2.0.3 resolution: "@types/tiny-async-pool@npm:2.0.3" @@ -6106,16 +6308,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": - version: 4.1.2 - resolution: "chalk@npm:4.1.2" - dependencies: - ansi-styles: "npm:^4.1.0" - supports-color: "npm:^7.1.0" - checksum: 10/cb3f3e594913d63b1814d7ca7c9bafbf895f75fbf93b92991980610dfd7b48500af4e3a5d4e3a8f337990a96b168d7eb84ee55efdce965e2ee8efc20f8c8f139 - languageName: node - linkType: hard - "chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -6127,6 +6319,16 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.2": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: 10/cb3f3e594913d63b1814d7ca7c9bafbf895f75fbf93b92991980610dfd7b48500af4e3a5d4e3a8f337990a96b168d7eb84ee55efdce965e2ee8efc20f8c8f139 + languageName: node + linkType: hard + "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -6192,10 +6394,10 @@ __metadata: languageName: node linkType: hard -"cli-width@npm:^3.0.0": - version: 3.0.0 - resolution: "cli-width@npm:3.0.0" - checksum: 10/8730848b04fb189666ab037a35888d191c8f05b630b1d770b0b0e4c920b47bb5cc14bddf6b8ffe5bfc66cee97c8211d4d18e756c1ffcc75d7dbe7e1186cd7826 +"cli-width@npm:^4.1.0": + version: 4.1.0 + resolution: "cli-width@npm:4.1.0" + checksum: 10/b58876fbf0310a8a35c79b72ecfcf579b354e18ad04e6b20588724ea2b522799a758507a37dfe132fafaf93a9922cafd9514d9e1598e6b2cd46694853aed099f languageName: node linkType: hard @@ -7538,6 +7740,22 @@ __metadata: languageName: node linkType: hard +"fast-string-truncated-width@npm:^3.0.2": + version: 3.0.3 + resolution: "fast-string-truncated-width@npm:3.0.3" + checksum: 10/3a1631e48927cb558b612a90ee78a61a660823c39b024bfc113935760b5b64805dbf03c4e696c33005294db578417687432e9d13567f1a582c2c75015e8a7648 + languageName: node + linkType: hard + +"fast-string-width@npm:^3.0.2": + version: 3.0.2 + resolution: "fast-string-width@npm:3.0.2" + dependencies: + fast-string-truncated-width: "npm:^3.0.2" + checksum: 10/5b9019769f2b00b96d43575c202f4e035a0e55eba7669a9a32351de9fa0805d0959a2afcaec6e4db5ee9b9a4c08d8e77f95abeb04b5bae2f76635cf04ddb4b80 + languageName: node + linkType: hard + "fast-uri@npm:^3.0.1": version: 3.0.1 resolution: "fast-uri@npm:3.0.1" @@ -7545,6 +7763,15 @@ __metadata: languageName: node linkType: hard +"fast-wrap-ansi@npm:^0.2.0": + version: 0.2.0 + resolution: "fast-wrap-ansi@npm:0.2.0" + dependencies: + fast-string-width: "npm:^3.0.2" + checksum: 10/e717a249dae84c9a964e6b5da05c373fadd92714b2afb2d6c7e6f766c3409c773c95b28e186dcdd397e2d7850533dbdd766845d0cd29e15d172d33128f9447d3 + languageName: node + linkType: hard + "fast-xml-builder@npm:^1.1.4": version: 1.1.4 resolution: "fast-xml-builder@npm:1.1.4" @@ -7634,15 +7861,6 @@ __metadata: languageName: node linkType: hard -"figures@npm:^3.0.0": - version: 3.2.0 - resolution: "figures@npm:3.2.0" - dependencies: - escape-string-regexp: "npm:^1.0.5" - checksum: 10/a3bf94e001be51d3770500789157f067218d4bc681a65e1f69d482de15120bcac822dceb1a7b3803f32e4e3a61a46df44f7f2c8ba95d6375e7491502e0dd3d97 - languageName: node - linkType: hard - "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -8397,7 +8615,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.7.0": +"iconv-lite@npm:^0.7.2": version: 0.7.2 resolution: "iconv-lite@npm:0.7.2" dependencies: @@ -8506,44 +8724,6 @@ __metadata: languageName: node linkType: hard -"inquirer-checkbox-plus-prompt@npm:1.4.2": - version: 1.4.2 - resolution: "inquirer-checkbox-plus-prompt@npm:1.4.2" - dependencies: - chalk: "npm:4.1.2" - cli-cursor: "npm:^3.1.0" - figures: "npm:^3.0.0" - lodash: "npm:^4.17.5" - rxjs: "npm:^6.6.7" - peerDependencies: - inquirer: < 9.x - checksum: 10/ab7c8db7b5dfb0d0963b1e8be61b0a3ba0f5994597485db40d49a02f41d7d20ed334c4b393ec549249a1427940106b0d4586668c8e429e19eb7d52dbc2817ec9 - languageName: node - linkType: hard - -"inquirer@npm:8.2.7": - version: 8.2.7 - resolution: "inquirer@npm:8.2.7" - dependencies: - "@inquirer/external-editor": "npm:^1.0.0" - ansi-escapes: "npm:^4.2.1" - chalk: "npm:^4.1.1" - cli-cursor: "npm:^3.1.0" - cli-width: "npm:^3.0.0" - figures: "npm:^3.0.0" - lodash: "npm:^4.17.21" - mute-stream: "npm:0.0.8" - ora: "npm:^5.4.1" - run-async: "npm:^2.4.0" - rxjs: "npm:^7.5.5" - string-width: "npm:^4.1.0" - strip-ansi: "npm:^6.0.0" - through: "npm:^2.3.6" - wrap-ansi: "npm:^6.0.1" - checksum: 10/526fb5ca55a29decda9b67c7b2bd437730152104c6e7c5f0d7ade90af6dc999371e1602ce86eb4a39ee3d91993501cddec32e4fe3f599723f2b653b02b685e3b - languageName: node - linkType: hard - "into-stream@npm:^6.0.0": version: 6.0.0 resolution: "into-stream@npm:6.0.0" @@ -9708,7 +9888,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.14, lodash@npm:^4.17.21, lodash@npm:^4.17.5": +"lodash@npm:^4.17.14, lodash@npm:^4.17.21": version: 4.17.23 resolution: "lodash@npm:4.17.23" checksum: 10/82504c88250f58da7a5a4289f57a4f759c44946c005dd232821c7688b5fcfbf4a6268f6a6cdde4b792c91edd2f3b5398c1d2a0998274432cff76def48735e233 @@ -10064,10 +10244,10 @@ __metadata: languageName: node linkType: hard -"mute-stream@npm:0.0.8": - version: 0.0.8 - resolution: "mute-stream@npm:0.0.8" - checksum: 10/a2d2e79dde87e3424ffc8c334472c7f3d17b072137734ca46e6f221131f1b014201cc593b69a38062e974fb2394d3d1cb4349f80f012bbf8b8ac1b28033e515f +"mute-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "mute-stream@npm:3.0.0" + checksum: 10/bee5db5c996a4585dbffc49e51fea10f3582d7f65441db9bc63126f16269541713c6ccb5a6fe37e08f627967b6eb28dd6b35e54a8dce53cf3837d7e010917b43 languageName: node linkType: hard @@ -10355,7 +10535,7 @@ __metadata: languageName: node linkType: hard -"ora@npm:5.4.1, ora@npm:^5.4.1": +"ora@npm:5.4.1": version: 5.4.1 resolution: "ora@npm:5.4.1" dependencies: @@ -11306,13 +11486,6 @@ __metadata: languageName: node linkType: hard -"run-async@npm:^2.4.0": - version: 2.4.1 - resolution: "run-async@npm:2.4.1" - checksum: 10/c79551224dafa26ecc281cb1efad3510c82c79116aaf681f8a931ce70fdf4ca880d58f97d3b930a38992c7aad7955a08e065b32ec194e1dd49d7790c874ece50 - languageName: node - linkType: hard - "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -11322,24 +11495,6 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^6.6.7": - version: 6.6.7 - resolution: "rxjs@npm:6.6.7" - dependencies: - tslib: "npm:^1.9.0" - checksum: 10/c8263ebb20da80dd7a91c452b9e96a178331f402344bbb40bc772b56340fcd48d13d1f545a1e3d8e464893008c5e306cc42a1552afe0d562b1a6d4e1e6262b03 - languageName: node - linkType: hard - -"rxjs@npm:^7.2.0, rxjs@npm:^7.5.5": - version: 7.8.1 - resolution: "rxjs@npm:7.8.1" - dependencies: - tslib: "npm:^2.1.0" - checksum: 10/b10cac1a5258f885e9dd1b70d23c34daeb21b61222ee735d2ec40a8685bdca40429000703a44f0e638c27a684ac139e1c37e835d2a0dc16f6fc061a138ae3abb - languageName: node - linkType: hard - "safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" @@ -11474,7 +11629,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f @@ -12065,13 +12220,6 @@ __metadata: languageName: node linkType: hard -"through@npm:^2.3.6": - version: 2.3.8 - resolution: "through@npm:2.3.8" - checksum: 10/5da78346f70139a7d213b65a0106f3c398d6bc5301f9248b5275f420abc2c4b1e77c2abc72d218dedc28c41efb2e7c312cb76a7730d04f9c2d37d247da3f4198 - languageName: node - linkType: hard - "tiny-async-pool@npm:2.1.0": version: 2.1.0 resolution: "tiny-async-pool@npm:2.1.0" @@ -12292,13 +12440,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.9.0": - version: 1.14.1 - resolution: "tslib@npm:1.14.1" - checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb - languageName: node - linkType: hard - "tslib@npm:^2.0.1, tslib@npm:^2.2.0, tslib@npm:^2.4.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" @@ -12776,17 +12917,6 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^6.0.1": - version: 6.2.0 - resolution: "wrap-ansi@npm:6.2.0" - dependencies: - ansi-styles: "npm:^4.0.0" - string-width: "npm:^4.1.0" - strip-ansi: "npm:^6.0.0" - checksum: 10/0d64f2d438e0b555e693b95aee7b2689a12c3be5ac458192a1ce28f542a6e9e59ddfecc37520910c2c88eb1f82a5411260566dba5064e8f9895e76e169e76187 - languageName: node - linkType: hard - "wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" From fe92e41f155a84a8c7a6ae53c28045fdd3a5a833 Mon Sep 17 00:00:00 2001 From: corentin Date: Fri, 10 Apr 2026 15:07:50 +0200 Subject: [PATCH 2/4] Ignore dynamic tooling dependencies in Knip --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index cb500c77b5..31412e7289 100644 --- a/package.json +++ b/package.json @@ -74,8 +74,11 @@ "knip": { "ignoreDependencies": [ "@datadog/datadog-ci-plugin-*", + "@inquirer/prompts", "@microsoft/eslint-formatter-sarif", "dd-trace", + "glob", + "tsdown", "syncpack" ] }, From c331ab6cdccf865ffd93b0ac7c61bcac28824764 Mon Sep 17 00:00:00 2001 From: corentin Date: Fri, 10 Apr 2026 17:21:09 +0200 Subject: [PATCH 3/4] Add searchable Lambda checkbox prompt with Inquirer core --- package.json | 1 + packages/base/package.json | 1 + packages/base/src/helpers/inquirer.ts | 58 +++++- .../src/__tests__/prompt.test.ts | 34 +++- packages/plugin-lambda/src/prompt.ts | 70 +------ .../src/searchable-checkbox-prompt.ts | 189 ++++++++++++++++++ yarn.lock | 3 +- 7 files changed, 281 insertions(+), 75 deletions(-) create mode 100644 packages/plugin-lambda/src/searchable-checkbox-prompt.ts diff --git a/package.json b/package.json index 31412e7289..5572c8ec23 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "ignoreDependencies": [ "@datadog/datadog-ci-plugin-*", "@inquirer/prompts", + "@inquirer/core", "@microsoft/eslint-formatter-sarif", "dd-trace", "glob", diff --git a/packages/base/package.json b/packages/base/package.json index 4b44e9bba9..9127e4ffc5 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -117,6 +117,7 @@ }, "dependencies": { "@antfu/install-pkg": "1.1.0", + "@inquirer/core": "11.1.8", "@inquirer/prompts": "8.4.1", "@types/datadog-metrics": "0.6.1", "async-retry": "1.3.3", diff --git a/packages/base/src/helpers/inquirer.ts b/packages/base/src/helpers/inquirer.ts index ddeb4507a8..3eb39d4bdb 100644 --- a/packages/base/src/helpers/inquirer.ts +++ b/packages/base/src/helpers/inquirer.ts @@ -51,10 +51,60 @@ export type InquirerPrompts = { select: (config: SelectConfig) => Promise } +type InquirerPromptStatus = 'idle' | 'loading' | 'done' + +type KeypressEvent = { + ctrl: boolean + name: string + shift: boolean +} + +type Readline = { + clearLine: (dir: number) => void + line: string + write: (input: string) => void +} + +type InquirerTheme = { + style: { + answer: (text: string) => string + error: (text: string) => string + highlight: (text: string) => string + message: (text: string, status: InquirerPromptStatus) => string + } +} + +type PaginationConfig = { + active: number + items: readonly Item[] + loop?: boolean + pageSize: number + renderItem: (options: {index: number; isActive: boolean; item: Item}) => string +} + +type PromptView = (config: Config, done: (value: Value) => void) => string | [string, string | undefined] + +type InquirerCore = { + createPrompt: (view: PromptView) => (config: Config) => Promise + isDownKey: (key: KeypressEvent) => boolean + isEnterKey: (key: KeypressEvent) => boolean + isSpaceKey: (key: KeypressEvent) => boolean + isUpKey: (key: KeypressEvent) => boolean + makeTheme: (defaultTheme: Theme, customTheme?: unknown) => Theme & InquirerTheme + useEffect: (effect: (readline: Readline) => void | (() => void), dependencies?: readonly unknown[]) => void + useKeypress: (handler: (key: KeypressEvent, readline: Readline) => void | Promise) => void + useMemo: (value: () => Value, dependencies: readonly unknown[]) => Value + usePagination: (config: PaginationConfig) => string + usePrefix: (options: {status: InquirerPromptStatus; theme?: unknown}) => string + useState: (value: Value) => [Value, (value: Value) => void] +} + // eslint-disable-next-line @typescript-eslint/no-implied-eval -- TypeScript rewrites plain `import()` to `require()` in our CommonJS emit. -const importInquirerPrompts = new Function('specifier', 'return import(specifier)') as ( - specifier: string -) => Promise +const importInquirerModule = new Function('specifier', 'return import(specifier)') as (specifier: string) => Promise< + unknown +> // Preserve a real runtime dynamic import so Node can load the ESM-only prompt package from CommonJS output. -export const loadPrompts = () => importInquirerPrompts('@inquirer/prompts') +export const loadPrompts = () => importInquirerModule('@inquirer/prompts') as Promise + +export const loadCore = () => importInquirerModule('@inquirer/core') as Promise diff --git a/packages/plugin-lambda/src/__tests__/prompt.test.ts b/packages/plugin-lambda/src/__tests__/prompt.test.ts index 2e443ac63b..93db8b9f8e 100644 --- a/packages/plugin-lambda/src/__tests__/prompt.test.ts +++ b/packages/plugin-lambda/src/__tests__/prompt.test.ts @@ -1,8 +1,9 @@ jest.mock('@datadog/datadog-ci-base/helpers/inquirer', () => ({ + loadCore: jest.fn(), loadPrompts: jest.fn(), })) import {MOCK_DATADOG_API_KEY} from '@datadog/datadog-ci-base/helpers/__tests__/testing-tools' -import {loadPrompts} from '@datadog/datadog-ci-base/helpers/inquirer' +import {loadCore, loadPrompts} from '@datadog/datadog-ci-base/helpers/inquirer' import {CI_API_KEY_ENV_VAR, CI_SITE_ENV_VAR} from '@datadog/datadog-ci-base/helpers/serverless/constants' import { @@ -26,12 +27,17 @@ import {mockAwsAccessKeyId, mockAwsSecretAccessKey} from './fixtures' describe('prompt', () => { const mockCheckbox = jest.fn() const mockConfirm = jest.fn() + const mockCreatePrompt = jest.fn() const mockInput = jest.fn() const mockPassword = jest.fn() + const mockSearchableCheckboxPrompt = jest.fn() const mockSelect = jest.fn() beforeEach(() => { jest.resetAllMocks() + ;(loadCore as jest.Mock).mockResolvedValue({ + createPrompt: mockCreatePrompt.mockReturnValue(mockSearchableCheckboxPrompt), + }) ;(loadPrompts as jest.Mock).mockResolvedValue({ checkbox: mockCheckbox, confirm: mockConfirm, @@ -114,13 +120,12 @@ describe('prompt', () => { test('returns question with the provided function names being its choices', () => { const functionNames = ['my-func', 'my-func-2', 'my-third-func'] const question = functionSelectionQuestion(functionNames) - expect(question.choices).toEqual( - functionNames.map((functionName) => ({ - checked: false, - name: functionName, - value: functionName, - })) + expect(question.choices).toEqual(functionNames) + expect(question.message).toBe( + 'Select the functions to modify (Press to select, p.s. start typing the name instead of manually scrolling)' ) + expect(question.validate(['my-func'])).toBe(true) + expect(question.validate([])).toBe('You must choose at least one function.') }) }) @@ -211,16 +216,23 @@ describe('prompt', () => { const selectedFunctions = ['my-func', 'my-func-2', 'my-third-func'] test('returns the selected functions', async () => { - mockInput.mockResolvedValue('') - mockCheckbox.mockResolvedValue(selectedFunctions) - mockConfirm.mockResolvedValue(false) + mockSearchableCheckboxPrompt.mockResolvedValue(selectedFunctions) const functions = await requestFunctionSelection(selectedFunctions) expect(functions).toEqual(selectedFunctions) + expect(mockCreatePrompt).toHaveBeenCalledTimes(1) + expect(mockSearchableCheckboxPrompt).toHaveBeenCalledWith( + expect.objectContaining({ + choices: selectedFunctions, + message: + 'Select the functions to modify (Press to select, p.s. start typing the name instead of manually scrolling)', + pageSize: 10, + }) + ) }) test('throws error when something unexpected happens while prompting', async () => { - mockInput.mockRejectedValue(new Error('Unexpected error')) + mockSearchableCheckboxPrompt.mockRejectedValue(new Error('Unexpected error')) let error try { await requestFunctionSelection(selectedFunctions) diff --git a/packages/plugin-lambda/src/prompt.ts b/packages/plugin-lambda/src/prompt.ts index e34c4a46d4..06aa36ef90 100644 --- a/packages/plugin-lambda/src/prompt.ts +++ b/packages/plugin-lambda/src/prompt.ts @@ -1,4 +1,5 @@ -import type {CheckboxConfig, InputConfig, PasswordConfig, SelectConfig} from '@datadog/datadog-ci-base/helpers/inquirer' +import type {InputConfig, PasswordConfig, SelectConfig} from '@datadog/datadog-ci-base/helpers/inquirer' +import type {SearchableCheckboxConfig} from './searchable-checkbox-prompt' import {DATADOG_SITES} from '@datadog/datadog-ci-base/constants' import {loadPrompts} from '@datadog/datadog-ci-base/helpers/inquirer' @@ -11,7 +12,6 @@ import { } from '@datadog/datadog-ci-base/helpers/serverless/constants' import {isValidDatadogSite} from '@datadog/datadog-ci-base/helpers/validation' import chalk from 'chalk' -import {filter} from 'fuzzy' import { AWS_ACCESS_KEY_ID_ENV_VAR, @@ -28,6 +28,7 @@ import { DATADOG_API_KEY_REG_EXP, } from './constants' import {isMissingAnyDatadogApiKeyEnvVar, sentenceMatchesRegEx} from './functions/commons' +import {loadSearchableCheckboxPrompt} from './searchable-checkbox-prompt' type DatadogApiKeyType = { envVar: string @@ -168,37 +169,11 @@ export const datadogEnvVarsQuestions = (datadogApiKeyType: DatadogApiKeyType): I }, }) -const getFilteredFunctionNames = (functionNames: string[], searchTerm?: string) => { - if (!searchTerm) { - return functionNames - } - - return filter(searchTerm, functionNames).map((element) => element.original) -} - -const functionSearchQuestion = (functionNames: string[]): InputConfig => ({ - default: '', - message: 'Filter functions to modify (press Enter to show all functions):', - validate: (value) => { - if (getFilteredFunctionNames(functionNames, value).length < 1) { - return 'No functions matched that filter. Try another search term.' - } - - return true - }, -}) - -export const functionSelectionQuestion = ( - functionNames: string[], - selectedFunctions: string[] = [] -): CheckboxConfig => ({ - choices: functionNames.map((functionName) => ({ - checked: selectedFunctions.includes(functionName), - name: functionName, - value: functionName, - })), +export const functionSelectionQuestion = (functionNames: string[]): SearchableCheckboxConfig => ({ + choices: functionNames, + message: + 'Select the functions to modify (Press to select, p.s. start typing the name instead of manually scrolling)', pageSize: 10, - message: 'Select the functions to modify (Press to select and to continue)', validate: (selectedFunctionNames) => { if (selectedFunctionNames.length < 1) { return 'You must choose at least one function.' @@ -274,35 +249,12 @@ export const requestEnvServiceVersion = async (): Promise => { export const requestFunctionSelection = async (functionNames: string[]): Promise => { try { - const {checkbox, confirm, input} = await loadPrompts() - const selectedFunctions = new Set() - let continueFiltering = true - - while (continueFiltering) { - const searchTerm = await input(functionSearchQuestion(functionNames)) - const filteredFunctionNames = getFilteredFunctionNames(functionNames, searchTerm) - const filteredSelection = await checkbox(functionSelectionQuestion(filteredFunctionNames, [...selectedFunctions])) - - for (const functionName of filteredFunctionNames) { - selectedFunctions.delete(functionName) - } - - for (const functionName of filteredSelection) { - selectedFunctions.add(functionName) - } - - continueFiltering = await confirm({ - default: false, - message: 'Would you like to apply another filter to add or remove more functions?', - }) - } + const searchableCheckboxPrompt = await loadSearchableCheckboxPrompt() - return [...selectedFunctions] + return await searchableCheckboxPrompt(functionSelectionQuestion(functionNames)) } catch (e) { - if (e instanceof Error) { - throw Error(`Couldn't receive selected functions. ${e.message}`) - } + const message = e instanceof Error ? e.message : String(e) - throw e + throw Error(`Couldn't receive selected functions. ${message}`) } } diff --git a/packages/plugin-lambda/src/searchable-checkbox-prompt.ts b/packages/plugin-lambda/src/searchable-checkbox-prompt.ts new file mode 100644 index 0000000000..f0d69b5578 --- /dev/null +++ b/packages/plugin-lambda/src/searchable-checkbox-prompt.ts @@ -0,0 +1,189 @@ +import {loadCore} from '@datadog/datadog-ci-base/helpers/inquirer' +import chalk from 'chalk' +import {filter} from 'fuzzy' + +const CURSOR_HIDE = '\u001B[?25l' + +export type SearchableCheckboxConfig = { + choices: string[] + message: string + pageSize: number + validate: (selectedFunctionNames: readonly string[]) => boolean | string | Promise +} + +type SearchableChoice = { + checked: boolean + name: string + short: string + value: string +} + +type SearchableCheckboxPrompt = (config: SearchableCheckboxConfig) => Promise + +let searchableCheckboxPromptPromise: Promise | undefined + +const getFilteredFunctionNames = (functionNames: string[], searchTerm?: string) => { + if (!searchTerm) { + return functionNames + } + + return filter(searchTerm, functionNames).map((element) => element.original) +} + +const createSearchableCheckboxPrompt = async (): Promise => { + const { + createPrompt, + isDownKey, + isEnterKey, + isSpaceKey, + isUpKey, + makeTheme, + useEffect, + useKeypress, + useMemo, + usePagination, + usePrefix, + useState, + } = await loadCore() + + return createPrompt((config, done) => { + const theme = makeTheme( + { + icon: { + checked: chalk.green('◉'), + cursor: '❯', + unchecked: '◯', + }, + style: { + answer: (text: string) => chalk.cyan(text), + empty: (text: string) => chalk.dim(text), + searchTerm: (text: string) => chalk.cyan(text), + }, + }, + undefined + ) + const [status, setStatus] = useState<'idle' | 'done'>('idle') + const prefix = usePrefix({status, theme}) + const [searchTerm, setSearchTerm] = useState('') + const [errorMessage, setErrorMessage] = useState(undefined) + const [items, setItems] = useState( + config.choices.map((functionName) => ({ + checked: false, + name: functionName, + short: functionName, + value: functionName, + })) + ) + const selectedItems = useMemo(() => items.filter((item) => item.checked), [items]) + const filteredItems = useMemo(() => { + if (!searchTerm) { + return items + } + + const filteredNamesSet = new Set( + getFilteredFunctionNames( + items.map((item) => item.name), + searchTerm + ) + ) + + return items.filter((item) => filteredNamesSet.has(item.name)) + }, [items, searchTerm]) + const [active, setActive] = useState(0) + + useEffect(() => { + if (filteredItems.length === 0) { + setActive(0) + + return + } + + if (active >= filteredItems.length) { + setActive(0) + } + }, [active, filteredItems.length]) + + useKeypress(async (key, readline) => { + if (isEnterKey(key)) { + const selectedFunctions = selectedItems.map((item) => item.value) + const isValid = await config.validate(selectedFunctions) + + if (isValid === true) { + setStatus('done') + done(selectedFunctions) + + return + } + + readline.write(searchTerm) + setErrorMessage(typeof isValid === 'string' ? isValid : 'You must choose at least one function.') + + return + } + + if (filteredItems.length > 0 && (isUpKey(key) || isDownKey(key))) { + readline.clearLine(0) + readline.write(searchTerm) + setErrorMessage(undefined) + const offset = isUpKey(key) ? -1 : 1 + setActive((active + offset + filteredItems.length) % filteredItems.length) + + return + } + + if (filteredItems.length > 0 && isSpaceKey(key)) { + readline.clearLine(0) + readline.write(searchTerm) + setErrorMessage(undefined) + const activeItem = filteredItems[active] + + if (!activeItem) { + return + } + + setItems(items.map((item) => (item.value === activeItem.value ? {...item, checked: !item.checked} : item))) + + return + } + + setSearchTerm(readline.line) + setErrorMessage(undefined) + }) + + const message = theme.style.message(config.message, status) + const search = searchTerm ? ` ${theme.style.searchTerm(searchTerm)}` : '' + + if (status === 'done') { + return [prefix, message, theme.style.answer(selectedItems.map((item) => item.short).join(', '))] + .filter(Boolean) + .join(' ') + } + + const page = + filteredItems.length === 0 + ? theme.style.empty('No results found') + : usePagination({ + active, + items: filteredItems, + loop: false, + pageSize: config.pageSize, + renderItem: ({isActive, item}) => { + const cursor = isActive ? theme.icon.cursor : ' ' + const checkbox = item.checked ? theme.icon.checked : theme.icon.unchecked + const line = `${cursor}${checkbox} ${item.name}` + + return isActive ? theme.style.highlight(line) : line + }, + }) + + const body = [page, errorMessage ? theme.style.error(errorMessage) : undefined].filter(Boolean).join('\n') + + return [[[prefix, message, search].filter(Boolean).join(' ').trimEnd(), CURSOR_HIDE].join(''), body || undefined] + }) +} + +export const loadSearchableCheckboxPrompt = async () => { + searchableCheckboxPromptPromise ??= createSearchableCheckboxPrompt() + + return searchableCheckboxPromptPromise +} diff --git a/yarn.lock b/yarn.lock index 7d8f3cc49e..576b069937 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1955,6 +1955,7 @@ __metadata: resolution: "@datadog/datadog-ci-base@workspace:packages/base" dependencies: "@antfu/install-pkg": "npm:1.1.0" + "@inquirer/core": "npm:11.1.8" "@inquirer/prompts": "npm:8.4.1" "@types/async-retry": "npm:1.4.8" "@types/datadog-metrics": "npm:0.6.1" @@ -2882,7 +2883,7 @@ __metadata: languageName: node linkType: hard -"@inquirer/core@npm:^11.1.8": +"@inquirer/core@npm:11.1.8, @inquirer/core@npm:^11.1.8": version: 11.1.8 resolution: "@inquirer/core@npm:11.1.8" dependencies: From 60a6ea673e93490a6f069ce518089fb5e0ae01ef Mon Sep 17 00:00:00 2001 From: corentin Date: Fri, 10 Apr 2026 18:41:56 +0200 Subject: [PATCH 4/4] Fix CI lint and license metadata for inquirer core --- LICENSE-3rdparty.csv | 1 + packages/base/src/helpers/inquirer.ts | 6 +++--- packages/plugin-lambda/src/prompt.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index cf0ae17311..3c37b5460d 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -85,6 +85,7 @@ jszip,import,MIT,Copyright (c) 2009-2016 Stuart Knightley and other contributors @google-cloud/run,import,Apache-2.0,Copyright (c) 2023 Google LLC and other contributors google-auth-library,import,Apache-2.0,Copyright (c) 2023 Google LLC and other contributors @google-cloud/logging,import,Apache-2.0,Copyright (c) 2023 Google LLC and other contributors +@inquirer/core,import,MIT,Copyright (c) 2025 Simon Boudrias @inquirer/prompts,import,MIT,Copyright (c) 2025 Simon Boudrias jest-diff,import,MIT,"Copyright (c) Meta Platforms, Inc. and other contributors" tsx,dev,MIT,Copyright (c) Hiroki Osame diff --git a/packages/base/src/helpers/inquirer.ts b/packages/base/src/helpers/inquirer.ts index 3eb39d4bdb..b5f2be5694 100644 --- a/packages/base/src/helpers/inquirer.ts +++ b/packages/base/src/helpers/inquirer.ts @@ -100,9 +100,9 @@ type InquirerCore = { } // eslint-disable-next-line @typescript-eslint/no-implied-eval -- TypeScript rewrites plain `import()` to `require()` in our CommonJS emit. -const importInquirerModule = new Function('specifier', 'return import(specifier)') as (specifier: string) => Promise< - unknown -> +const importInquirerModule = new Function('specifier', 'return import(specifier)') as ( + specifier: string +) => Promise // Preserve a real runtime dynamic import so Node can load the ESM-only prompt package from CommonJS output. export const loadPrompts = () => importInquirerModule('@inquirer/prompts') as Promise diff --git a/packages/plugin-lambda/src/prompt.ts b/packages/plugin-lambda/src/prompt.ts index 06aa36ef90..0ecb94c2e4 100644 --- a/packages/plugin-lambda/src/prompt.ts +++ b/packages/plugin-lambda/src/prompt.ts @@ -1,5 +1,5 @@ -import type {InputConfig, PasswordConfig, SelectConfig} from '@datadog/datadog-ci-base/helpers/inquirer' import type {SearchableCheckboxConfig} from './searchable-checkbox-prompt' +import type {InputConfig, PasswordConfig, SelectConfig} from '@datadog/datadog-ci-base/helpers/inquirer' import {DATADOG_SITES} from '@datadog/datadog-ci-base/constants' import {loadPrompts} from '@datadog/datadog-ci-base/helpers/inquirer'