@@ -22,7 +22,7 @@ pub fn generate_zsh_plugin() -> Result<String> {
2222 // Iterate through all embedded files in shell-plugin/lib, stripping comments
2323 // and empty lines. All files in this directory are .zsh files.
2424 for file in forge_embed:: files ( & ZSH_PLUGIN_LIB ) {
25- let content = std:: str:: from_utf8 ( file. contents ( ) ) ?;
25+ let content = super :: normalize_script ( std:: str:: from_utf8 ( file. contents ( ) ) ?) ;
2626 for line in content. lines ( ) {
2727 let trimmed = line. trim ( ) ;
2828 // Skip empty lines and comment lines
@@ -51,14 +51,28 @@ pub fn generate_zsh_plugin() -> Result<String> {
5151
5252/// Generates the ZSH theme for Forge
5353pub fn generate_zsh_theme ( ) -> Result < String > {
54- let mut content = include_str ! ( "../../../../shell-plugin/forge.theme.zsh" ) . to_string ( ) ;
54+ let mut content =
55+ super :: normalize_script ( include_str ! ( "../../../../shell-plugin/forge.theme.zsh" ) ) ;
5556
5657 // Set environment variable to indicate theme is loaded (with timestamp)
5758 content. push_str ( "\n _FORGE_THEME_LOADED=$(date +%s)\n " ) ;
5859
5960 Ok ( content)
6061}
6162
63+ /// Creates a temporary zsh script file for Windows execution
64+ fn create_temp_zsh_script ( script_content : & str ) -> Result < ( tempfile:: TempDir , PathBuf ) > {
65+ use std:: io:: Write ;
66+
67+ let temp_dir = tempfile:: tempdir ( ) . context ( "Failed to create temp directory" ) ?;
68+ let script_path = temp_dir. path ( ) . join ( "forge_script.zsh" ) ;
69+ let mut file = fs:: File :: create ( & script_path) . context ( "Failed to create temp script file" ) ?;
70+ file. write_all ( script_content. as_bytes ( ) )
71+ . context ( "Failed to write temp script" ) ?;
72+
73+ Ok ( ( temp_dir, script_path) )
74+ }
75+
6276/// Executes a ZSH script with streaming output
6377///
6478/// # Arguments
@@ -71,14 +85,43 @@ pub fn generate_zsh_theme() -> Result<String> {
7185/// Returns error if the script cannot be executed, if output streaming fails,
7286/// or if the script exits with a non-zero status code
7387fn execute_zsh_script_with_streaming ( script_content : & str , script_name : & str ) -> Result < ( ) > {
74- // Execute the script in a zsh subprocess with piped output
75- let mut child = std:: process:: Command :: new ( "zsh" )
76- . arg ( "-c" )
77- . arg ( script_content)
78- . stdout ( Stdio :: piped ( ) )
79- . stderr ( Stdio :: piped ( ) )
80- . spawn ( )
81- . context ( format ! ( "Failed to execute zsh {} script" , script_name) ) ?;
88+ let script_content = super :: normalize_script ( script_content) ;
89+
90+ // On Unix, pass the script via `zsh -c`. Command::arg() uses execve,
91+ // which forwards arguments directly without shell interpretation, so
92+ // embedded quotes are safe.
93+ //
94+ // On Windows, we write the script to a temp file and run `zsh -f <file>`
95+ // instead. A temp file is necessary because:
96+ // 1. CI has core.autocrlf=true, so checked-out files contain CRLF; writing
97+ // through normalize_script ensures the temp file has LF.
98+ // 2. CreateProcess mangles quotes, so passing the script via -c corrupts any
99+ // embedded quoting.
100+ // 3. Piping via stdin is unreliable -- Windows caps pipe buffer size, which
101+ // can truncate or block on larger scripts.
102+ // The -f flag also prevents ~/.zshrc from loading during execution.
103+ let ( _temp_dir, mut child) = if cfg ! ( windows) {
104+ let ( temp_dir, script_path) = create_temp_zsh_script ( & script_content) ?;
105+ let child = std:: process:: Command :: new ( "zsh" )
106+ // -f: don't load ~/.zshrc (prevents theme loading during doctor)
107+ . arg ( "-f" )
108+ . arg ( script_path. to_string_lossy ( ) . as_ref ( ) )
109+ . stdout ( Stdio :: piped ( ) )
110+ . stderr ( Stdio :: piped ( ) )
111+ . spawn ( )
112+ . context ( format ! ( "Failed to execute zsh {} script" , script_name) ) ?;
113+ // Keep temp_dir alive by boxing it in the tuple
114+ ( Some ( temp_dir) , child)
115+ } else {
116+ let child = std:: process:: Command :: new ( "zsh" )
117+ . arg ( "-c" )
118+ . arg ( & script_content)
119+ . stdout ( Stdio :: piped ( ) )
120+ . stderr ( Stdio :: piped ( ) )
121+ . spawn ( )
122+ . context ( format ! ( "Failed to execute zsh {} script" , script_name) ) ?;
123+ ( None , child)
124+ } ;
82125
83126 // Get stdout and stderr handles
84127 let stdout = child. stdout . take ( ) . context ( "Failed to capture stdout" ) ?;
@@ -209,7 +252,8 @@ pub fn setup_zsh_integration(
209252) -> Result < ZshSetupResult > {
210253 const START_MARKER : & str = "# >>> forge initialize >>>" ;
211254 const END_MARKER : & str = "# <<< forge initialize <<<" ;
212- const FORGE_INIT_CONFIG : & str = include_str ! ( "../../../../shell-plugin/forge.setup.zsh" ) ;
255+ const FORGE_INIT_CONFIG_RAW : & str = include_str ! ( "../../../../shell-plugin/forge.setup.zsh" ) ;
256+ let forge_init_config = super :: normalize_script ( FORGE_INIT_CONFIG_RAW ) ;
213257
214258 let home = std:: env:: var ( "HOME" ) . context ( "HOME environment variable not set" ) ?;
215259 let zdotdir = std:: env:: var ( "ZDOTDIR" ) . unwrap_or_else ( |_| home. clone ( ) ) ;
@@ -230,7 +274,7 @@ pub fn setup_zsh_integration(
230274
231275 // Build the forge config block with markers
232276 let mut forge_config: Vec < String > = vec ! [ START_MARKER . to_string( ) ] ;
233- forge_config. extend ( FORGE_INIT_CONFIG . lines ( ) . map ( String :: from) ) ;
277+ forge_config. extend ( forge_init_config . lines ( ) . map ( String :: from) ) ;
234278
235279 // Add nerd font configuration if requested
236280 if disable_nerd_font {
0 commit comments