66 * This is now JS/Bun-only.
77 */
88
9- import { join } from "node:path" ;
9+ import { join , resolve } from "node:path" ;
1010import { scan } from "../src/scanner.ts" ;
1111
1212const args = Bun . argv . slice ( 2 ) ;
@@ -34,20 +34,46 @@ function shouldSkip(p: string): boolean {
3434 ) ;
3535}
3636
37+ function shellQuote ( value : string ) : string {
38+ return `'${ value . replaceAll ( "'" , "'\\''" ) } '` ;
39+ }
40+
3741/** Convert a Windows absolute path to a WSL /mnt/... path. */
3842function toWslPath ( winPath : string ) : string {
39- // e.g. C:\Users\foo → /mnt/c/Users/foo
40- return winPath
41- . replace ( / ^ ( [ A - Z a - z ] ) : \\ / , ( _ , drive ) => `/mnt/${ drive . toLowerCase ( ) } /` )
42- . replaceAll ( "\\" , "/" ) ;
43+ const normalizedPath = resolve ( winPath ) . replaceAll ( "\\" , "/" ) ;
44+ const driveMatch = / ^ ( [ A - Z a - z ] ) : \/ ( .* ) $ / . exec ( normalizedPath ) ;
45+ if ( ! driveMatch ?. [ 1 ] ) {
46+ return normalizedPath ;
47+ }
48+
49+ const drive = driveMatch [ 1 ] . toLowerCase ( ) ;
50+ const rest = driveMatch [ 2 ] ?? "" ;
51+ return `/mnt/${ drive } /${ rest } ` ;
52+ }
53+
54+ async function hasWslNpkill ( ) : Promise < boolean > {
55+ const probe = Bun . spawn ( {
56+ cmd : [
57+ "wsl" ,
58+ "bash" ,
59+ "-lc" ,
60+ "command -v script >/dev/null && command -v npkill >/dev/null" ,
61+ ] ,
62+ stdout : "ignore" ,
63+ stderr : "ignore" ,
64+ } ) ;
65+
66+ return await probe . exited === 0 ;
4367}
4468
4569async function npkillScan ( dir : string , useWsl = false ) : Promise < number > {
46- // Use login shell so node/npkill are in PATH inside WSL.
47- // Only silence stderr (2>/dev/null) so stdout flows to proc.stdout for parsing.
48- // Terminal corruption from npkill's \r sequences is handled by printRow's \r\x1b[2K.
70+ if ( useWsl && ! ( await hasWslNpkill ( ) ) ) {
71+ return - 1 ;
72+ }
73+
74+ const wslCommand = `npkill -d ${ shellQuote ( toWslPath ( dir ) ) } -nu --hide-errors` ;
4975 const scriptCmd = useWsl
50- ? [ "wsl" , "bash" , "-lc" , `script -q /dev/null -c 'npkill -d " ${ toWslPath ( dir ) } " -nu --hide-errors' 2>/dev/null` ]
76+ ? [ "wsl" , "bash" , "-lc" , `script -q /dev/null -c ${ shellQuote ( wslCommand ) } 2>/dev/null` ]
5177 : [ "script" , "-q" , "/dev/null" , "npkill" , "-d" , dir , "-nu" , "--hide-errors" ] ;
5278
5379 try {
@@ -76,6 +102,10 @@ async function npkillScan(dir: string, useWsl = false): Promise<number> {
76102 clearTimeout ( killTimer ) ;
77103 try { proc . kill ( ) ; } catch { }
78104
105+ if ( await proc . exited !== 0 ) {
106+ return - 1 ;
107+ }
108+
79109 const raw = Buffer . concat ( chunks ) . toString ( "utf8" ) ;
80110 const clean = raw . replace ( / \x1b \[ [ 0 - 9 ; ] * [ a - z A - Z ] / g, "" ) . replace ( / \r / g, "" ) ;
81111
@@ -99,22 +129,23 @@ async function bunGlobScan(dir: string): Promise<number> {
99129 onlyFiles : false ,
100130 followSymlinks : false ,
101131 } ) ) {
102- // Normalize to forward slashes for consistent checks
103132 const normalized = relative . replaceAll ( "\\" , "/" ) ;
104133 const fullPath = join ( dir , relative ) ;
105134 const fullNormalized = fullPath . replaceAll ( "\\" , "/" ) ;
106135
107- // Skip nested node_modules (same as bunkill)
108- if ( fullNormalized . includes ( "/node_modules/" ) ) continue ;
136+ const targetSegments = normalized . split ( "/" )
137+ . filter ( ( segment ) => segment === "node_modules" ) ;
138+ if ( targetSegments . length > 1 ) continue ;
109139 if ( shouldSkip ( fullNormalized ) ) continue ;
110140
111141 const depth = normalized . split ( "/" ) . filter ( Boolean ) . length ;
112142 if ( depth > MAX_DEPTH ) continue ;
113143
114- // Require a package.json in the parent directory (same as bunkill)
115- const parentPath = fullPath . slice ( 0 , fullPath . length - "/node_modules" . length ) ;
116- const pkgJson = Bun . file ( join ( parentPath , "package.json" ) ) ;
117- if ( ! ( await pkgJson . exists ( ) ) ) continue ;
144+ const projectPath = join ( fullPath , ".." ) ;
145+ const packageJson = await Bun . file ( join ( projectPath , "package.json" ) )
146+ . json ( )
147+ . catch ( ( ) => null ) ;
148+ if ( ! packageJson ) continue ;
118149
119150 results . push ( fullPath ) ;
120151 }
@@ -219,17 +250,19 @@ const npkillPath = isWindows
219250 ? await Bun . $ . nothrow ( ) `where npkill` . text ( ) . then ( ( s ) => s . trim ( ) . split ( "\n" ) [ 0 ] ?. trim ( ) ?? "" ) . catch ( ( ) => "" )
220251 : await Bun . $ . nothrow ( ) `which npkill` . text ( ) . then ( ( s ) => s . trim ( ) ) . catch ( ( ) => "" ) ;
221252
222- // Detect WSL (Windows only)
223253const wslAvailable = isWindows
224254 ? await Bun . $ . nothrow ( ) `where wsl` . text ( ) . then ( ( s ) => s . trim ( ) . length > 0 ) . catch ( ( ) => false )
225255 : false ;
256+ const wslNpkillAvailable = isWindows && wslAvailable
257+ ? await hasWslNpkill ( )
258+ : false ;
226259
227260const npkillStatus = ! npkillPath
228261 ? "NOT FOUND (npm install -g npkill)"
229- : isWindows && wslAvailable
262+ : isWindows && wslNpkillAvailable
230263 ? `${ npkillPath } (via WSL)`
231264 : isWindows
232- ? `${ npkillPath } (skipped: WSL not found — install WSL to enable )`
265+ ? `${ npkillPath } (skipped: WSL npkill/script unavailable )`
233266 : npkillPath ;
234267
235268console . log ( ` npkill : ${ npkillStatus } ` ) ;
@@ -244,13 +277,13 @@ const [globResult, bunkillResult] = await Promise.all([
244277] ) ;
245278
246279// On Windows: run npkill via WSL if available; on Unix: run directly
247- const canRunNpkill = npkillPath && ( ! isWindows || wslAvailable ) ;
280+ const canRunNpkill = npkillPath && ( ! isWindows || wslNpkillAvailable ) ;
248281const npkillResult = canRunNpkill
249282 ? await bench ( "npkill" , ( d ) => npkillScan ( d , isWindows && wslAvailable ) , SCAN_DIR , 1 )
250283 : {
251284 label : "npkill" ,
252285 avgMs : 0 , minMs : 0 , maxMs : 0 , found : - 1 , available : false ,
253- note : ! npkillPath ? "not installed" : "WSL not found (install WSL to enable) " ,
286+ note : ! npkillPath ? "not installed" : "WSL npkill/script unavailable " ,
254287 } ;
255288
256289const available = [ npkillResult , globResult , bunkillResult ] . filter ( ( r ) => r . available ) ;
0 commit comments