@@ -19,18 +19,35 @@ import path from 'node:path'
1919
2020import { parseTarGzip } from 'nanotar'
2121
22+ // Platform detection
23+ const WIN32 = process . platform === 'win32'
24+
2225// Configurable constants with environment variable overrides.
2326// os.homedir() can throw if no home directory is available.
2427let SOCKET_HOME : string
25- try {
26- SOCKET_HOME = process . env [ 'SOCKET_HOME' ] || path . join ( os . homedir ( ) , '.socket' )
27- } catch ( error ) {
28- console . error (
29- 'Fatal: Unable to determine home directory. Set SOCKET_HOME environment variable.' ,
30- )
31- console . error ( `Error: ${ formatError ( error ) } ` )
32- // eslint-disable-next-line n/no-process-exit
33- process . exit ( 1 )
28+ if ( process . env [ 'SOCKET_HOME' ] ) {
29+ SOCKET_HOME = process . env [ 'SOCKET_HOME' ]
30+ } else {
31+ // Try multiple fallbacks to determine home directory
32+ let homeDir : string | undefined
33+ try {
34+ homeDir = os . homedir ( )
35+ } catch {
36+ // os.homedir() can throw in some environments
37+ }
38+
39+ // Fallback to environment variables
40+ if ( ! homeDir ) {
41+ homeDir = process . env [ 'HOME' ] || process . env [ 'USERPROFILE' ]
42+ }
43+
44+ // Last resort: use temp directory
45+ if ( ! homeDir ) {
46+ homeDir = path . join ( os . tmpdir ( ) , '.socket-cli-home' )
47+ console . warn ( `Warning: Using temporary directory as home: ${ homeDir } ` )
48+ }
49+
50+ SOCKET_HOME = path . join ( homeDir , '.socket' )
3451}
3552const CLI_INSTALL_LOCK_FILE_NAME = '.install.lock'
3653const DOWNLOAD_MESSAGE_DELAY_MS = 2_000
@@ -49,7 +66,18 @@ const SOCKET_CLI_PACKAGE =
4966 process . env [ 'SOCKET_CLI_PACKAGE' ] || '@socketsecurity/cli'
5067const SOCKET_CLI_PACKAGE_JSON = path . join ( SOCKET_CLI_PACKAGE_DIR , 'package.json' )
5168// Minimum Node.js version for system Node (v22 = Active LTS)
52- const MIN_NODE_VERSION = parseInt ( process . env [ 'MIN_NODE_VERSION' ] || '22' , 10 )
69+ // Parse MIN_NODE_VERSION with validation
70+ const MIN_NODE_VERSION = ( ( ) => {
71+ const envValue = process . env [ 'MIN_NODE_VERSION' ]
72+ if ( envValue ) {
73+ const parsed = parseInt ( envValue , 10 )
74+ if ( ! isNaN ( parsed ) && parsed > 0 && parsed < 100 ) {
75+ return parsed
76+ }
77+ console . warn ( `Warning: Invalid MIN_NODE_VERSION "${ envValue } ", using default 22` )
78+ }
79+ return 22
80+ } ) ( )
5381
5482// ============================================================================
5583// Helper utilities
@@ -125,18 +153,30 @@ async function remove(
125153 }
126154 }
127155
128- // Perform deletion.
156+ // Perform deletion with retry logic for Windows file locks .
129157 try {
130158 const stats = await fs . stat ( absolutePath )
131159 if ( stats . isDirectory ( ) ) {
132- await fs . rm ( absolutePath , { recursive : true , force : true } )
160+ // More retries for directories
161+ await retryWithBackoff (
162+ ( ) => fs . rm ( absolutePath , { recursive : true , force : true } ) ,
163+ 3 ,
164+ 200 ,
165+ 2 ,
166+ )
133167 } else {
134- await fs . unlink ( absolutePath )
168+ await retryWithBackoff ( ( ) => fs . unlink ( absolutePath ) , 2 , 200 , 2 )
135169 }
136170 } catch ( error ) {
137171 const code = ( error as NodeJS . ErrnoException ) ?. code
138172 // Silently ignore if file doesn't exist.
139173 if ( code !== 'ENOENT' ) {
174+ // Check if it's a permission error on Windows
175+ if ( code === 'EPERM' && WIN32 ) {
176+ debugLog (
177+ `Warning: Unable to delete ${ absolutePath } (Windows EPERM). File may be locked by antivirus or another process.` ,
178+ )
179+ }
140180 throw error
141181 }
142182 }
@@ -169,15 +209,26 @@ async function acquireLock(): Promise<string> {
169209 const lockContent = await fs . readFile ( lockPath , 'utf8' )
170210 const lockPid = Number . parseInt ( lockContent . trim ( ) , 10 )
171211 if ( ! Number . isNaN ( lockPid ) ) {
172- // Try to check if process still exists (Unix-like systems).
173- // On Windows this will always succeed, but that's okay - timeout will handle it.
212+ // Try to check if process still exists.
174213 try {
175214 process . kill ( lockPid , 0 )
176215 // Process exists, wait and retry.
177- } catch {
178- // Process doesn't exist, remove stale lock.
179- await remove ( lockPath )
180- continue
216+ } catch ( killError : any ) {
217+ // On Windows, EPERM means the process exists but we don't have permission.
218+ // On Unix, ESRCH means the process doesn't exist.
219+ if ( killError . code === 'ESRCH' ) {
220+ // Process doesn't exist, remove stale lock.
221+ await remove ( lockPath )
222+ continue
223+ } else if ( killError . code === 'EPERM' && WIN32 ) {
224+ // On Windows, EPERM could mean process exists but owned by another user.
225+ // We'll continue waiting as if the process exists.
226+ debugLog ( 'Lock file process may exist (Windows EPERM), waiting...' )
227+ } else {
228+ // Process doesn't exist or other error, try to remove stale lock.
229+ await remove ( lockPath )
230+ continue
231+ }
181232 }
182233 }
183234 } catch {
@@ -618,12 +669,16 @@ async function retryWithBackoff<T>(
618669 return await fn ( )
619670 } catch ( error ) {
620671 lastError = error
621- // Only retry on transient errors (EBUSY, EMFILE, ENFILE).
672+ // Only retry on transient errors (EBUSY, EMFILE, ENFILE, EPERM on Windows ).
622673 const code = ( error as NodeJS . ErrnoException ) ?. code
623- if (
624- attempt < maxRetries &&
625- ( code === 'EBUSY' || code === 'EMFILE' || code === 'ENFILE' )
626- ) {
674+ const shouldRetry =
675+ code === 'EBUSY' ||
676+ code === 'EMFILE' ||
677+ code === 'ENFILE' ||
678+ ( code === 'EPERM' && WIN32 )
679+
680+ if ( attempt < maxRetries && shouldRetry ) {
681+ debugLog ( `Retrying after ${ code } error (attempt ${ attempt + 1 } /${ maxRetries } )` )
627682 await new Promise ( resolve => setTimeout ( resolve , delay ) )
628683 delay *= backoffFactor
629684 continue
0 commit comments