1111// `agentsView` fall back to `agentArgs` — an autonomous session with native
1212// approvals bypassed/auto-approved.
1313import { spawn } from "node:child_process" ;
14- import { existsSync , statSync } from "node:fs" ;
14+ import { existsSync , readFileSync , statSync } from "node:fs" ;
1515import path from "node:path" ;
1616
1717export const ENGINES = {
@@ -82,15 +82,54 @@ export function resolveEngine(token) {
8282 return key ? [ key , ENGINES [ key ] ] : null ;
8383}
8484
85- /** Is `bin` an executable on PATH? (cross-platform-ish) */
86- export function isInstalled ( bin ) {
87- const exts = process . platform === "win32" ? ( process . env . PATHEXT || ".EXE;.CMD;.BAT" ) . split ( ";" ) : [ "" ] ;
88- for ( const dir of ( process . env . PATH || "" ) . split ( path . delimiter ) . filter ( Boolean ) ) {
85+ function executableCandidates ( bin ) {
86+ const exts = process . platform === "win32" ? [ "" , ...( process . env . PATHEXT || ".EXE;.CMD;.BAT" ) . split ( ";" ) ] : [ "" ] ;
87+ const dirs = path . isAbsolute ( bin ) || bin . includes ( path . sep ) ? [ "" ] : ( process . env . PATH || "" ) . split ( path . delimiter ) . filter ( Boolean ) ;
88+ const seen = new Set ( ) ;
89+ const candidates = [ ] ;
90+ for ( const dir of dirs ) {
8991 for ( const ext of exts ) {
90- try { if ( existsSync ( path . join ( dir , bin + ext ) ) && statSync ( path . join ( dir , bin + ext ) ) . isFile ( ) ) return true ; } catch { /* keep looking */ }
92+ const candidate = dir ? path . join ( dir , bin + ext ) : bin + ext ;
93+ const key = candidate . toLowerCase ( ) ;
94+ if ( ! seen . has ( key ) ) {
95+ seen . add ( key ) ;
96+ candidates . push ( candidate ) ;
97+ }
9198 }
9299 }
93- return false ;
100+ return candidates ;
101+ }
102+
103+ function resolveExecutable ( bin ) {
104+ for ( const candidate of executableCandidates ( bin ) ) {
105+ try {
106+ if ( existsSync ( candidate ) && statSync ( candidate ) . isFile ( ) ) return candidate ;
107+ } catch { /* keep looking */ }
108+ }
109+ return null ;
110+ }
111+
112+ function nodeShebang ( file ) {
113+ try {
114+ const head = readFileSync ( file , "utf8" ) . slice ( 0 , 80 ) ;
115+ return / ^ # ! .* \b n o d e (?: \. e x e ) ? \b / . test ( head ) ;
116+ } catch {
117+ return false ;
118+ }
119+ }
120+
121+ function spawnSpec ( bin , args = [ ] ) {
122+ const resolved = resolveExecutable ( bin ) ;
123+ if ( ! resolved ) return { cmd : bin , args } ;
124+ if ( process . platform === "win32" && path . extname ( resolved ) === "" && nodeShebang ( resolved ) ) {
125+ return { cmd : process . execPath , args : [ resolved , ...args ] } ;
126+ }
127+ return { cmd : resolved , args } ;
128+ }
129+
130+ /** Is `bin` an executable on PATH? (cross-platform-ish) */
131+ export function isInstalled ( bin ) {
132+ return Boolean ( resolveExecutable ( bin ) ) ;
94133}
95134
96135// Headless "run one prompt, print the answer, exit" invocation per engine — the
@@ -147,7 +186,8 @@ export function agentLaunchArgs(engine, args = []) {
147186export function runCmd ( cmd , args = [ ] ) {
148187 return new Promise ( ( resolve ) => {
149188 let child ;
150- try { child = spawn ( cmd , args , { stdio : "inherit" } ) ; }
189+ const spec = spawnSpec ( cmd , args ) ;
190+ try { child = spawn ( spec . cmd , spec . args , { stdio : "inherit" } ) ; }
151191 catch ( e ) { resolve ( { ok : false , error : e } ) ; return ; }
152192 child . on ( "error" , ( e ) => resolve ( { ok : false , error : e } ) ) ;
153193 child . on ( "exit" , ( code , signal ) => resolve ( { ok : true , code, signal } ) ) ;
@@ -168,7 +208,8 @@ export function openPassthrough(target, args = []) {
168208 for ( const k of target . stripEnv ) delete env [ k ] ;
169209 }
170210 let child ;
171- try { child = spawn ( target . bin , args , { stdio : "inherit" , env } ) ; }
211+ const spec = spawnSpec ( target . bin , args ) ;
212+ try { child = spawn ( spec . cmd , spec . args , { stdio : "inherit" , env } ) ; }
172213 catch ( e ) { resolve ( { ok : false , error : e } ) ; return ; }
173214 child . on ( "error" , ( e ) => resolve ( { ok : false , error : e } ) ) ;
174215 child . on ( "exit" , ( code , signal ) => resolve ( { ok : true , code, signal } ) ) ;
0 commit comments