From 052e2e8683b6fa64f166e8e22d91bd17dd7f7a47 Mon Sep 17 00:00:00 2001 From: Seto Elkahfi <1797197+setoelkahfi@users.noreply.github.com> Date: Wed, 30 Sep 2026 07:27:50 +0200 Subject: [PATCH 1/2] Let pnpm recreate node_modules during Next.js deploy The install step captures pnpm's output, so pnpm has no TTY. When it needs to recreate node_modules (for example after a different pnpm major version installed it), it can't ask for confirmation and aborts with ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY. Pass --config.confirm-modules-purge=false to pnpm so it purges without asking. npm and yarn are left alone. Co-Authored-By: siGit Code --- .../cloud-deploy/process_deploy_nextjs_ssr.rs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/cli/src/cloud-deploy/process_deploy_nextjs_ssr.rs b/crates/cli/src/cloud-deploy/process_deploy_nextjs_ssr.rs index a0313bf..e4a1b61 100644 --- a/crates/cli/src/cloud-deploy/process_deploy_nextjs_ssr.rs +++ b/crates/cli/src/cloud-deploy/process_deploy_nextjs_ssr.rs @@ -215,16 +215,20 @@ pub async fn process_deploy_nextjs_ssr(env: Environment, config: Config) -> Resu // Capture stdout/stderr so pnpm's output does not interleave with the // spinner animation. On failure the captured output is printed for the user. - let install_output = Command::new(package_manager) - .args(["install", "--ignore-scripts"]) - .current_dir(source) - .output() - .map_err(|e| { - anyhow!(fail_message(&format!( - "Failed to spawn '{} install': {}", - package_manager, e - ))) - })?; + let mut install_command = Command::new(package_manager); + install_command.args(["install", "--ignore-scripts"]); + // With stdio captured pnpm has no TTY, so when it needs to recreate + // node_modules (store or linker change) it aborts with + // ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY instead of prompting. + if package_manager == "pnpm" { + install_command.arg("--config.confirm-modules-purge=false"); + } + let install_output = install_command.current_dir(source).output().map_err(|e| { + anyhow!(fail_message(&format!( + "Failed to spawn '{} install': {}", + package_manager, e + ))) + })?; if !install_output.status.success() { install_spinner.stop_and_persist(&fail_symbol(), fail_message("Install failed.")); From 761660c3403e4312eb37f04eab4ab9a02237d542 Mon Sep 17 00:00:00 2001 From: Seto Elkahfi <1797197+setoelkahfi@users.noreply.github.com> Date: Wed, 30 Sep 2026 07:32:49 +0200 Subject: [PATCH 2/2] Match pnpm by file stem and note why CI=true is not used Co-Authored-By: siGit Code --- crates/cli/src/cloud-deploy/process_deploy_nextjs_ssr.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/cli/src/cloud-deploy/process_deploy_nextjs_ssr.rs b/crates/cli/src/cloud-deploy/process_deploy_nextjs_ssr.rs index e4a1b61..2e9e30f 100644 --- a/crates/cli/src/cloud-deploy/process_deploy_nextjs_ssr.rs +++ b/crates/cli/src/cloud-deploy/process_deploy_nextjs_ssr.rs @@ -220,7 +220,12 @@ pub async fn process_deploy_nextjs_ssr(env: Environment, config: Config) -> Resu // With stdio captured pnpm has no TTY, so when it needs to recreate // node_modules (store or linker change) it aborts with // ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY instead of prompting. - if package_manager == "pnpm" { + // CI=true would also skip the prompt, but it turns on --frozen-lockfile. + // Matching on the file stem also catches full paths and Windows `pnpm.cmd`. + let is_pnpm = std::path::Path::new(package_manager) + .file_stem() + .is_some_and(|stem| stem.eq_ignore_ascii_case("pnpm")); + if is_pnpm { install_command.arg("--config.confirm-modules-purge=false"); } let install_output = install_command.current_dir(source).output().map_err(|e| {