11import process from 'node:process' ;
2- import { Buffer } from 'node:buffer' ;
32import path from 'node:path' ;
43import { fileURLToPath } from 'node:url' ;
5- import { promisify } from 'node:util' ;
64import childProcess from 'node:child_process' ;
75import fs , { constants as fsConstants } from 'node:fs/promises' ;
8- import { isWsl , powerShellPath , convertWslPathToWindows } from 'wsl-utils' ;
6+ import {
7+ isWsl ,
8+ powerShellPath ,
9+ convertWslPathToWindows ,
10+ canAccessPowerShell ,
11+ wslDefaultBrowser ,
12+ } from 'wsl-utils' ;
13+ import { executePowerShell } from 'powershell-utils' ;
914import defineLazyProperty from 'define-lazy-prop' ;
10- import defaultBrowser from 'default-browser' ;
15+ import defaultBrowser , { _windowsBrowserProgIdMap } from 'default-browser' ;
1116import isInsideContainer from 'is-inside-container' ;
1217import isInSsh from 'is-in-ssh' ;
1318
14- const execFile = promisify ( childProcess . execFile ) ;
1519const fallbackAttemptSymbol = Symbol ( 'fallbackAttempt' ) ;
1620
1721// Path to included `xdg-open`.
@@ -20,77 +24,6 @@ const localXdgOpenPath = path.join(__dirname, 'xdg-open');
2024
2125const { platform, arch} = process ;
2226
23- /**
24- Escape value for PowerShell. Single-quoted strings are literal (no variable expansion or escape sequences), unlike double-quoted strings. Escapes single quotes by doubling (handles already-doubled quotes correctly).
25- */
26- const escapeForPowerShell = value => `'${ String ( value ) . replaceAll ( '\'' , '\'\'' ) } '` ;
27-
28- /**
29- Cached promise for PowerShell accessibility check.
30- We only need to check once per process since PowerShell availability doesn't change at runtime.
31- Caching the promise (rather than the result) ensures concurrent calls don't trigger duplicate checks.
32- */
33- let powerShellAccessiblePromise ;
34-
35- /**
36- Check if PowerShell is accessible in WSL.
37- This is used to determine whether to use Windows integration (PowerShell) or fall back to Linux behavior (xdg-open).
38- In sandboxed WSL environments where Windows access is restricted, PowerShell may not be accessible,
39- and we should automatically use native Linux tools instead.
40-
41- @returns {Promise<boolean> } True if PowerShell is accessible, false otherwise.
42- */
43- async function isPowerShellAccessible ( ) {
44- powerShellAccessiblePromise ??= ( async ( ) => {
45- try {
46- const psPath = await powerShellPath ( ) ;
47- await fs . access ( psPath , fsConstants . X_OK ) ;
48- return true ;
49- } catch {
50- // PowerShell is not accessible (either doesn't exist, no execute permission, or other error)
51- return false ;
52- }
53- } ) ( ) ;
54-
55- return powerShellAccessiblePromise ;
56- }
57-
58- /**
59- Get the default browser name in Windows from WSL.
60-
61- @returns {Promise<string> } Browser name.
62- */
63- async function getWindowsDefaultBrowserFromWsl ( ) {
64- const powershellPath = await powerShellPath ( ) ;
65- const rawCommand = String . raw `(Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice").ProgId` ;
66- const encodedCommand = Buffer . from ( rawCommand , 'utf16le' ) . toString ( 'base64' ) ;
67-
68- const { stdout} = await execFile (
69- powershellPath ,
70- [
71- '-NoProfile' ,
72- '-NonInteractive' ,
73- '-ExecutionPolicy' ,
74- 'Bypass' ,
75- '-EncodedCommand' ,
76- encodedCommand ,
77- ] ,
78- { encoding : 'utf8' } ,
79- ) ;
80-
81- const progId = stdout . trim ( ) ;
82-
83- // Map ProgId to browser IDs
84- const browserMap = {
85- ChromeHTML : 'com.google.chrome' ,
86- BraveHTML : 'com.brave.Browser' ,
87- MSEdgeHTM : 'com.microsoft.edge' ,
88- FirefoxURL : 'org.mozilla.firefox' ,
89- } ;
90-
91- return browserMap [ progId ] ? { id : browserMap [ progId ] } : { } ;
92- }
93-
9427const tryEachApp = async ( apps , opener ) => {
9528 if ( apps . length === 0 ) {
9629 // No app was provided
@@ -146,18 +79,19 @@ const baseOpen = async options => {
14679 }
14780
14881 if ( app === 'browser' || app === 'browserPrivate' ) {
149- // IDs from default-browser for macOS and windows are the same
82+ // IDs from default-browser for macOS and windows are the same.
83+ // IDs are lowercased to increase chances of a match.
15084 const ids = {
15185 'com.google.chrome' : 'chrome' ,
15286 'google-chrome.desktop' : 'chrome' ,
153- 'com.brave.Browser ' : 'brave' ,
87+ 'com.brave.browser ' : 'brave' ,
15488 'org.mozilla.firefox' : 'firefox' ,
15589 'firefox.desktop' : 'firefox' ,
15690 'com.microsoft.msedge' : 'edge' ,
15791 'com.microsoft.edge' : 'edge' ,
15892 'com.microsoft.edgemac' : 'edge' ,
15993 'microsoft-edge.desktop' : 'edge' ,
160- 'com.apple.Safari ' : 'safari' ,
94+ 'com.apple.safari ' : 'safari' ,
16195 } ;
16296
16397 // Incognito flags for each browser in `apps`.
@@ -169,9 +103,17 @@ const baseOpen = async options => {
169103 // Safari doesn't support private mode via command line
170104 } ;
171105
172- const browser = isWsl ? await getWindowsDefaultBrowserFromWsl ( ) : await defaultBrowser ( ) ;
106+ let browser ;
107+ if ( isWsl ) {
108+ const progId = await wslDefaultBrowser ( ) ;
109+ const browserInfo = _windowsBrowserProgIdMap . get ( progId ) ;
110+ browser = browserInfo ?? { } ;
111+ } else {
112+ browser = await defaultBrowser ( ) ;
113+ }
114+
173115 if ( browser . id in ids ) {
174- const browserName = ids [ browser . id ] ;
116+ const browserName = ids [ browser . id . toLowerCase ( ) ] ;
175117
176118 if ( app === 'browserPrivate' ) {
177119 // Safari doesn't support private mode via command line
@@ -203,7 +145,7 @@ const baseOpen = async options => {
203145 // This allows the package to work in sandboxed WSL environments where Windows access is restricted.
204146 let shouldUseWindowsInWsl = false ;
205147 if ( isWsl && ! isInsideContainer ( ) && ! isInSsh && ! app ) {
206- shouldUseWindowsInWsl = await isPowerShellAccessible ( ) ;
148+ shouldUseWindowsInWsl = await canAccessPowerShell ( ) ;
207149 }
208150
209151 if ( platform === 'darwin' ) {
@@ -227,13 +169,7 @@ const baseOpen = async options => {
227169 } else if ( platform === 'win32' || shouldUseWindowsInWsl ) {
228170 command = await powerShellPath ( ) ;
229171
230- cliArguments . push (
231- '-NoProfile' ,
232- '-NonInteractive' ,
233- '-ExecutionPolicy' ,
234- 'Bypass' ,
235- '-EncodedCommand' ,
236- ) ;
172+ cliArguments . push ( ...executePowerShell . argumentsPrefix ) ;
237173
238174 if ( ! isWsl ) {
239175 childProcessOptions . windowsVerbatimArguments = true ;
@@ -252,21 +188,21 @@ const baseOpen = async options => {
252188 }
253189
254190 if ( app ) {
255- encodedArguments . push ( escapeForPowerShell ( app ) ) ;
191+ encodedArguments . push ( executePowerShell . escapeArgument ( app ) ) ;
256192 if ( options . target ) {
257193 appArguments . push ( options . target ) ;
258194 }
259195 } else if ( options . target ) {
260- encodedArguments . push ( escapeForPowerShell ( options . target ) ) ;
196+ encodedArguments . push ( executePowerShell . escapeArgument ( options . target ) ) ;
261197 }
262198
263199 if ( appArguments . length > 0 ) {
264- appArguments = appArguments . map ( argument => escapeForPowerShell ( argument ) ) ;
200+ appArguments = appArguments . map ( argument => executePowerShell . escapeArgument ( argument ) ) ;
265201 encodedArguments . push ( '-ArgumentList' , appArguments . join ( ',' ) ) ;
266202 }
267203
268204 // Using Base64-encoded command, accepted by PowerShell, to allow special characters.
269- options . target = Buffer . from ( encodedArguments . join ( ' ' ) , 'utf16le' ) . toString ( 'base64' ) ;
205+ options . target = executePowerShell . encodeCommand ( encodedArguments . join ( ' ' ) ) ;
270206
271207 if ( ! options . wait ) {
272208 // PowerShell will keep the parent process alive unless stdio is ignored.
0 commit comments