From 447cfe083423e21afdcd9c319230b0d0338da86c Mon Sep 17 00:00:00 2001 From: Sandipsinh Dilipsinh Rathod <62684960+ssddOnTop@users.noreply.github.com> Date: Fri, 20 Feb 2026 00:34:11 -0500 Subject: [PATCH 01/10] fix: zsh commands on windows --- crates/forge_main/src/zsh/plugin.rs | 37 ++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index f56c5c6b43..c641ff84a0 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -57,7 +57,7 @@ pub fn generate_zsh_plugin() -> Result { /// Generates the ZSH theme for Forge pub fn generate_zsh_theme() -> Result { - let mut content = include_str!("../../../../shell-plugin/forge.theme.zsh").to_string(); + let mut content = include_str!("../../../../shell-plugin/forge.theme.zsh").replace('\r', ""); // Set environment variable to indicate theme is loaded (with timestamp) content.push_str("\n_FORGE_THEME_LOADED=$(date +%s)\n"); @@ -77,15 +77,26 @@ pub fn generate_zsh_theme() -> Result { /// Returns error if the script cannot be executed, if output streaming fails, /// or if the script exits with a non-zero status code fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> Result<()> { - // Execute the script in a zsh subprocess with piped output + // Normalize line endings to LF (strip carriage returns from CRLF) + let script_content = script_content.replace('\r', ""); + + // Execute zsh with stdin piped - avoids shell escaping issues with long scripts let mut child = std::process::Command::new("zsh") - .arg("-c") - .arg(script_content) + .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .context(format!("Failed to execute zsh {} script", script_name))?; + // Write script content to stdin + if let Some(mut stdin) = child.stdin.take() { + use std::io::Write; + stdin + .write_all(script_content.as_bytes()) + .context("Failed to write script to zsh stdin")?; + // stdin is automatically closed when dropped + } + // Get stdout and stderr handles let stdout = child.stdout.take().context("Failed to capture stdout")?; let stderr = child.stderr.take().context("Failed to capture stderr")?; @@ -120,12 +131,20 @@ fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> .wait() .context(format!("Failed to wait for zsh {} script", script_name))?; + // For diagnostic scripts (doctor, keyboard), non-zero exit codes are informational + // They indicate environment issues found, not script execution failures + // Only propagate the error if the script actually failed to execute if !status.success() { - anyhow::bail!( - "ZSH {} script failed with exit code: {:?}", - script_name, - status.code() - ); + // Exit codes 1-125 are typically used for reporting issues found, not execution errors + if let Some(code) = status.code() { + if code > 125 { + anyhow::bail!( + "ZSH {} script failed with exit code: {}", + script_name, + code + ); + } + } } Ok(()) From c2432ce2358fd7a8f80973e4c0781264665e58ab Mon Sep 17 00:00:00 2001 From: Sandipsinh Dilipsinh Rathod <62684960+ssddOnTop@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:24:50 -0500 Subject: [PATCH 02/10] fix(zsh): normalize line endings and improve script execution reliability --- crates/forge_main/src/zsh/plugin.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index c641ff84a0..e468f94ebd 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -75,28 +75,26 @@ pub fn generate_zsh_theme() -> Result { /// # Errors /// /// Returns error if the script cannot be executed, if output streaming fails, -/// or if the script exits with a non-zero status code +/// or if the script exits with a critical error code (>125) fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> Result<()> { // Normalize line endings to LF (strip carriage returns from CRLF) let script_content = script_content.replace('\r', ""); - // Execute zsh with stdin piped - avoids shell escaping issues with long scripts + // Write script to a temporary file - this is the most reliable approach + // Used by many production tools (kubectl, terraform, etc.) + let temp_dir = std::env::temp_dir(); + let script_path = temp_dir.join(format!("forge_{}.zsh", script_name)); + fs::write(&script_path, &script_content) + .context(format!("Failed to write {} script to temp file", script_name))?; + + // Execute the script file in a zsh subprocess with piped output let mut child = std::process::Command::new("zsh") - .stdin(Stdio::piped()) + .arg(&script_path) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .context(format!("Failed to execute zsh {} script", script_name))?; - // Write script content to stdin - if let Some(mut stdin) = child.stdin.take() { - use std::io::Write; - stdin - .write_all(script_content.as_bytes()) - .context("Failed to write script to zsh stdin")?; - // stdin is automatically closed when dropped - } - // Get stdout and stderr handles let stdout = child.stdout.take().context("Failed to capture stdout")?; let stderr = child.stderr.take().context("Failed to capture stderr")?; @@ -131,6 +129,9 @@ fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> .wait() .context(format!("Failed to wait for zsh {} script", script_name))?; + // Clean up temporary script file + let _ = fs::remove_file(&script_path); + // For diagnostic scripts (doctor, keyboard), non-zero exit codes are informational // They indicate environment issues found, not script execution failures // Only propagate the error if the script actually failed to execute From ce16c285a083210e23bf286e92446bb93be7dcf6 Mon Sep 17 00:00:00 2001 From: Sandipsinh Dilipsinh Rathod <62684960+ssddOnTop@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:49:07 -0500 Subject: [PATCH 03/10] fix(zsh): simplify script execution by removing temp file approach --- crates/forge_main/src/zsh/plugin.rs | 40 ++++++++--------------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index c583cef29f..374764701b 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -51,7 +51,7 @@ pub fn generate_zsh_plugin() -> Result { /// Generates the ZSH theme for Forge pub fn generate_zsh_theme() -> Result { - let mut content = include_str!("../../../../shell-plugin/forge.theme.zsh").replace('\r', ""); + let mut content = include_str!("../../../../shell-plugin/forge.theme.zsh").to_string(); // Set environment variable to indicate theme is loaded (with timestamp) content.push_str("\n_FORGE_THEME_LOADED=$(date +%s)\n"); @@ -69,21 +69,12 @@ pub fn generate_zsh_theme() -> Result { /// # Errors /// /// Returns error if the script cannot be executed, if output streaming fails, -/// or if the script exits with a critical error code (>125) +/// or if the script exits with a non-zero status code fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> Result<()> { - // Normalize line endings to LF (strip carriage returns from CRLF) - let script_content = script_content.replace('\r', ""); - - // Write script to a temporary file - this is the most reliable approach - // Used by many production tools (kubectl, terraform, etc.) - let temp_dir = std::env::temp_dir(); - let script_path = temp_dir.join(format!("forge_{}.zsh", script_name)); - fs::write(&script_path, &script_content) - .context(format!("Failed to write {} script to temp file", script_name))?; - - // Execute the script file in a zsh subprocess with piped output + // Execute the script in a zsh subprocess with piped output let mut child = std::process::Command::new("zsh") - .arg(&script_path) + .arg("-c") + .arg(script_content) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() @@ -123,23 +114,12 @@ fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> .wait() .context(format!("Failed to wait for zsh {} script", script_name))?; - // Clean up temporary script file - let _ = fs::remove_file(&script_path); - - // For diagnostic scripts (doctor, keyboard), non-zero exit codes are informational - // They indicate environment issues found, not script execution failures - // Only propagate the error if the script actually failed to execute if !status.success() { - // Exit codes 1-125 are typically used for reporting issues found, not execution errors - if let Some(code) = status.code() { - if code > 125 { - anyhow::bail!( - "ZSH {} script failed with exit code: {}", - script_name, - code - ); - } - } + anyhow::bail!( + "ZSH {} script failed with exit code: {:?}", + script_name, + status.code() + ); } Ok(()) From 4da972b711c45c60d1eee3747940b61cf6d571b6 Mon Sep 17 00:00:00 2001 From: Sandipsinh Dilipsinh Rathod <62684960+ssddOnTop@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:50:36 -0500 Subject: [PATCH 04/10] fix(zsh): simplify script execution by removing temp file approach --- crates/forge_main/src/zsh/plugin.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index 374764701b..bc77ea99b1 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -71,15 +71,23 @@ pub fn generate_zsh_theme() -> Result { /// Returns error if the script cannot be executed, if output streaming fails, /// or if the script exits with a non-zero status code fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> Result<()> { + let script_content = script_content.replace('\r', ""); + + // Write script to a temporary file - this is the most reliable approach + // Used by many production tools (kubectl, terraform, etc.) + let temp_dir = std::env::temp_dir(); + let script_path = temp_dir.join(format!("forge_{}.zsh", script_name)); + fs::write(&script_path, &script_content) + .context(format!("Failed to write {} script to temp file", script_name))?; + // Execute the script in a zsh subprocess with piped output let mut child = std::process::Command::new("zsh") - .arg("-c") - .arg(script_content) + .arg(&script_path) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .context(format!("Failed to execute zsh {} script", script_name))?; - + let _ = fs::remove_file(&script_path); // Get stdout and stderr handles let stdout = child.stdout.take().context("Failed to capture stdout")?; let stderr = child.stderr.take().context("Failed to capture stderr")?; From 508176986818e0ae65690b0028cd8888d9148b7d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 06:53:21 +0000 Subject: [PATCH 05/10] [autofix.ci] apply automated fixes --- crates/forge_main/src/zsh/plugin.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index bc77ea99b1..d51377d7b9 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -77,8 +77,10 @@ fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> // Used by many production tools (kubectl, terraform, etc.) let temp_dir = std::env::temp_dir(); let script_path = temp_dir.join(format!("forge_{}.zsh", script_name)); - fs::write(&script_path, &script_content) - .context(format!("Failed to write {} script to temp file", script_name))?; + fs::write(&script_path, &script_content).context(format!( + "Failed to write {} script to temp file", + script_name + ))?; // Execute the script in a zsh subprocess with piped output let mut child = std::process::Command::new("zsh") From 79cf0d7c8311004af02c635405b53d9ca7bfb6d0 Mon Sep 17 00:00:00 2001 From: Sandipsinh Dilipsinh Rathod <62684960+ssddOnTop@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:54:44 -0500 Subject: [PATCH 06/10] fix rm file --- crates/forge_main/src/zsh/plugin.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index bc77ea99b1..7ab85efa4e 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -51,7 +51,7 @@ pub fn generate_zsh_plugin() -> Result { /// Generates the ZSH theme for Forge pub fn generate_zsh_theme() -> Result { - let mut content = include_str!("../../../../shell-plugin/forge.theme.zsh").to_string(); + let mut content = include_str!("../../../../shell-plugin/forge.theme.zsh").replace('\r', ""); // Set environment variable to indicate theme is loaded (with timestamp) content.push_str("\n_FORGE_THEME_LOADED=$(date +%s)\n"); @@ -87,7 +87,7 @@ fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> .stderr(Stdio::piped()) .spawn() .context(format!("Failed to execute zsh {} script", script_name))?; - let _ = fs::remove_file(&script_path); + // Get stdout and stderr handles let stdout = child.stdout.take().context("Failed to capture stdout")?; let stderr = child.stderr.take().context("Failed to capture stderr")?; @@ -122,6 +122,12 @@ fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> .wait() .context(format!("Failed to wait for zsh {} script", script_name))?; + // Clean up temporary script file + let _ = fs::remove_file(&script_path); + + // For diagnostic scripts (doctor, keyboard), non-zero exit codes are informational + // They indicate environment issues found, not script execution failures + // Only propagate the error if the script actually failed to execute if !status.success() { anyhow::bail!( "ZSH {} script failed with exit code: {:?}", From abb42c710f88aa1518edd8547fc911451f552a47 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 06:56:42 +0000 Subject: [PATCH 07/10] [autofix.ci] apply automated fixes --- crates/forge_main/src/zsh/plugin.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index 52b7f6633f..36fb6fc8ef 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -127,9 +127,10 @@ fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> // Clean up temporary script file let _ = fs::remove_file(&script_path); - // For diagnostic scripts (doctor, keyboard), non-zero exit codes are informational - // They indicate environment issues found, not script execution failures - // Only propagate the error if the script actually failed to execute + // For diagnostic scripts (doctor, keyboard), non-zero exit codes are + // informational They indicate environment issues found, not script + // execution failures Only propagate the error if the script actually failed + // to execute if !status.success() { anyhow::bail!( "ZSH {} script failed with exit code: {:?}", From 6a9ba09950b1ceb6e24598adb85dd578fe0eb12b Mon Sep 17 00:00:00 2001 From: Sandipsinh Dilipsinh Rathod <62684960+ssddOnTop@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:07:10 -0400 Subject: [PATCH 08/10] fix(zsh): normalize CRLF in scripts and use platform-specific execution --- crates/forge_main/Cargo.toml | 2 +- crates/forge_main/src/zsh/mod.rs | 9 ++++ crates/forge_main/src/zsh/plugin.rs | 77 ++++++++++++++++++----------- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/crates/forge_main/Cargo.toml b/crates/forge_main/Cargo.toml index 27305a1526..98cebe83da 100644 --- a/crates/forge_main/Cargo.toml +++ b/crates/forge_main/Cargo.toml @@ -64,6 +64,7 @@ forge_markdown_stream.workspace = true strip-ansi-escapes.workspace = true terminal_size = "0.4" rustls.workspace = true +tempfile.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] arboard = "3.4" @@ -72,7 +73,6 @@ arboard = "3.4" tokio = { workspace = true, features = ["macros", "rt", "time", "test-util"] } insta.workspace = true pretty_assertions.workspace = true -tempfile.workspace = true serial_test = "3.4" fake = { version = "5.1.0", features = ["derive"] } forge_domain = { path = "../forge_domain" } diff --git a/crates/forge_main/src/zsh/mod.rs b/crates/forge_main/src/zsh/mod.rs index 5bf5325f67..3348473002 100644 --- a/crates/forge_main/src/zsh/mod.rs +++ b/crates/forge_main/src/zsh/mod.rs @@ -11,6 +11,15 @@ mod plugin; mod rprompt; mod style; +/// Normalizes shell script content for cross-platform compatibility. +/// +/// Strips carriage returns (`\r`) that appear when `include_str!` or +/// `include_dir!` embed files on Windows (where `git core.autocrlf=true` +/// converts LF to CRLF on checkout). Zsh cannot parse `\r` in scripts. +pub(crate) fn normalize_script(content: &str) -> String { + content.replace("\r\n", "\n").replace('\r', "\n") +} + pub use plugin::{ generate_zsh_plugin, generate_zsh_theme, run_zsh_doctor, run_zsh_keyboard, setup_zsh_integration, diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index e3fc744341..34fc6c6fd5 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -22,7 +22,7 @@ pub fn generate_zsh_plugin() -> Result { // Iterate through all embedded files in shell-plugin/lib, stripping comments // and empty lines. All files in this directory are .zsh files. for file in forge_embed::files(&ZSH_PLUGIN_LIB) { - let content = std::str::from_utf8(file.contents())?; + let content = super::normalize_script(std::str::from_utf8(file.contents())?); for line in content.lines() { let trimmed = line.trim(); // Skip empty lines and comment lines @@ -51,7 +51,8 @@ pub fn generate_zsh_plugin() -> Result { /// Generates the ZSH theme for Forge pub fn generate_zsh_theme() -> Result { - let mut content = include_str!("../../../../shell-plugin/forge.theme.zsh").replace('\r', ""); + let mut content = + super::normalize_script(include_str!("../../../../shell-plugin/forge.theme.zsh")); // Set environment variable to indicate theme is loaded (with timestamp) content.push_str("\n_FORGE_THEME_LOADED=$(date +%s)\n"); @@ -59,6 +60,19 @@ pub fn generate_zsh_theme() -> Result { Ok(content) } +/// Creates a temporary zsh script file for Windows execution +fn create_temp_zsh_script(script_content: &str) -> Result<(tempfile::TempDir, PathBuf)> { + use std::io::Write; + + let temp_dir = tempfile::tempdir().context("Failed to create temp directory")?; + let script_path = temp_dir.path().join("forge_script.zsh"); + let mut file = fs::File::create(&script_path).context("Failed to create temp script file")?; + file.write_all(script_content.as_bytes()) + .context("Failed to write temp script")?; + + Ok((temp_dir, script_path)) +} + /// Executes a ZSH script with streaming output /// /// # Arguments @@ -71,24 +85,35 @@ pub fn generate_zsh_theme() -> Result { /// Returns error if the script cannot be executed, if output streaming fails, /// or if the script exits with a non-zero status code fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> Result<()> { - let script_content = script_content.replace('\r', ""); - - // Write script to a temporary file - this is the most reliable approach - // Used by many production tools (kubectl, terraform, etc.) - let temp_dir = std::env::temp_dir(); - let script_path = temp_dir.join(format!("forge_{}.zsh", script_name)); - fs::write(&script_path, &script_content).context(format!( - "Failed to write {} script to temp file", - script_name - ))?; - - // Execute the script in a zsh subprocess with piped output - let mut child = std::process::Command::new("zsh") - .arg(&script_path) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .context(format!("Failed to execute zsh {} script", script_name))?; + let script_content = super::normalize_script(script_content); + + // On Unix, pass script via `zsh -c` -- Command::arg() uses execve which + // passes arguments directly without shell interpretation, so embedded + // quotes are safe. + // On Windows, write script to temp file and execute it with -f (no rc files) + // This avoids CreateProcess quote mangling AND prevents ~/.zshrc loading + let (_temp_dir, mut child) = if cfg!(windows) { + let (temp_dir, script_path) = create_temp_zsh_script(&script_content)?; + let child = std::process::Command::new("zsh") + // -f: don't load ~/.zshrc (prevents theme loading during doctor) + .arg("-f") + .arg(script_path.to_string_lossy().as_ref()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context(format!("Failed to execute zsh {} script", script_name))?; + // Keep temp_dir alive by boxing it in the tuple + (Some(temp_dir), child) + } else { + let child = std::process::Command::new("zsh") + .arg("-c") + .arg(&script_content) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context(format!("Failed to execute zsh {} script", script_name))?; + (None, child) + }; // Get stdout and stderr handles let stdout = child.stdout.take().context("Failed to capture stdout")?; @@ -124,13 +149,6 @@ fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> .wait() .context(format!("Failed to wait for zsh {} script", script_name))?; - // Clean up temporary script file - let _ = fs::remove_file(&script_path); - - // For diagnostic scripts (doctor, keyboard), non-zero exit codes are - // informational They indicate environment issues found, not script - // execution failures Only propagate the error if the script actually failed - // to execute if !status.success() { let exit_code = status .code() @@ -226,7 +244,8 @@ pub fn setup_zsh_integration( ) -> Result { const START_MARKER: &str = "# >>> forge initialize >>>"; const END_MARKER: &str = "# <<< forge initialize <<<"; - const FORGE_INIT_CONFIG: &str = include_str!("../../../../shell-plugin/forge.setup.zsh"); + const FORGE_INIT_CONFIG_RAW: &str = include_str!("../../../../shell-plugin/forge.setup.zsh"); + let forge_init_config = super::normalize_script(FORGE_INIT_CONFIG_RAW); let home = std::env::var("HOME").context("HOME environment variable not set")?; let zdotdir = std::env::var("ZDOTDIR").unwrap_or_else(|_| home.clone()); @@ -247,7 +266,7 @@ pub fn setup_zsh_integration( // Build the forge config block with markers let mut forge_config: Vec = vec![START_MARKER.to_string()]; - forge_config.extend(FORGE_INIT_CONFIG.lines().map(String::from)); + forge_config.extend(forge_init_config.lines().map(String::from)); // Add nerd font configuration if requested if disable_nerd_font { From 934b75e598212bd1120e5773c8191af8ea3f541e Mon Sep 17 00:00:00 2001 From: Sandipsinh Dilipsinh Rathod <62684960+ssddOnTop@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:22:03 -0400 Subject: [PATCH 09/10] docs(zsh): clarify Windows temp-file rationale in script execution comments --- crates/forge_main/src/zsh/plugin.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index 34fc6c6fd5..535bb44b6b 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -87,11 +87,19 @@ fn create_temp_zsh_script(script_content: &str) -> Result<(tempfile::TempDir, Pa fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> Result<()> { let script_content = super::normalize_script(script_content); - // On Unix, pass script via `zsh -c` -- Command::arg() uses execve which - // passes arguments directly without shell interpretation, so embedded - // quotes are safe. - // On Windows, write script to temp file and execute it with -f (no rc files) - // This avoids CreateProcess quote mangling AND prevents ~/.zshrc loading + // On Unix, pass the script via `zsh -c`. Command::arg() uses execve, + // which forwards arguments directly without shell interpretation, so + // embedded quotes are safe. + // + // On Windows, we write the script to a temp file and run `zsh -f ` + // instead. A temp file is necessary because: + // 1. CI has core.autocrlf=true, so checked-out files contain CRLF; + // writing through normalize_script ensures the temp file has LF. + // 2. CreateProcess mangles quotes, so passing the script via -c + // corrupts any embedded quoting. + // 3. Piping via stdin is unreliable -- Windows caps pipe buffer size, + // which can truncate or block on larger scripts. + // The -f flag also prevents ~/.zshrc from loading during execution. let (_temp_dir, mut child) = if cfg!(windows) { let (temp_dir, script_path) = create_temp_zsh_script(&script_content)?; let child = std::process::Command::new("zsh") From 88829488bb04065ef3a1b43714bbe75efb38d3c3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 05:23:51 +0000 Subject: [PATCH 10/10] [autofix.ci] apply automated fixes --- crates/forge_main/src/zsh/plugin.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index 535bb44b6b..27ec8678eb 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -93,12 +93,12 @@ fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> // // On Windows, we write the script to a temp file and run `zsh -f ` // instead. A temp file is necessary because: - // 1. CI has core.autocrlf=true, so checked-out files contain CRLF; - // writing through normalize_script ensures the temp file has LF. - // 2. CreateProcess mangles quotes, so passing the script via -c - // corrupts any embedded quoting. - // 3. Piping via stdin is unreliable -- Windows caps pipe buffer size, - // which can truncate or block on larger scripts. + // 1. CI has core.autocrlf=true, so checked-out files contain CRLF; writing + // through normalize_script ensures the temp file has LF. + // 2. CreateProcess mangles quotes, so passing the script via -c corrupts any + // embedded quoting. + // 3. Piping via stdin is unreliable -- Windows caps pipe buffer size, which + // can truncate or block on larger scripts. // The -f flag also prevents ~/.zshrc from loading during execution. let (_temp_dir, mut child) = if cfg!(windows) { let (temp_dir, script_path) = create_temp_zsh_script(&script_content)?;