diff --git a/.changeset/fix-prompt-no-tty.md b/.changeset/fix-prompt-no-tty.md new file mode 100644 index 0000000000..55acb97681 --- /dev/null +++ b/.changeset/fix-prompt-no-tty.md @@ -0,0 +1,14 @@ +--- +"@pnpm/plugin-commands-script-runners": patch +"pnpm": patch +--- + +Handle non-TTY environments correctly when using `verifyDepsBeforeRun: prompt`. + +Previously, in non-interactive environments like CI, using `verifyDepsBeforeRun: prompt` would silently exit with code 0 even when node_modules were out of sync. This could cause tests to pass even when they should fail. + +Now, pnpm will throw an error in non-TTY environments, alerting users that they need to run `pnpm install` first. + +Also handles Ctrl+C gracefully during the prompt - exits cleanly without showing a stack trace. + +Fixes #10889, #10888 diff --git a/exec/plugin-commands-script-runners/src/runDepsStatusCheck.ts b/exec/plugin-commands-script-runners/src/runDepsStatusCheck.ts index 243f379f0b..68a3721676 100644 --- a/exec/plugin-commands-script-runners/src/runDepsStatusCheck.ts +++ b/exec/plugin-commands-script-runners/src/runDepsStatusCheck.ts @@ -27,14 +27,28 @@ export async function runDepsStatusCheck (opts: RunDepsStatusCheckOptions): Prom install() break case 'prompt': { - const confirmed = await enquirer.prompt<{ runInstall: boolean }>({ - type: 'confirm', - name: 'runInstall', - message: `Your "node_modules" directory is out of sync with the "pnpm-lock.yaml" file. This can lead to issues during scripts execution. + // In non-TTY environments (like CI), we can't prompt the user + // Exit with error to alert users that node_modules are out of sync + if (!process.stdin.isTTY) { + throw new PnpmError('VERIFY_DEPS_BEFORE_RUN', issue ?? 'Your node_modules are out of sync with your lockfile', { + hint: 'Run "pnpm install" before running scripts. The "verifyDepsBeforeRun: prompt" setting cannot prompt for confirmation in non-interactive environments.', + }) + } + let confirmed: { runInstall: boolean } + try { + confirmed = await enquirer.prompt<{ runInstall: boolean }>({ + type: 'confirm', + name: 'runInstall', + message: `Your "node_modules" directory is out of sync with the "pnpm-lock.yaml" file. This can lead to issues during scripts execution. Would you like to run "pnpm ${command.join(' ')}" to update your "node_modules"?`, - initial: true, - }) + initial: true, + }) + } catch { + // User cancelled the prompt (e.g. Ctrl+C) — exit immediately + // so the caller doesn't proceed to run the script. + process.exit(1) + } if (confirmed.runInstall) { install() } diff --git a/exec/plugin-commands-script-runners/test/verifyDepsBeforeRun.ts b/exec/plugin-commands-script-runners/test/verifyDepsBeforeRun.ts index 014f3b9247..1b3e153e7a 100644 --- a/exec/plugin-commands-script-runners/test/verifyDepsBeforeRun.ts +++ b/exec/plugin-commands-script-runners/test/verifyDepsBeforeRun.ts @@ -88,7 +88,14 @@ test('prompt the user if verifyDepsBeforeRun is set to prompt', async () => { // Mock the user confirming the prompt jest.mocked(enquirer.prompt).mockResolvedValue({ runInstall: true }) - await runTest('prompt') + const originalIsTTY = process.stdin.isTTY + Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true }) + + try { + await runTest('prompt') + } finally { + Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true }) + } expect(enquirer.prompt).toHaveBeenCalledWith({ type: 'confirm', @@ -101,3 +108,24 @@ test('prompt the user if verifyDepsBeforeRun is set to prompt', async () => { expect(fs.existsSync(path.resolve('node_modules'))).toBeTruthy() }) + +test('throw an error if verifyDepsBeforeRun is set to prompt in non-TTY environment', async () => { + prepare(rootProjectManifest) + + // Mock non-TTY environment + const originalIsTTY = process.stdin.isTTY + Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true }) + + let err!: Error + try { + await runTest('prompt') + } catch (_err) { + err = _err as Error + } finally { + // Restore original value + Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true }) + } + + expect(err.message).toContain('Cannot check whether dependencies are outdated') + expect(fs.existsSync(path.resolve('node_modules'))).toBeFalsy() +})