99 getConfigDir ,
1010 getLogsDir ,
1111 getVenvDir ,
12- getVenvJupyterPath ,
13- getVenvPipPath ,
14- getVenvPythonPath ,
1512 getInvocationCwd ,
1613} from "./config.js" ;
1714
@@ -41,12 +38,105 @@ export function getDefaultPackages(): string[] {
4138 return [ ...DEFAULT_PACKAGES ] ;
4239}
4340
41+ function isUvAvailable ( ) : boolean {
42+ try {
43+ const res = spawnSync ( "uv" , [ "--version" ] , { stdio : "ignore" } ) ;
44+ return res . status === 0 ;
45+ } catch {
46+ return false ;
47+ }
48+ }
49+
50+ export async function ensureUvInstalled ( opts ?: {
51+ onMessage ?: ( line : string ) => void ;
52+ signal ?: AbortSignal ;
53+ } ) : Promise < void > {
54+ if ( isUvAvailable ( ) ) {
55+ return ; // Already installed
56+ }
57+
58+ const onMessage = opts ?. onMessage ?? ( ( ) => { } ) ;
59+ const platform = os . platform ( ) ;
60+
61+ onMessage ( "uv not found. Installing uv for faster Python environment management..." ) ;
62+
63+ try {
64+ if ( platform === "win32" ) {
65+ // For Windows, try to use the PowerShell installer
66+ await runCommand ( "powershell" , [ "-c" , "irm https://astral.sh/uv/install.ps1 | iex" ] , {
67+ onMessage,
68+ signal : opts ?. signal ,
69+ } ) ;
70+ } else {
71+ // Try curl first, fallback to wget if curl is not available
72+ let installCommand : string ;
73+ let installArgs : string [ ] ;
74+
75+ try {
76+ const curlCheck = spawnSync ( "curl" , [ "--version" ] , { stdio : "ignore" } ) ;
77+ if ( curlCheck . status === 0 ) {
78+ installCommand = "sh" ;
79+ installArgs = [ "-c" , "curl -LsSf https://astral.sh/uv/install.sh | sh" ] ;
80+ } else {
81+ throw new Error ( "curl not available" ) ;
82+ }
83+ } catch {
84+ try {
85+ const wgetCheck = spawnSync ( "wget" , [ "--version" ] , { stdio : "ignore" } ) ;
86+ if ( wgetCheck . status === 0 ) {
87+ installCommand = "sh" ;
88+ installArgs = [ "-c" , "wget -qO- https://astral.sh/uv/install.sh | sh" ] ;
89+ } else {
90+ throw new Error ( "Neither curl nor wget available" ) ;
91+ }
92+ } catch {
93+ throw new Error ( "Cannot install uv: neither curl nor wget is available" ) ;
94+ }
95+ }
96+
97+ await runCommand ( installCommand , installArgs , {
98+ onMessage,
99+ signal : opts ?. signal ,
100+ } ) ;
101+ }
102+
103+ // Verify installation
104+ if ( ! isUvAvailable ( ) ) {
105+ throw new Error (
106+ "uv installation completed but uv is still not available. You may need to restart your terminal or add uv to your PATH." ,
107+ ) ;
108+ }
109+
110+ onMessage ( "✅ uv installed successfully!" ) ;
111+ } catch ( error ) {
112+ const errorMsg = error instanceof Error ? error . message : String ( error ) ;
113+ onMessage ( `❌ Failed to install uv: ${ errorMsg } ` ) ;
114+ onMessage (
115+ "You can install uv manually by visiting: https://docs.astral.sh/uv/getting-started/installation/" ,
116+ ) ;
117+ throw new Error ( `uv installation failed: ${ errorMsg } ` ) ;
118+ }
119+ }
120+
44121export function isVenvReady ( ) : boolean {
45- const pythonPath = getVenvPythonPath ( ) ;
46- const pipPath = getVenvPipPath ( ) ;
47- const jupyterPath = getVenvJupyterPath ( ) ;
48- // Consider the environment ready only if the venv exists AND jupyter is installed.
49- return existsSync ( pythonPath ) && existsSync ( pipPath ) && existsSync ( jupyterPath ) ;
122+ const venvDir = getVenvDir ( ) ;
123+ if ( ! existsSync ( venvDir ) ) {
124+ return false ;
125+ }
126+
127+ try {
128+ // Check if uv can find python in the venv and if jupyter is installed
129+ const pythonCheck = spawnSync ( "uv" , [ "run" , "--python" , venvDir , "python" , "--version" ] , {
130+ stdio : "ignore" ,
131+ } ) ;
132+ const jupyterCheck = spawnSync ( "uv" , [ "run" , "--python" , venvDir , "jupyter" , "--version" ] , {
133+ stdio : "ignore" ,
134+ } ) ;
135+
136+ return pythonCheck . status === 0 && jupyterCheck . status === 0 ;
137+ } catch {
138+ return false ;
139+ }
50140}
51141
52142export async function ensureVenvAndPackages ( opts ?: {
@@ -56,14 +146,15 @@ export async function ensureVenvAndPackages(opts?: {
56146} ) : Promise < void > {
57147 ensureConfigDir ( ) ;
58148 const venvDir = getVenvDir ( ) ;
59- const pythonPath = getVenvPythonPath ( ) ;
60149 const packages = opts ?. packages ?? DEFAULT_PACKAGES ;
61150 const onMessage = opts ?. onMessage ?? ( ( ) => { } ) ;
62151
63- if ( ! existsSync ( pythonPath ) ) {
64- onMessage ( `Setting up Python venv at ${ venvDir } ...` ) ;
65- const pythonExe = detectPythonExecutable ( ) ;
66- await runCommand ( pythonExe . command , [ ...pythonExe . argsPrefix , "-m" , "venv" , venvDir ] , {
152+ // Ensure uv is installed first
153+ await ensureUvInstalled ( { onMessage : opts ?. onMessage , signal : opts ?. signal } ) ;
154+
155+ if ( ! existsSync ( venvDir ) ) {
156+ onMessage ( `Setting up Python venv at ${ venvDir } using uv...` ) ;
157+ await runCommand ( "uv" , [ "venv" , venvDir ] , {
67158 onMessage,
68159 signal : opts ?. signal ,
69160 } ) ;
@@ -77,37 +168,27 @@ export async function updateVenvPackages(opts?: {
77168 onMessage ?: ( line : string ) => void ;
78169 signal ?: AbortSignal ;
79170} ) : Promise < void > {
80- const pipPath = getVenvPipPath ( ) ;
81- const pythonPath = getVenvPythonPath ( ) ;
171+ const venvDir = getVenvDir ( ) ;
82172 const packages = opts ?. packages ?? DEFAULT_PACKAGES ;
83173 const onMessage = opts ?. onMessage ?? ( ( ) => { } ) ;
84174
85- if ( ! existsSync ( pythonPath ) ) {
175+ if ( ! existsSync ( venvDir ) ) {
86176 throw new Error ( "Python venv is not installed. Restart the app to set it up." ) ;
87177 }
88178
89- onMessage ( "Updating Python packages ..." ) ;
179+ // Ensure uv is available
180+ await ensureUvInstalled ( { onMessage : opts ?. onMessage , signal : opts ?. signal } ) ;
181+
182+ onMessage ( "Updating Python packages using uv..." ) ;
90183 const pipOnMessage = ( chunk : string ) => {
91184 for ( const line of chunk . split ( / \r ? \n / ) ) {
92185 if ( ! line ) continue ;
93186 if ( line . startsWith ( "Requirement already satisfied:" ) ) continue ;
94187 onMessage ( line ) ;
95188 }
96189 } ;
97- // Ensure pip exists; some environments may not provision pip in venv by default
98- if ( ! existsSync ( pipPath ) ) {
99- await runCommand ( pythonPath , [ "-m" , "ensurepip" , "--upgrade" ] , {
100- onMessage : pipOnMessage ,
101- signal : opts ?. signal ,
102- } ) ;
103- }
104190
105- // Use python -m pip for better reliability on Windows
106- await runCommand ( pythonPath , [ "-m" , "pip" , "install" , "--upgrade" , "pip" ] , {
107- onMessage : pipOnMessage ,
108- signal : opts ?. signal ,
109- } ) ;
110- await runCommand ( pythonPath , [ "-m" , "pip" , "install" , ...packages ] , {
191+ await runCommand ( "uv" , [ "pip" , "install" , "--python" , venvDir , ...packages ] , {
111192 onMessage : pipOnMessage ,
112193 signal : opts ?. signal ,
113194 } ) ;
@@ -132,15 +213,23 @@ export async function startServerInBackground(opts?: {
132213} ) : Promise < void > {
133214 const onMessage = opts ?. onMessage ?? ( ( ) => { } ) ;
134215 ensureConfigDir ( ) ;
135- const jupyterPath = getVenvJupyterPath ( ) ;
216+ const venvDir = getVenvDir ( ) ;
136217 const envPort = Number ( process . env . JUPYTER_PORT || "" ) ;
137218 const port = opts ?. port ?? ( Number . isFinite ( envPort ) && envPort > 0 ? envPort : 8888 ) ;
138219 const notebookDir = opts ?. notebookDir ?? getInvocationCwd ( ) ;
139220 const logsDir = getLogsDir ( ) ;
140221 const outPath = path . join ( logsDir , "jupyter.out.log" ) ;
141222 const errPath = path . join ( logsDir , "jupyter.err.log" ) ;
142223
143- if ( ! existsSync ( jupyterPath ) ) {
224+ // Check if jupyter is available in the venv
225+ try {
226+ const jupyterCheck = spawnSync ( "uv" , [ "run" , "--python" , venvDir , "jupyter" , "--version" ] , {
227+ stdio : "ignore" ,
228+ } ) ;
229+ if ( jupyterCheck . status !== 0 ) {
230+ throw new Error ( "Jupyter is not installed in the virtual environment." ) ;
231+ }
232+ } catch {
144233 throw new Error ( "Jupyter is not installed. Run setup first." ) ;
145234 }
146235
@@ -157,8 +246,12 @@ export async function startServerInBackground(opts?: {
157246
158247 const isWindows = os . platform ( ) === "win32" ;
159248 const child = spawn (
160- jupyterPath ,
249+ "uv" ,
161250 [
251+ "run" ,
252+ "--python" ,
253+ venvDir ,
254+ "jupyter" ,
162255 "notebook" ,
163256 // Keep browser disabled here; we'll explicitly open URLs when needed
164257 "--no-browser" ,
@@ -272,6 +365,21 @@ function cleanupMetaFile(): void {
272365 }
273366}
274367
368+ export async function runInVenv (
369+ command : string ,
370+ args : string [ ] = [ ] ,
371+ opts ?: {
372+ cwd ?: string ;
373+ onMessage ?: ( line : string ) => void ;
374+ signal ?: AbortSignal ;
375+ } ,
376+ ) : Promise < void > {
377+ const venvDir = getVenvDir ( ) ;
378+ await ensureUvInstalled ( { onMessage : opts ?. onMessage , signal : opts ?. signal } ) ;
379+
380+ await runCommand ( "uv" , [ "run" , "--python" , venvDir , command , ...args ] , opts ) ;
381+ }
382+
275383async function runCommand (
276384 cmd : string ,
277385 args : string [ ] ,
@@ -311,40 +419,6 @@ async function runCommand(
311419 } ) ;
312420}
313421
314- function detectPythonExecutable ( ) : { command : string ; argsPrefix : string [ ] } {
315- // On Windows prefer the Python launcher `py -3` if available.
316- // Else try `python3`, then `python`.
317- // As a final fallback, use Node's execPath (rarely useful).
318- const platform = os . platform ( ) ;
319- if ( platform === "win32" ) {
320- try {
321- const res = spawnSync ( "py" , [ "-3" , "--version" ] , { stdio : "ignore" } ) ;
322- if ( res . status === 0 ) return { command : "py" , argsPrefix : [ "-3" ] } ;
323- } catch {
324- // ignore
325- }
326- try {
327- const res = spawnSync ( "python" , [ "--version" ] , { stdio : "ignore" } ) ;
328- if ( res . status === 0 ) return { command : "python" , argsPrefix : [ ] } ;
329- } catch {
330- // ignore
331- }
332- }
333- try {
334- const res = spawnSync ( "python3" , [ "--version" ] , { stdio : "ignore" } ) ;
335- if ( res . status === 0 ) return { command : "python3" , argsPrefix : [ ] } ;
336- } catch {
337- // ignore
338- }
339- try {
340- const res = spawnSync ( "python" , [ "--version" ] , { stdio : "ignore" } ) ;
341- if ( res . status === 0 ) return { command : "python" , argsPrefix : [ ] } ;
342- } catch {
343- // ignore
344- }
345- return { command : process . execPath , argsPrefix : [ ] } ;
346- }
347-
348422async function waitForProcessExit ( pid : number , timeoutMs : number ) : Promise < boolean > {
349423 const deadline = Date . now ( ) + timeoutMs ;
350424 while ( Date . now ( ) < deadline ) {
0 commit comments