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,