@@ -87,14 +87,18 @@ class WhisperInstaller {
8787 // 1. Honor WHISPER_COMMAND env if user has it set already
8888 const fromEnv = ( process . env . WHISPER_COMMAND || '' ) . trim ( ) ;
8989 if ( fromEnv ) {
90- const probe = await this . _probe ( fromEnv ) ;
91- if ( probe . ok ) {
92- return {
93- found : true ,
94- command : fromEnv ,
95- version : probe . version ,
96- source : 'env' ,
97- } ;
90+ const parsed = this . _parseCommandString ( fromEnv ) ;
91+ if ( parsed && parsed . length > 0 ) {
92+ // eslint-disable-next-line no-await-in-loop
93+ const probe = await this . _probe ( parsed ) ;
94+ if ( probe . ok ) {
95+ return {
96+ found : true ,
97+ command : fromEnv ,
98+ version : probe . version ,
99+ source : 'env' ,
100+ } ;
101+ }
98102 }
99103 }
100104
@@ -106,7 +110,9 @@ class WhisperInstaller {
106110 if ( probe . ok ) {
107111 return {
108112 found : true ,
109- command : candidate ,
113+ // Join for display in the wizard UI; the array is preserved
114+ // separately if we ever need to re-execute.
115+ command : candidate . join ( ' ' ) ,
110116 version : probe . version ,
111117 source : 'probe' ,
112118 } ;
@@ -206,41 +212,65 @@ class WhisperInstaller {
206212
207213 _candidateCommands ( ) {
208214 if ( this . platform === 'win32' ) {
209- const localVenv = path . join ( this . cwd , '.venv-whisper' , 'Scripts' , 'whisper.exe' ) ;
215+ const venvWhisper = path . join ( this . cwd , '.venv-whisper' , 'Scripts' , 'whisper.exe' ) ;
216+ const venvPython = path . join ( this . cwd , '.venv-whisper' , 'Scripts' , 'python.exe' ) ;
210217 return [
211- 'whisper' ,
212- 'whisper.exe' ,
213- localVenv ,
214- path . join ( this . cwd , '.venv-whisper' , 'Scripts' , 'python.exe' ) + ' -m whisper' ,
215- 'python -m whisper' ,
216- 'python3 -m whisper' ,
218+ // Plain `whisper` on PATH — respects PATHEXT, picks up `whisper.exe`
219+ [ 'whisper' ] ,
220+ [ 'whisper.exe' ] ,
221+ // Project-local venv direct path — works even when venv's Scripts
222+ // folder isn't on PATH. path.join keeps backslashes intact.
223+ [ venvWhisper ] ,
224+ // Run whisper as a Python module through the venv's python.exe.
225+ // Using array form (not string concat) so paths-with-spaces work.
226+ [ venvPython , '-m' , 'whisper' ] ,
227+ // System Python via the launcher `py` (the canonical Windows
228+ // invocation), then bare `python`, then `python3`.
229+ [ 'py' , '-m' , 'whisper' ] ,
230+ [ 'python' , '-m' , 'whisper' ] ,
231+ [ 'python3' , '-m' , 'whisper' ] ,
217232 ] ;
218233 }
219234 if ( this . platform === 'darwin' ) {
220235 const homebrew = '/opt/homebrew/bin/whisper' ;
221236 const usrLocal = '/usr/local/bin/whisper' ;
237+ const venvWhisper = path . join ( this . cwd , '.venv-whisper' , 'bin' , 'whisper' ) ;
222238 return [
223- homebrew ,
224- usrLocal ,
225- 'whisper' ,
226- 'python3 -m whisper' ,
227- path . join ( this . cwd , '.venv-whisper' , 'bin' , 'whisper' ) ,
239+ [ homebrew ] ,
240+ [ usrLocal ] ,
241+ [ 'whisper' ] ,
242+ [ 'python3' , '-m' , ' whisper'] ,
243+ [ venvWhisper ] ,
228244 ] ;
229245 }
230246 const localVenvBin = path . join ( this . cwd , '.venv-whisper' , 'bin' , 'whisper' ) ;
231247 return [
232- 'whisper' ,
233- '/usr/local/bin/whisper' ,
234- '/usr/bin/whisper' ,
235- localVenvBin ,
236- 'python3 -m whisper' ,
248+ [ 'whisper' ] ,
249+ [ '/usr/local/bin/whisper' ] ,
250+ [ '/usr/bin/whisper' ] ,
251+ [ localVenvBin ] ,
252+ [ 'python3' , '-m' , ' whisper'] ,
237253 ] ;
238254 }
239255
240- async _probe ( command ) {
241- const parts = command . match ( / (?: [ ^ \s " ] + | " [ ^ " ] * " ) + / g) || [ command ] ;
242- const cmd = parts [ 0 ] . replace ( / ^ " | " $ / g, '' ) ;
243- const args = [ ...parts . slice ( 1 ) . map ( ( a ) => a . replace ( / ^ " | " $ / g, '' ) ) , '--help' ] ;
256+ /**
257+ * Parse a user-supplied command string (e.g. from WHISPER_COMMAND env)
258+ * into a `[cmd, ...args]` tuple. Respects double-quoted segments so
259+ * paths-with-spaces survive intact.
260+ */
261+ _parseCommandString ( cmdString ) {
262+ if ( ! cmdString ) return null ;
263+ const trimmed = String ( cmdString ) . trim ( ) ;
264+ if ( ! trimmed ) return null ;
265+ const parts = trimmed . match ( / (?: [ ^ \s " ] + | " [ ^ " ] * " ) + / g) || [ trimmed ] ;
266+ return parts . map ( ( p ) => p . replace ( / ^ " | " $ / g, '' ) ) ;
267+ }
268+
269+ async _probe ( candidate ) {
270+ // Candidate is now always `[cmd, ...args]`. We append `--help` to
271+ // confirm the binary actually runs without surfacing a usage error.
272+ const cmd = candidate [ 0 ] ;
273+ const args = [ ...candidate . slice ( 1 ) , '--help' ] ;
244274 const r = await this . runExec ( cmd , args ) ;
245275 if ( ! r . ok ) return { ok : false } ;
246276 const version = this . _extractVersion ( r . stdout + r . stderr ) ;
@@ -281,8 +311,11 @@ class WhisperInstaller {
281311 }
282312
283313 _detectPython ( ) {
314+ // On Windows, `py` (the Python Launcher) is almost always on PATH
315+ // when Python is installed — `python` and `python3` are optional.
316+ // On macOS/Linux, prefer `python3` since `python` may point at Py2.
284317 const candidates = this . platform === 'win32'
285- ? [ 'python ' , 'py ' , 'python3' ]
318+ ? [ 'py ' , 'python ' , 'python3' ]
286319 : [ 'python3' , 'python' ] ;
287320 // Sync probe — just check existence. We don't have an async probe
288321 // here without breaking the install flow; fall back to first candidate.
0 commit comments