1- import { useCallback , useEffect , useState } from 'react'
1+ import { readdir , stat } from 'node:fs/promises'
2+ import { delimiter , join } from 'node:path'
3+ import { useEffect , useState } from 'react'
24
35import { debug as debugLogger } from '#core/utils/debugLogger'
46import { logError } from '#core/utils/log'
@@ -7,61 +9,114 @@ import {
79 getMinimalFallbackCommands ,
810} from '#cli-utils/completion/commonUnixCommands'
911
12+ const COMMAND_SCAN_BATCH_SIZE = 64
13+ type CommandDirent = {
14+ name : string | Buffer
15+ isFile ( ) : boolean
16+ isSymbolicLink ( ) : boolean
17+ }
18+
19+ async function yieldToEventLoop ( ) : Promise < void > {
20+ await new Promise ( resolve => setTimeout ( resolve , 0 ) )
21+ }
22+
23+ async function loadCommandsFromPath (
24+ pathValue : string ,
25+ shouldStop : ( ) => boolean = ( ) => false ,
26+ ) : Promise < string [ ] > {
27+ const pathDirs = Array . from (
28+ new Set (
29+ pathValue
30+ . split ( delimiter )
31+ . map ( dir => dir . trim ( ) )
32+ . filter ( Boolean ) ,
33+ ) ,
34+ )
35+ const commandSet = new Set < string > ( getEssentialCommands ( ) )
36+
37+ for ( const dir of pathDirs ) {
38+ if ( shouldStop ( ) ) break
39+
40+ let entries : CommandDirent [ ]
41+ try {
42+ entries = ( await readdir ( dir , {
43+ withFileTypes : true ,
44+ } ) ) as CommandDirent [ ]
45+ } catch {
46+ continue
47+ }
48+
49+ for ( let i = 0 ; i < entries . length ; i += COMMAND_SCAN_BATCH_SIZE ) {
50+ if ( shouldStop ( ) ) break
51+
52+ const batch = entries . slice ( i , i + COMMAND_SCAN_BATCH_SIZE )
53+ const commandNames = await Promise . all (
54+ batch . map ( async entry => {
55+ try {
56+ if ( ! entry . isFile ( ) && ! entry . isSymbolicLink ( ) ) return null
57+
58+ const entryName = String ( entry . name )
59+ const fullPath = join ( dir , entryName )
60+ const stats = await stat ( fullPath )
61+ const isExecutable =
62+ process . platform === 'win32' || ( stats . mode & 0o111 ) !== 0
63+ return stats . isFile ( ) && isExecutable ? entryName : null
64+ } catch {
65+ return null
66+ }
67+ } ) ,
68+ )
69+
70+ for ( const commandName of commandNames ) {
71+ if ( commandName ) commandSet . add ( commandName )
72+ }
73+
74+ await yieldToEventLoop ( )
75+ }
76+ }
77+
78+ return Array . from ( commandSet ) . sort ( )
79+ }
80+
1081export function useSystemCommands ( ) : {
1182 systemCommands : string [ ]
1283 isLoadingCommands : boolean
1384} {
14- const [ systemCommands , setSystemCommands ] = useState < string [ ] > ( [ ] )
85+ const [ systemCommands , setSystemCommands ] = useState < string [ ] > ( ( ) =>
86+ getEssentialCommands ( ) ,
87+ )
1588 const [ isLoadingCommands , setIsLoadingCommands ] = useState ( false )
1689
17- const loadSystemCommands = useCallback ( async ( ) => {
18- if ( systemCommands . length > 0 || isLoadingCommands ) return
90+ useEffect ( ( ) => {
91+ let cancelled = false
92+ const shouldStop = ( ) => cancelled
1993
20- setIsLoadingCommands ( true )
21- try {
22- const { readdirSync, statSync } = await import ( 'fs' )
23- const pathDirs = ( process . env . PATH || '' ) . split ( ':' ) . filter ( Boolean )
24- const commandSet = new Set < string > ( )
25-
26- getEssentialCommands ( ) . forEach ( cmd => commandSet . add ( cmd ) )
27-
28- for ( const dir of pathDirs ) {
29- try {
30- if ( readdirSync && statSync ) {
31- const entries = readdirSync ( dir )
32- for ( const entry of entries ) {
33- try {
34- const fullPath = `${ dir } /${ entry } `
35- const stats = statSync ( fullPath )
36- if ( stats . isFile ( ) && ( stats . mode & 0o111 ) !== 0 ) {
37- commandSet . add ( entry )
38- }
39- } catch {
40- // Skip files we can't stat
41- }
42- }
43- }
44- } catch {
45- // Skip directories we can't read
46- }
94+ async function loadSystemCommands ( ) : Promise < void > {
95+ setIsLoadingCommands ( true )
96+ try {
97+ const next = await loadCommandsFromPath (
98+ process . env . PATH || '' ,
99+ shouldStop ,
100+ )
101+ if ( ! shouldStop ( ) ) setSystemCommands ( next )
102+ } catch ( error ) {
103+ logError ( error )
104+ debugLogger . warn ( 'UNIFIED_COMPLETION_SYSTEM_COMMANDS_LOAD_FAILED' , {
105+ error : error instanceof Error ? error . message : String ( error ) ,
106+ } )
107+ if ( ! shouldStop ( ) ) setSystemCommands ( getMinimalFallbackCommands ( ) )
108+ } finally {
109+ if ( ! shouldStop ( ) ) setIsLoadingCommands ( false )
47110 }
48-
49- const next = Array . from ( commandSet ) . sort ( )
50- setSystemCommands ( next )
51- } catch ( error ) {
52- logError ( error )
53- debugLogger . warn ( 'UNIFIED_COMPLETION_SYSTEM_COMMANDS_LOAD_FAILED' , {
54- error : error instanceof Error ? error . message : String ( error ) ,
55- } )
56- setSystemCommands ( getMinimalFallbackCommands ( ) )
57- } finally {
58- setIsLoadingCommands ( false )
59111 }
60- } , [ systemCommands . length , isLoadingCommands ] )
61112
62- useEffect ( ( ) => {
63- loadSystemCommands ( )
64- } , [ loadSystemCommands ] )
113+ void loadSystemCommands ( )
114+ return ( ) => {
115+ cancelled = true
116+ }
117+ } , [ ] )
65118
66119 return { systemCommands, isLoadingCommands }
67120}
121+
122+ export const __loadCommandsFromPathForTests = loadCommandsFromPath
0 commit comments