From 5bf7768ca4d0969fa8f87157928ababc2741daa4 Mon Sep 17 00:00:00 2001 From: Brandon Cheng Date: Tue, 10 Feb 2026 20:39:23 -0500 Subject: [PATCH] feat: skip confirm modules purge prompt if `--yes` is passed (#10383) * feat: add --yes command line option * feat: skip confirm modules purge prompt if --yes is passed * refactor: factor out `ExecPnpmSyncOpts` * test: add end-to-end test for --yes flag --- .changeset/tasty-eyes-retire.md | 8 +++++ cli/common-cli-options-help/src/index.ts | 5 +++ config/config/src/Config.ts | 1 + config/config/src/configFileKey.ts | 1 + config/config/src/index.ts | 7 ++++ config/config/src/types.ts | 1 + config/config/test/index.ts | 18 +++++++++++ .../core/src/install/extendInstallOptions.ts | 4 ++- pnpm/src/cmd/index.ts | 1 + pnpm/src/shorthands.ts | 1 + pnpm/test/install/yesFlag.ts | 32 +++++++++++++++++++ pnpm/test/utils/execPnpm.ts | 18 ++++++----- 12 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 .changeset/tasty-eyes-retire.md create mode 100644 pnpm/test/install/yesFlag.ts diff --git a/.changeset/tasty-eyes-retire.md b/.changeset/tasty-eyes-retire.md new file mode 100644 index 0000000000..89c9dfc997 --- /dev/null +++ b/.changeset/tasty-eyes-retire.md @@ -0,0 +1,8 @@ +--- +"@pnpm/common-cli-options-help": minor +"@pnpm/core": minor +"@pnpm/config": minor +"pnpm": minor +--- + +A new `--yes` flag can be passed to pnpm to automatically confirm prompts. This is useful when running pnpm in non-interactive script. diff --git a/cli/common-cli-options-help/src/index.ts b/cli/common-cli-options-help/src/index.ts index f78ae6bfcc..6a78e571fa 100644 --- a/cli/common-cli-options-help/src/index.ts +++ b/cli/common-cli-options-help/src/index.ts @@ -35,6 +35,11 @@ export const UNIVERSAL_OPTIONS = [ name: '--help', shortAlias: '-h', }, + { + description: 'Automatically answer yes to prompts and run non-interactively. Will abort if an undesirable situation occurs and user input is strictly necessary.', + name: '--yes', + shortAlias: '-y', + }, { description: `Change to directory (default: ${process.cwd()})`, name: '--dir ', diff --git a/config/config/src/Config.ts b/config/config/src/Config.ts index 5827ce531a..04d9a980e3 100644 --- a/config/config/src/Config.ts +++ b/config/config/src/Config.ts @@ -27,6 +27,7 @@ export interface Config extends AuthInfo, OptionsFromRootManifest { allProjectsGraph?: ProjectsGraph allowNew: boolean + autoConfirmAllPrompts?: boolean autoInstallPeers?: boolean bail: boolean color: 'always' | 'auto' | 'never' diff --git a/config/config/src/configFileKey.ts b/config/config/src/configFileKey.ts index e4f455cc45..34bdf7c2a8 100644 --- a/config/config/src/configFileKey.ts +++ b/config/config/src/configFileKey.ts @@ -154,6 +154,7 @@ export const excludedPnpmKeys = [ 'libc', 'os', 'audit-level', + 'yes', ] as const satisfies ReadonlyArray> export type ExcludedPnpmKey = typeof excludedPnpmKeys[number] diff --git a/config/config/src/index.ts b/config/config/src/index.ts index c34c94405b..9f0722cfc3 100644 --- a/config/config/src/index.ts +++ b/config/config/src/index.ts @@ -612,6 +612,13 @@ export async function getConfig (opts: { pnpmConfig.enableGlobalVirtualStore = false } + // The yes option is only meant to be a CLI option. Remove it from the + // returned pnpm config. + delete (pnpmConfig as { yes?: boolean }).yes + if (cliOptions.yes) { + pnpmConfig.autoConfirmAllPrompts = true + } + transformPathKeys(pnpmConfig, os.homedir()) return { config: pnpmConfig, warnings } diff --git a/config/config/src/types.ts b/config/config/src/types.ts index 961e8e7274..9c2073e001 100644 --- a/config/config/src/types.ts +++ b/config/config/src/types.ts @@ -129,6 +129,7 @@ export const pnpmTypes = { 'workspace-concurrency': Number, 'workspace-packages': [String, Array], 'workspace-root': Boolean, + yes: Boolean, 'test-pattern': [String, Array], 'changed-files-ignore-pattern': [String, Array], 'embed-readme': Boolean, diff --git a/config/config/test/index.ts b/config/config/test/index.ts index 40aeebd610..01aef24e25 100644 --- a/config/config/test/index.ts +++ b/config/config/test/index.ts @@ -1404,6 +1404,24 @@ test('no warning when directory does not contain PATH delimiter character', asyn } }) +test.each([ + [undefined, undefined], + [false, undefined], + [true, true], +])('sets autoConfirmAllPrompts when CLI is passed --yes=%s', async (cliValue?: boolean, expectedValue?: boolean) => { + const { config } = await getConfig({ + cliOptions: { + 'yes': cliValue, + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + }) + + expect(config.autoConfirmAllPrompts).toBe(expectedValue) +}) + describe('global config.yaml', () => { let XDG_CONFIG_HOME: string | undefined diff --git a/pkg-manager/core/src/install/extendInstallOptions.ts b/pkg-manager/core/src/install/extendInstallOptions.ts index 0e999f22dc..285f18077a 100644 --- a/pkg-manager/core/src/install/extendInstallOptions.ts +++ b/pkg-manager/core/src/install/extendInstallOptions.ts @@ -25,6 +25,7 @@ import { pnpmPkgJson } from '../pnpmPkgJson.js' import { type ReporterFunction } from '../types.js' export interface StrictInstallOptions { + autoConfirmAllPrompts: boolean autoInstallPeers: boolean autoInstallPeersFromHighestMatch: boolean catalogs: Catalogs @@ -188,11 +189,12 @@ const defaults = (opts: InstallOptions): StrictInstallOptions => { return { allowedDeprecatedVersions: {}, allowUnusedPatches: false, + autoConfirmAllPrompts: opts.autoConfirmAllPrompts ?? false, autoInstallPeers: true, autoInstallPeersFromHighestMatch: false, catalogs: {}, childConcurrency: 5, - confirmModulesPurge: !opts.force, + confirmModulesPurge: !(opts.autoConfirmAllPrompts || opts.force), depth: 0, dedupeInjectedDeps: true, enableGlobalVirtualStore: false, diff --git a/pnpm/src/cmd/index.ts b/pnpm/src/cmd/index.ts index 36daef66ec..41665dcea3 100644 --- a/pnpm/src/cmd/index.ts +++ b/pnpm/src/cmd/index.ts @@ -54,6 +54,7 @@ export const GLOBAL_OPTIONS = pick([ 'ignore-workspace', 'workspace-packages', 'workspace-root', + 'yes', 'include-workspace-root', 'fail-if-no-match', ], allTypes) diff --git a/pnpm/src/shorthands.ts b/pnpm/src/shorthands.ts index 67d396787b..b6f1a00be3 100644 --- a/pnpm/src/shorthands.ts +++ b/pnpm/src/shorthands.ts @@ -31,4 +31,5 @@ export const shorthands: Record = { w: '--workspace-root', i: '--interactive', F: '--filter', + y: '--yes', } diff --git a/pnpm/test/install/yesFlag.ts b/pnpm/test/install/yesFlag.ts new file mode 100644 index 0000000000..3df45618e3 --- /dev/null +++ b/pnpm/test/install/yesFlag.ts @@ -0,0 +1,32 @@ +import fs from 'fs' +import path from 'path' +import { prepare } from '@pnpm/prepare' +import { type PackageManifest } from '@pnpm/types' +import { loadJsonFileSync } from 'load-json-file' +import { execPnpmSync } from '../utils/index.js' +import { type ExecPnpmSyncOpts } from '../utils/execPnpm.js' + +const basicPackageManifest = loadJsonFileSync(path.join(import.meta.dirname, '../utils/simple-package.json')) + +describe('pnpm install --yes', () => { + beforeEach(() => { + prepare(basicPackageManifest) + execPnpmSync(['install']) + + // Write an incompatible layoutVersion to force a module purge prompt. + fs.writeFileSync('node_modules/.modules.yaml', 'layoutVersion: 1') + }) + + const execPnpmOpts: ExecPnpmSyncOpts = { + expectSuccess: true, + env: { CI: 'false' }, + } + + test('prompts without --yes flag', () => { + expect(() => execPnpmSync(['install'], execPnpmOpts)).toThrow('Aborted removal of modules directory due to no TTY') + }) + + test('skips prompt when --yes is passed', () => { + expect(() => execPnpmSync(['install', '--yes'], execPnpmOpts)).not.toThrow() + }) +}) diff --git a/pnpm/test/utils/execPnpm.ts b/pnpm/test/utils/execPnpm.ts index 6d2cc5c61e..2934250243 100644 --- a/pnpm/test/utils/execPnpm.ts +++ b/pnpm/test/utils/execPnpm.ts @@ -93,16 +93,18 @@ export interface ChildProcess { stderr: { toString: () => string } } +export interface ExecPnpmSyncOpts { + cwd?: string + env?: Record + expectSuccess?: boolean // similar to expect(status).toBe(0), but also prints error messages, which makes it easier to debug failed tests + stdio?: StdioOptions + storeDir?: string + timeout?: number +} + export function execPnpmSync ( args: string[], - opts?: { - cwd?: string - env?: Record - expectSuccess?: boolean // similar to expect(status).toBe(0), but also prints error messages, which makes it easier to debug failed tests - stdio?: StdioOptions - storeDir?: string - timeout?: number - } + opts?: ExecPnpmSyncOpts ): ChildProcess { const execResult = crossSpawn.sync(process.execPath, [pnpmBinLocation, ...args], { cwd: opts?.cwd,