11import { app } from 'electron' ;
2- import { spawn } from 'child_process' ;
2+ import { execSync , spawn } from 'child_process' ;
33import { existsSync } from 'fs' ;
44import { join } from 'path' ;
55import { getUvMirrorEnv } from './uv-env' ;
6+ import { logger } from './logger' ;
67
78/**
89 * Get the path to the bundled uv binary
@@ -14,31 +15,59 @@ function getBundledUvPath(): string {
1415 const binName = platform === 'win32' ? 'uv.exe' : 'uv' ;
1516
1617 if ( app . isPackaged ) {
17- // In production, we flattened the structure to 'bin/'
1818 return join ( process . resourcesPath , 'bin' , binName ) ;
1919 } else {
20- // In dev, resources are at project root/resources/bin/<platform>-<arch>
2120 return join ( process . cwd ( ) , 'resources' , 'bin' , target , binName ) ;
2221 }
2322}
2423
2524/**
26- * Check if uv is available (either in system PATH or bundled)
25+ * Resolve the best uv binary to use.
26+ *
27+ * In packaged mode we always prefer the bundled binary so we never accidentally
28+ * pick up a system-wide uv that may be a different (possibly broken) version.
29+ * In dev we fall through to the system PATH for convenience.
2730 */
28- export async function checkUvInstalled ( ) : Promise < boolean > {
29- // 1. Check system PATH first
30- const inPath = await new Promise < boolean > ( ( resolve ) => {
31- const cmd = process . platform === 'win32' ? 'where.exe' : 'which' ;
32- const child = spawn ( cmd , [ 'uv' ] ) ;
33- child . on ( 'close' , ( code ) => resolve ( code === 0 ) ) ;
34- child . on ( 'error' , ( ) => resolve ( false ) ) ;
35- } ) ;
31+ function resolveUvBin ( ) : { bin : string ; source : 'bundled' | 'path' | 'bundled-fallback' } {
32+ const bundled = getBundledUvPath ( ) ;
33+
34+ if ( app . isPackaged ) {
35+ if ( existsSync ( bundled ) ) {
36+ return { bin : bundled , source : 'bundled' } ;
37+ }
38+ logger . warn ( `Bundled uv binary not found at ${ bundled } , falling back to system PATH` ) ;
39+ }
40+
41+ // Dev mode or missing bundled binary — check system PATH
42+ const found = findUvInPathSync ( ) ;
43+ if ( found ) return { bin : 'uv' , source : 'path' } ;
3644
37- if ( inPath ) return true ;
45+ if ( existsSync ( bundled ) ) {
46+ return { bin : bundled , source : 'bundled-fallback' } ;
47+ }
3848
39- // 2. Check bundled path
40- const bin = getBundledUvPath ( ) ;
41- return existsSync ( bin ) ;
49+ return { bin : 'uv' , source : 'path' } ;
50+ }
51+
52+ function findUvInPathSync ( ) : boolean {
53+ try {
54+ const cmd = process . platform === 'win32' ? 'where.exe uv' : 'which uv' ;
55+ execSync ( cmd , { stdio : 'ignore' , timeout : 5000 } ) ;
56+ return true ;
57+ } catch {
58+ return false ;
59+ }
60+ }
61+
62+ /**
63+ * Check if uv is available (either bundled or in system PATH)
64+ */
65+ export async function checkUvInstalled ( ) : Promise < boolean > {
66+ const { bin, source } = resolveUvBin ( ) ;
67+ if ( source === 'bundled' || source === 'bundled-fallback' ) {
68+ return existsSync ( bin ) ;
69+ }
70+ return findUvInPathSync ( ) ;
4271}
4372
4473/**
@@ -51,22 +80,14 @@ export async function installUv(): Promise<void> {
5180 const bin = getBundledUvPath ( ) ;
5281 throw new Error ( `uv not found in system PATH and bundled binary missing at ${ bin } ` ) ;
5382 }
54- console . log ( 'uv is available and ready to use' ) ;
83+ logger . info ( 'uv is available and ready to use' ) ;
5584}
5685
5786/**
5887 * Check if a managed Python 3.12 is ready and accessible
5988 */
6089export async function isPythonReady ( ) : Promise < boolean > {
61- // Use 'uv' if in PATH, otherwise use full bundled path
62- const inPath = await new Promise < boolean > ( ( resolve ) => {
63- const cmd = process . platform === 'win32' ? 'where.exe' : 'which' ;
64- const child = spawn ( cmd , [ 'uv' ] ) ;
65- child . on ( 'close' , ( code ) => resolve ( code === 0 ) ) ;
66- child . on ( 'error' , ( ) => resolve ( false ) ) ;
67- } ) ;
68-
69- const uvBin = inPath ? 'uv' : getBundledUvPath ( ) ;
90+ const { bin : uvBin } = resolveUvBin ( ) ;
7091
7192 return new Promise < boolean > ( ( resolve ) => {
7293 try {
@@ -82,70 +103,119 @@ export async function isPythonReady(): Promise<boolean> {
82103}
83104
84105/**
85- * Use bundled uv to install a managed Python version (default 3.12)
86- * Automatically picks the best available uv binary
106+ * Run ` uv python install 3.12` once with the given environment.
107+ * Returns on success, throws with captured stderr on failure.
87108 */
88- export async function setupManagedPython ( ) : Promise < void > {
89- // Use 'uv' if in PATH, otherwise use full bundled path
90- const inPath = await new Promise < boolean > ( ( resolve ) => {
91- const cmd = process . platform === 'win32' ? 'where.exe' : 'which' ;
92- const child = spawn ( cmd , [ 'uv' ] ) ;
93- child . on ( 'close' , ( code ) => resolve ( code === 0 ) ) ;
94- child . on ( 'error' , ( ) => resolve ( false ) ) ;
95- } ) ;
96-
97- const uvBin = inPath ? 'uv' : getBundledUvPath ( ) ;
98-
99- console . log ( `Setting up python with: ${ uvBin } ` ) ;
100- const uvEnv = await getUvMirrorEnv ( ) ;
109+ async function runPythonInstall (
110+ uvBin : string ,
111+ env : Record < string , string | undefined > ,
112+ label : string ,
113+ ) : Promise < void > {
114+ return new Promise < void > ( ( resolve , reject ) => {
115+ const stderrChunks : string [ ] = [ ] ;
116+ const stdoutChunks : string [ ] = [ ] ;
101117
102- await new Promise < void > ( ( resolve , reject ) => {
103118 const child = spawn ( uvBin , [ 'python' , 'install' , '3.12' ] , {
104119 shell : process . platform === 'win32' ,
105- env : {
106- ...process . env ,
107- ...uvEnv ,
108- } ,
120+ env,
109121 } ) ;
110122
111123 child . stdout ?. on ( 'data' , ( data ) => {
112- console . log ( `python setup stdout: ${ data } ` ) ;
124+ const line = data . toString ( ) . trim ( ) ;
125+ if ( line ) {
126+ stdoutChunks . push ( line ) ;
127+ logger . debug ( `[python-setup:${ label } ] stdout: ${ line } ` ) ;
128+ }
113129 } ) ;
114130
115131 child . stderr ?. on ( 'data' , ( data ) => {
116- // uv prints progress to stderr, so we log it as info
117- console . log ( `python setup info: ${ data . toString ( ) . trim ( ) } ` ) ;
132+ const line = data . toString ( ) . trim ( ) ;
133+ if ( line ) {
134+ stderrChunks . push ( line ) ;
135+ logger . info ( `[python-setup:${ label } ] stderr: ${ line } ` ) ;
136+ }
118137 } ) ;
119138
120139 child . on ( 'close' , ( code ) => {
121- if ( code === 0 ) resolve ( ) ;
122- else reject ( new Error ( `Python installation failed with code ${ code } ` ) ) ;
140+ if ( code === 0 ) {
141+ resolve ( ) ;
142+ } else {
143+ const stderr = stderrChunks . join ( '\n' ) ;
144+ const stdout = stdoutChunks . join ( '\n' ) ;
145+ const detail = stderr || stdout || '(no output captured)' ;
146+ reject ( new Error (
147+ `Python installation failed with code ${ code } [${ label } ]\n` +
148+ ` uv binary: ${ uvBin } \n` +
149+ ` platform: ${ process . platform } /${ process . arch } \n` +
150+ ` output: ${ detail } `
151+ ) ) ;
152+ }
123153 } ) ;
124154
125- child . on ( 'error' , ( err ) => reject ( err ) ) ;
155+ child . on ( 'error' , ( err ) => {
156+ reject ( new Error (
157+ `Python installation spawn error [${ label } ]: ${ err . message } \n` +
158+ ` uv binary: ${ uvBin } \n` +
159+ ` platform: ${ process . platform } /${ process . arch } `
160+ ) ) ;
161+ } ) ;
126162 } ) ;
163+ }
164+
165+ /**
166+ * Use bundled uv to install a managed Python version (default 3.12).
167+ *
168+ * Tries with mirror env first (for CN region), then retries without mirror
169+ * if the first attempt fails, to rule out mirror-specific issues.
170+ */
171+ export async function setupManagedPython ( ) : Promise < void > {
172+ const { bin : uvBin , source } = resolveUvBin ( ) ;
173+ const uvEnv = await getUvMirrorEnv ( ) ;
174+ const hasMirror = Object . keys ( uvEnv ) . length > 0 ;
175+
176+ logger . info (
177+ `Setting up managed Python 3.12 ` +
178+ `(uv=${ uvBin } , source=${ source } , arch=${ process . arch } , mirror=${ hasMirror } )`
179+ ) ;
180+
181+ const baseEnv : Record < string , string | undefined > = { ...process . env } ;
182+
183+ // Attempt 1: with mirror (if applicable)
184+ try {
185+ await runPythonInstall ( uvBin , { ...baseEnv , ...uvEnv } , hasMirror ? 'mirror' : 'default' ) ;
186+ } catch ( firstError ) {
187+ logger . warn ( 'Python install attempt 1 failed:' , firstError ) ;
188+
189+ if ( hasMirror ) {
190+ // Attempt 2: retry without mirror to rule out mirror issues
191+ logger . info ( 'Retrying Python install without mirror...' ) ;
192+ try {
193+ await runPythonInstall ( uvBin , baseEnv , 'no-mirror' ) ;
194+ } catch ( secondError ) {
195+ logger . error ( 'Python install attempt 2 (no mirror) also failed:' , secondError ) ;
196+ throw secondError ;
197+ }
198+ } else {
199+ throw firstError ;
200+ }
201+ }
127202
128- // After installation, find and print where the Python executable is
203+ // After installation, verify and log the Python path
129204 try {
130205 const findPath = await new Promise < string > ( ( resolve ) => {
131206 const child = spawn ( uvBin , [ 'python' , 'find' , '3.12' ] , {
132207 shell : process . platform === 'win32' ,
133- env : {
134- ...process . env ,
135- ...uvEnv ,
136- } ,
208+ env : { ...process . env , ...uvEnv } ,
137209 } ) ;
138210 let output = '' ;
139211 child . stdout ?. on ( 'data' , ( data ) => { output += data ; } ) ;
140212 child . on ( 'close' , ( ) => resolve ( output . trim ( ) ) ) ;
141213 } ) ;
142214
143215 if ( findPath ) {
144- console . log ( `✅ Managed Python 3.12 path: ${ findPath } ` ) ;
145- // Note: uv stores environments in a central cache,
146- // Individual skills will create their own venvs in ~/.cache/uv or similar.
216+ logger . info ( `Managed Python 3.12 installed at: ${ findPath } ` ) ;
147217 }
148218 } catch ( err ) {
149- console . warn ( 'Could not determine Python path:' , err ) ;
219+ logger . warn ( 'Could not determine Python path after install :' , err ) ;
150220 }
151221}
0 commit comments