11const fs = require ( 'node:fs' ) ;
22const os = require ( 'node:os' ) ;
33const path = require ( 'node:path' ) ;
4- const { execFileSync } = require ( 'node:child_process' ) ;
4+ const { execFileSync, execSync } = require ( 'node:child_process' ) ;
55
66function safeResolve ( resolver ) {
77 try {
@@ -55,14 +55,15 @@ function directoryExists(cwd, fsModule = fs) {
5555}
5656
5757function resolveSpawnConfig ( options , runtime = { } ) {
58- const { cols = 80 , rows = 30 , cwd } = options || { } ;
58+ const { cols = 80 , rows = 30 , cwd, shell : explicitShell , args : explicitArgs } = options || { } ;
5959 const env = runtime . env || process . env ;
6060 const platform = runtime . platform || process . platform ;
6161 const osModule = runtime . osModule || os ;
6262 const fsModule = runtime . fsModule || fs ;
6363 const defaultCwd = resolveDefaultCwd ( platform , env , osModule ) ;
6464 const missingExplicitCwd = Boolean ( cwd ) && ! directoryExists ( cwd , fsModule ) ;
65- const shell = resolveDefaultShell ( platform , env ) ;
65+ const shell = explicitShell || resolveDefaultShell ( platform , env ) ;
66+ const shellArgs = explicitArgs || resolveLoginArg ( shell , platform ) ;
6667
6768 return {
6869 cols,
@@ -71,12 +72,146 @@ function resolveSpawnConfig(options, runtime = {}) {
7172 cwdWarning : missingExplicitCwd ? `unable to restore because directory ${ cwd } was removed` : null ,
7273 env : { ...env , TERM_PROGRAM : 'MouseTerm' } ,
7374 shell,
74- loginArg : resolveLoginArg ( shell , platform ) ,
75+ shellArgs ,
7576 } ;
7677}
7778
7879module . exports . resolveSpawnConfig = resolveSpawnConfig ;
7980
81+ // ── Shell detection ────────────────────────────────────────────────────────
82+
83+ function fileExists ( filePath , fsModule = fs ) {
84+ try {
85+ return fsModule . statSync ( filePath ) . isFile ( ) ;
86+ } catch {
87+ return false ;
88+ }
89+ }
90+
91+ function detectWindowsShells ( runtime = { } ) {
92+ const env = runtime . env || process . env ;
93+ const fsModule = runtime . fsModule || fs ;
94+ const execSyncFn = runtime . execSync || execSync ;
95+ const systemRoot = env . SystemRoot || env . SYSTEMROOT || 'C:\\Windows' ;
96+ const shells = [ ] ;
97+
98+ // Windows PowerShell (built-in)
99+ const winPowerShell = path . win32 . join ( systemRoot , 'System32' , 'WindowsPowerShell' , 'v1.0' , 'powershell.exe' ) ;
100+ if ( fileExists ( winPowerShell , fsModule ) ) {
101+ shells . push ( { name : 'Windows PowerShell' , path : winPowerShell , args : [ ] } ) ;
102+ }
103+
104+ // Command Prompt
105+ const cmdPath = env . ComSpec || env . COMSPEC || path . win32 . join ( systemRoot , 'System32' , 'cmd.exe' ) ;
106+ if ( fileExists ( cmdPath , fsModule ) ) {
107+ shells . push ( { name : 'Command Prompt' , path : cmdPath , args : [ ] } ) ;
108+ }
109+
110+ // PowerShell Core (pwsh) — scan Program Files
111+ const pwshDirs = [
112+ path . win32 . join ( env . ProgramFiles || 'C:\\Program Files' , 'PowerShell' ) ,
113+ path . win32 . join ( env [ 'ProgramFiles(x86)' ] || 'C:\\Program Files (x86)' , 'PowerShell' ) ,
114+ ] ;
115+ for ( const dir of pwshDirs ) {
116+ try {
117+ const versions = fsModule . readdirSync ( dir ) . sort ( ) . reverse ( ) ;
118+ for ( const ver of versions ) {
119+ const pwshPath = path . win32 . join ( dir , ver , 'pwsh.exe' ) ;
120+ if ( fileExists ( pwshPath , fsModule ) ) {
121+ shells . push ( { name : 'PowerShell' , path : pwshPath , args : [ ] } ) ;
122+ break ; // only add the newest version
123+ }
124+ }
125+ if ( shells . some ( ( s ) => s . name === 'PowerShell' ) ) break ;
126+ } catch { /* dir doesn't exist */ }
127+ }
128+
129+ // WSL distributions
130+ try {
131+ const wslExe = path . win32 . join ( systemRoot , 'System32' , 'wsl.exe' ) ;
132+ if ( fileExists ( wslExe , fsModule ) ) {
133+ const raw = execSyncFn ( `"${ wslExe } " -l -q` , {
134+ encoding : 'utf-16le' ,
135+ stdio : [ 'ignore' , 'pipe' , 'ignore' ] ,
136+ timeout : 5000 ,
137+ } ) ;
138+ const distros = raw . split ( / \r ? \n / )
139+ . map ( ( line ) => line . replace ( / \0 / g, '' ) . trim ( ) )
140+ . filter ( Boolean ) ;
141+ for ( const distro of distros ) {
142+ shells . push ( { name : distro , path : wslExe , args : [ '-d' , distro ] } ) ;
143+ }
144+ }
145+ } catch { /* WSL not installed or no distros */ }
146+
147+ // Git Bash
148+ const gitBashPaths = [
149+ path . win32 . join ( env . ProgramFiles || 'C:\\Program Files' , 'Git' , 'bin' , 'bash.exe' ) ,
150+ path . win32 . join ( env [ 'ProgramFiles(x86)' ] || 'C:\\Program Files (x86)' , 'Git' , 'bin' , 'bash.exe' ) ,
151+ ] ;
152+ for ( const gbPath of gitBashPaths ) {
153+ if ( fileExists ( gbPath , fsModule ) ) {
154+ shells . push ( { name : 'Git Bash' , path : gbPath , args : [ '--login' , '-i' ] } ) ;
155+ break ;
156+ }
157+ }
158+
159+ // Visual Studio Developer shells
160+ const vsBasePaths = [
161+ env . ProgramFiles || 'C:\\Program Files' ,
162+ env [ 'ProgramFiles(x86)' ] || 'C:\\Program Files (x86)' ,
163+ ] ;
164+ for ( const base of vsBasePaths ) {
165+ const vsRoot = path . win32 . join ( base , 'Microsoft Visual Studio' ) ;
166+ let years ;
167+ try { years = fsModule . readdirSync ( vsRoot ) ; } catch { continue ; }
168+ for ( const year of years . sort ( ) . reverse ( ) ) {
169+ let editions ;
170+ try { editions = fsModule . readdirSync ( path . win32 . join ( vsRoot , year ) ) ; } catch { continue ; }
171+ for ( const edition of editions ) {
172+ const toolsDir = path . win32 . join ( vsRoot , year , edition , 'Common7' , 'Tools' ) ;
173+
174+ // Developer Command Prompt
175+ const vsDevCmd = path . win32 . join ( toolsDir , 'VsDevCmd.bat' ) ;
176+ if ( fileExists ( vsDevCmd , fsModule ) && fileExists ( cmdPath , fsModule ) ) {
177+ shells . push ( {
178+ name : `Developer Command Prompt for VS ${ year } ` ,
179+ path : cmdPath ,
180+ args : [ '/k' , vsDevCmd ] ,
181+ } ) ;
182+ }
183+
184+ // Developer PowerShell
185+ const launchScript = path . win32 . join ( toolsDir , 'Launch-VsDevShell.ps1' ) ;
186+ if ( fileExists ( launchScript , fsModule ) && fileExists ( winPowerShell , fsModule ) ) {
187+ shells . push ( {
188+ name : `Developer PowerShell for VS ${ year } ` ,
189+ path : winPowerShell ,
190+ args : [ '-NoExit' , '-Command' , `& { Import-Module "${ launchScript } " }` ] ,
191+ } ) ;
192+ }
193+ }
194+ }
195+ }
196+
197+ return shells ;
198+ }
199+
200+ function detectAvailableShells ( runtime = { } ) {
201+ const platform = runtime . platform || process . platform ;
202+ if ( platform === 'win32' ) {
203+ return detectWindowsShells ( runtime ) ;
204+ }
205+
206+ // macOS / Linux: return $SHELL or /bin/sh
207+ const env = runtime . env || process . env ;
208+ const shellPath = env . SHELL || '/bin/sh' ;
209+ const name = path . posix . basename ( shellPath ) ;
210+ return [ { name, path : shellPath , args : [ ] } ] ;
211+ }
212+
213+ module . exports . detectAvailableShells = detectAvailableShells ;
214+
80215function parseCwdFromLsof ( output , pid ) {
81216 const lines = output . split ( / \r ? \n / ) ;
82217 let inTargetProcess = false ;
@@ -173,7 +308,7 @@ module.exports.create = function create(send, ptyModule) {
173308
174309 let p ;
175310 try {
176- p = pty . spawn ( config . shell , config . loginArg , {
311+ p = pty . spawn ( config . shell , config . shellArgs , {
177312 name : 'xterm-256color' ,
178313 cols : config . cols ,
179314 rows : config . rows ,
@@ -267,5 +402,9 @@ module.exports.create = function create(send, ptyModule) {
267402 } , timeout ) ;
268403 }
269404
270- return { spawn, write, resize, kill, killAll, list, getCwd, getScrollback, gracefulKillAll } ;
405+ function getShells ( requestId ) {
406+ send ( 'shells' , { shells : detectAvailableShells ( ) , requestId } ) ;
407+ }
408+
409+ return { spawn, write, resize, kill, killAll, list, getCwd, getScrollback, gracefulKillAll, getShells } ;
271410} ;
0 commit comments