33 execFile ,
44} from "node:child_process" ;
55import os from "node:os" ;
6+ import path from "node:path" ;
67import { promisify } from "node:util" ;
78
89const execFileAsync = promisify ( execFile ) ;
@@ -19,14 +20,56 @@ let pathFixAttempted = false;
1920let pathFixSucceeded = false ;
2021
2122/**
22- * Gets the full shell environment by spawning a login shell.
23- * This captures PATH and other environment variables set in shell profiles
24- * which includes tools like git-lfs installed via homebrew.
23+ * Build Windows PATH by combining process.env.PATH with common install locations.
24+ * This ensures packaged apps on Windows can find user-installed tools.
25+ */
26+ function buildWindowsPath ( ) : string {
27+ const paths : string [ ] = [ ] ;
28+ const pathSeparator = ";" ;
29+
30+ // Start with existing PATH from process.env
31+ if ( process . env . PATH ) {
32+ paths . push ( ...process . env . PATH . split ( pathSeparator ) . filter ( Boolean ) ) ;
33+ }
34+
35+ // Add Windows-specific common paths
36+ const commonPaths = [
37+ // User-local installations (where tools like Claude CLI, git-lfs are often installed)
38+ path . join ( os . homedir ( ) , ".local" , "bin" ) ,
39+ // Git for Windows default location
40+ "C:\\Program Files\\Git\\cmd" ,
41+ "C:\\Program Files\\Git\\bin" ,
42+ // System paths (usually already in PATH, but ensure they're present)
43+ path . join ( process . env . SystemRoot || "C:\\Windows" , "System32" ) ,
44+ path . join ( process . env . SystemRoot || "C:\\Windows" ) ,
45+ ] ;
46+
47+ // Add common paths that aren't already in PATH
48+ for ( const commonPath of commonPaths ) {
49+ const normalizedPath = path . normalize ( commonPath ) ;
50+ // Case-insensitive check for Windows
51+ const normalizedLower = normalizedPath . toLowerCase ( ) ;
52+ const alreadyExists = paths . some (
53+ ( p ) => path . normalize ( p ) . toLowerCase ( ) === normalizedLower ,
54+ ) ;
55+ if ( ! alreadyExists ) {
56+ paths . push ( normalizedPath ) ;
57+ }
58+ }
59+
60+ return paths . join ( pathSeparator ) ;
61+ }
62+
63+ /**
64+ * Gets the full shell environment with proper PATH for all platforms.
65+ *
66+ * - **Windows**: Derives PATH from process.env + common install locations (no shell spawn)
67+ * - **macOS/Linux**: Spawns login shell to capture PATH from shell profiles
2568 *
26- * Uses -lc (login, command) instead of -ilc to avoid interactive prompts
27- * and TTY issues from dotfiles expecting a terminal .
69+ * This captures PATH and other environment variables needed to find user-installed tools
70+ * like git-lfs (homebrew on macOS) or Claude CLI (user-local on Windows) .
2871 *
29- * Results are cached for 1 minute to avoid spawning shells repeatedly .
72+ * Results are cached for 1 minute to avoid repeated operations .
3073 */
3174export async function getShellEnvironment ( ) : Promise < Record < string , string > > {
3275 const now = Date . now ( ) ;
@@ -36,20 +79,38 @@ export async function getShellEnvironment(): Promise<Record<string, string>> {
3679 return { ...cachedEnv } ;
3780 }
3881
39- // On Windows, use process.env directly (no Unix shell available)
82+ // Windows: derive PATH without shell invocation
83+ // Git Bash PATH doesn't include Windows user paths, so we build it manually
4084 if ( process . platform === "win32" ) {
41- const fallback : Record < string , string > = { } ;
42- for ( const [ key , value ] of Object . entries ( process . env ) ) {
85+ console . log (
86+ "[shell-env] Windows detected, deriving PATH without shell invocation" ,
87+ ) ;
88+ const env : Record < string , string > = {
89+ ...process . env ,
90+ PATH : buildWindowsPath ( ) ,
91+ HOME : os . homedir ( ) ,
92+ USER : os . userInfo ( ) . username ,
93+ USERPROFILE : os . homedir ( ) ,
94+ } ;
95+
96+ // Ensure all values are strings
97+ const stringEnv : Record < string , string > = { } ;
98+ for ( const [ key , value ] of Object . entries ( env ) ) {
4399 if ( typeof value === "string" ) {
44- fallback [ key ] = value ;
100+ stringEnv [ key ] = value ;
45101 }
46102 }
47- cachedEnv = fallback ;
103+
104+ cachedEnv = stringEnv ;
48105 cacheTime = now ;
49106 isFallbackCache = false ;
50- return { ...fallback } ;
107+ console . log (
108+ `[shell-env] Built Windows environment with ${ Object . keys ( stringEnv ) . length } vars` ,
109+ ) ;
110+ return { ...stringEnv } ;
51111 }
52112
113+ // macOS/Linux: spawn login shell to get full environment
53114 const shell =
54115 process . env . SHELL ||
55116 ( process . platform === "darwin" ? "/bin/zsh" : "/bin/bash" ) ;
0 commit comments