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
This commit is contained in:
Brandon Cheng
2026-02-10 20:39:23 -05:00
committed by GitHub
parent fa5ff08473
commit 5bf7768ca4
12 changed files with 88 additions and 9 deletions

View File

@@ -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.

View File

@@ -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 <dir> (default: ${process.cwd()})`,
name: '--dir <dir>',

View File

@@ -27,6 +27,7 @@ export interface Config extends AuthInfo, OptionsFromRootManifest {
allProjectsGraph?: ProjectsGraph
allowNew: boolean
autoConfirmAllPrompts?: boolean
autoInstallPeers?: boolean
bail: boolean
color: 'always' | 'auto' | 'never'

View File

@@ -154,6 +154,7 @@ export const excludedPnpmKeys = [
'libc',
'os',
'audit-level',
'yes',
] as const satisfies ReadonlyArray<Exclude<PnpmKey, PnpmConfigFileKey>>
export type ExcludedPnpmKey = typeof excludedPnpmKeys[number]

View File

@@ -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 }

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -54,6 +54,7 @@ export const GLOBAL_OPTIONS = pick([
'ignore-workspace',
'workspace-packages',
'workspace-root',
'yes',
'include-workspace-root',
'fail-if-no-match',
], allTypes)

View File

@@ -31,4 +31,5 @@ export const shorthands: Record<string, string> = {
w: '--workspace-root',
i: '--interactive',
F: '--filter',
y: '--yes',
}

View File

@@ -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<PackageManifest>(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()
})
})

View File

@@ -93,16 +93,18 @@ export interface ChildProcess {
stderr: { toString: () => string }
}
export interface ExecPnpmSyncOpts {
cwd?: string
env?: Record<string, string>
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<string, string>
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,