@@ -5,12 +5,19 @@ import * as path from "path";
55import * as pathWin32 from "path/win32" ;
66
77const WINDOWS_GIT_LOCATIONS = [ "C:\\Program Files\\Git\\cmd\\git.exe" , "C:\\Program Files (x86)\\Git\\cmd\\git.exe" ] ;
8+ const WINDOWS_BASH_LOCATIONS = [ "C:\\Program Files\\Git\\bin\\bash.exe" , "C:\\Program Files (x86)\\Git\\bin\\bash.exe" ] ;
89
910const NUL_REDIRECT_REGEX = / ( \d ? & ? > + \s * ) [ N n ] [ U u ] [ L l ] (? = \s | $ | [ | & ; ) \n ] ) / g;
1011let cachedGitBashPath : string | null = null ;
1112
1213export type ShellKind = "bash" | "zsh" | "unknown" ;
1314
15+ type WindowsGitBashLookup = {
16+ findExecutableCandidates : ( executable : string ) => string [ ] ;
17+ findGitExecPath : ( ) => string | null ;
18+ existsSync : ( candidate : string ) => boolean ;
19+ } ;
20+
1421export function setShellIfWindows ( ) : void {
1522 if ( process . platform !== "win32" ) {
1623 return ;
@@ -23,16 +30,30 @@ export function findGitBashPath(): string {
2330 return cachedGitBashPath ;
2431 }
2532
26- for ( const gitPath of findAllWindowsExecutableCandidates ( "git" ) ) {
27- const bashPath = pathWin32 . join ( gitPath , ".." , ".." , "bin" , "bash.exe" ) ;
28- if ( fs . existsSync ( bashPath ) ) {
29- cachedGitBashPath = bashPath ;
30- return bashPath ;
31- }
33+ const bashPath = resolveWindowsGitBashPath ( {
34+ findExecutableCandidates : findAllWindowsExecutableCandidates ,
35+ findGitExecPath,
36+ existsSync : fs . existsSync ,
37+ } ) ;
38+ if ( bashPath ) {
39+ cachedGitBashPath = bashPath ;
40+ return bashPath ;
3241 }
3342
3443 throw new Error (
35- "Deep Code on Windows requires Git Bash. Install Git Bash for Windows and ensure bash.exe is available in PATH."
44+ "Deep Code on Windows requires Git Bash. Install Git for Windows, or ensure Git's bash.exe is available in PATH."
45+ ) ;
46+ }
47+
48+ export function resolveWindowsGitBashPath ( lookup : WindowsGitBashLookup ) : string | null {
49+ return firstExistingWindowsPath (
50+ [
51+ ...lookup . findExecutableCandidates ( "bash" ) ,
52+ ...WINDOWS_BASH_LOCATIONS ,
53+ ...gitExecPathToBashCandidates ( lookup . findGitExecPath ( ) ) ,
54+ ...lookup . findExecutableCandidates ( "git" ) . flatMap ( gitExecutableToBashCandidates ) ,
55+ ] ,
56+ lookup . existsSync
3657 ) ;
3758}
3859
@@ -146,26 +167,76 @@ export function buildShellEnv(shellPath: string, extraEnv: Record<string, string
146167}
147168
148169function findAllWindowsExecutableCandidates ( executable : string ) : string [ ] {
149- const extraCandidates = executable === "git" ? WINDOWS_GIT_LOCATIONS : [ ] ;
170+ const extraCandidates =
171+ executable === "git" ? WINDOWS_GIT_LOCATIONS : executable === "bash" ? WINDOWS_BASH_LOCATIONS : [ ] ;
150172
151173 try {
152174 const output = execFileSync ( "where.exe" , [ executable ] , {
153175 encoding : "utf8" ,
154176 stdio : [ "ignore" , "pipe" , "ignore" ] ,
155177 windowsHide : true ,
156178 } ) ;
157- return filterWindowsExecutableCandidates ( [
158- ...output
159- . split ( / \r ? \n / )
160- . map ( ( line ) => line . trim ( ) )
161- . filter ( Boolean ) ,
162- ...extraCandidates ,
163- ] ) ;
179+ let whereResults = output
180+ . split ( / \r ? \n / )
181+ . map ( ( line ) => line . trim ( ) )
182+ . filter ( Boolean ) ;
183+ if ( executable === "bash" ) {
184+ // Skip WSL's deprecated bash.exe launcher (C:\Windows\System32\bash.exe).
185+ // It would start commands inside the Linux distro instead of the Windows host,
186+ // breaking all path translations and tool invocations.
187+ whereResults = whereResults . filter ( ( candidate ) => ! / s y s t e m 3 2 [ \\ / ] b a s h \. e x e $ / i. test ( candidate ) ) ;
188+ }
189+ return filterWindowsExecutableCandidates ( [ ...whereResults , ...extraCandidates ] ) ;
164190 } catch {
165191 return filterWindowsExecutableCandidates ( extraCandidates ) ;
166192 }
167193}
168194
195+ function findGitExecPath ( ) : string | null {
196+ try {
197+ const output = execFileSync ( "git" , [ "--exec-path" ] , {
198+ encoding : "utf8" ,
199+ stdio : [ "ignore" , "pipe" , "ignore" ] ,
200+ windowsHide : true ,
201+ } ) . trim ( ) ;
202+ return output || null ;
203+ } catch {
204+ return null ;
205+ }
206+ }
207+
208+ function gitExecPathToBashCandidates ( execPath : string | null ) : string [ ] {
209+ if ( ! execPath ) {
210+ return [ ] ;
211+ }
212+
213+ const normalized = execPath . replace ( / \/ / g, "\\" ) ;
214+ return [
215+ pathWin32 . join ( normalized , ".." , ".." , ".." , "bin" , "bash.exe" ) ,
216+ pathWin32 . join ( normalized , ".." , ".." , "bin" , "bash.exe" ) ,
217+ ] ;
218+ }
219+
220+ function gitExecutableToBashCandidates ( gitPath : string ) : string [ ] {
221+ return [ pathWin32 . join ( gitPath , ".." , ".." , "bin" , "bash.exe" ) , pathWin32 . join ( gitPath , ".." , "bin" , "bash.exe" ) ] ;
222+ }
223+
224+ function firstExistingWindowsPath ( candidates : string [ ] , existsSync : ( candidate : string ) => boolean ) : string | null {
225+ const seen = new Set < string > ( ) ;
226+ for ( const candidate of candidates ) {
227+ const normalized = pathWin32 . resolve ( candidate ) ;
228+ const key = normalized . toLowerCase ( ) ;
229+ if ( seen . has ( key ) ) {
230+ continue ;
231+ }
232+ seen . add ( key ) ;
233+ if ( getShellKind ( normalized ) === "bash" && existsSync ( normalized ) ) {
234+ return normalized ;
235+ }
236+ }
237+ return null ;
238+ }
239+
169240function filterWindowsExecutableCandidates ( candidates : string [ ] ) : string [ ] {
170241 const cwd = process . cwd ( ) . toLowerCase ( ) ;
171242 const seen = new Set < string > ( ) ;
0 commit comments