1111
1212import argparse
1313import contextlib
14+ import glob as glob_module
1415import json
1516import os
1617import platform
@@ -363,8 +364,21 @@ def find_command_robust(cmd: str, fallback_paths: list[str] | None = None) -> st
363364 ]
364365 elif cmd == 'node' :
365366 common_paths = [
367+ # Official installer paths
366368 r'C:\Program Files\nodejs\node.exe' ,
367369 r'C:\Program Files (x86)\nodejs\node.exe' ,
370+ # nvm-windows: %APPDATA%\nvm\<version>\node.exe
371+ os .path .expandvars (r'%APPDATA%\nvm' ),
372+ # fnm: %LOCALAPPDATA%\fnm_multishells\<id>\node.exe
373+ os .path .expandvars (r'%LOCALAPPDATA%\fnm_multishells' ),
374+ # volta: %USERPROFILE%\.volta\bin\node.exe
375+ os .path .expandvars (r'%USERPROFILE%\.volta\bin\node.exe' ),
376+ # scoop: %USERPROFILE%\scoop\apps\nodejs\current\node.exe
377+ os .path .expandvars (r'%USERPROFILE%\scoop\apps\nodejs\current\node.exe' ),
378+ # scoop (alternative): %USERPROFILE%\scoop\shims\node.exe
379+ os .path .expandvars (r'%USERPROFILE%\scoop\shims\node.exe' ),
380+ # chocolatey: C:\ProgramData\chocolatey\bin\node.exe
381+ r'C:\ProgramData\chocolatey\bin\node.exe' ,
368382 ]
369383 elif cmd == 'npm' :
370384 common_paths = [
@@ -391,10 +405,25 @@ def find_command_robust(cmd: str, fallback_paths: list[str] | None = None) -> st
391405 ]
392406
393407 # Check common locations
408+
394409 for path in common_paths :
395410 expanded = os .path .expandvars (path )
396- if Path (expanded ).exists ():
397- return str (Path (expanded ).resolve ())
411+ expanded_path = Path (expanded )
412+
413+ # Direct file check
414+ if expanded_path .exists () and expanded_path .is_file ():
415+ return str (expanded_path .resolve ())
416+
417+ # Directory-based search for version managers (nvm, fnm)
418+ # These store node.exe in subdirectories like: nvm/<version>/node.exe
419+ if expanded_path .exists () and expanded_path .is_dir () and cmd == 'node' :
420+ # Search for node.exe in subdirectories (one level deep)
421+ pattern = str (expanded_path / '*' / 'node.exe' )
422+ matches = glob_module .glob (pattern )
423+ if matches :
424+ # Return the most recently modified (likely active version)
425+ matches .sort (key = lambda x : os .path .getmtime (x ), reverse = True )
426+ return str (Path (matches [0 ]).resolve ())
398427
399428 # Tertiary: Custom fallback paths
400429 if fallback_paths :
@@ -3045,63 +3074,56 @@ def install_claude(version: str | None = None) -> bool:
30453074def verify_nodejs_available () -> bool :
30463075 """Verify Node.js is available before MCP configuration.
30473076
3048- This function addresses the Windows 10+ PATH propagation bug where MSI
3049- installations update the registry but the changes don't propagate to
3050- running processes immediately. We explicitly check for Node.js and
3051- update the current process PATH if needed.
3077+ Uses shutil.which() to find Node.js in PATH, supporting all installation methods
3078+ (official installer, nvm, fnm, volta, scoop, chocolatey, etc.).
30523079
30533080 Returns:
30543081 True if Node.js is available, False otherwise.
30553082 """
30563083 if platform .system () != 'Windows' :
30573084 return True # Assume available on Unix
30583085
3059- nodejs_path = r'C:\Program Files\nodejs'
3060- node_exe = Path (nodejs_path ) / 'node.exe'
3061-
3062- # Check binary exists
3063- if not node_exe .exists ():
3064- error (f'Node.js binary not found at { node_exe } ' )
3065- return False
3066-
3067- # Check if node command works (3 attempts with 2s delay)
3068- for attempt in range (3 ):
3086+ # Primary: Use shutil.which for proper PATH-based detection
3087+ node_path = shutil .which ('node' )
3088+ if node_path :
3089+ # Verify node actually works
30693090 try :
3070- # Try with 'node' command
30713091 result = subprocess .run (
3072- ['node' , '--version' ],
3092+ [node_path , '--version' ],
30733093 capture_output = True ,
30743094 text = True ,
30753095 timeout = 5 ,
30763096 )
30773097 if result .returncode == 0 :
3078- success (f'Node.js verified: { result .stdout .strip ()} ' )
3098+ success (f'Node.js verified at : { node_path } ( { result .stdout .strip ()} ) ' )
30793099 return True
3080- except (FileNotFoundError , subprocess .TimeoutExpired ):
3081- # Try with full path
3082- try :
3083- result = subprocess .run (
3084- [str (node_exe ), '--version' ],
3085- capture_output = True ,
3086- text = True ,
3087- timeout = 5 ,
3088- )
3089- if result .returncode == 0 :
3090- # Works with full path, add to PATH
3091- current_path = os .environ .get ('PATH' , '' )
3092- if nodejs_path not in current_path :
3093- os .environ ['PATH' ] = f'{ nodejs_path } ;{ current_path } '
3094- info (f'Added { nodejs_path } to PATH' )
3095- success (f'Node.js verified: { result .stdout .strip ()} ' )
3096- return True
3097- except Exception :
3098- pass
3100+ except (subprocess .TimeoutExpired , OSError ):
3101+ pass
30993102
3100- if attempt < 2 :
3101- info (f'Node.js not ready, waiting 2s... (attempt { attempt + 1 } /3)' )
3102- time .sleep (2 )
3103+ # Secondary: Try find_command_robust with common installation paths
3104+ node_path = find_command_robust ('node' )
3105+ if node_path :
3106+ # Verify node actually works
3107+ try :
3108+ result = subprocess .run (
3109+ [node_path , '--version' ],
3110+ capture_output = True ,
3111+ text = True ,
3112+ timeout = 5 ,
3113+ )
3114+ if result .returncode == 0 :
3115+ # Add to PATH if not already there
3116+ node_dir = str (Path (node_path ).parent )
3117+ current_path = os .environ .get ('PATH' , '' )
3118+ if node_dir .lower () not in current_path .lower ():
3119+ os .environ ['PATH' ] = f'{ node_dir } ;{ current_path } '
3120+ info (f'Added { node_dir } to PATH' )
3121+ success (f'Node.js verified at: { node_path } ({ result .stdout .strip ()} )' )
3122+ return True
3123+ except (subprocess .TimeoutExpired , OSError ):
3124+ pass
31033125
3104- error ('Node.js is not available ' )
3126+ error ('Node.js not found in PATH ' )
31053127 return False
31063128
31073129
@@ -3408,8 +3430,10 @@ def create_additional_settings(
34083430 company_announcements: Optional list of company announcement strings
34093431 attribution: Optional dict with 'commit' and 'pr' keys for custom attribution strings.
34103432 Empty strings hide attribution. Takes precedence over include_co_authored_by.
3411- status_line: Optional dict with 'file' key for status line script path and optional
3412- 'padding' key. The file is downloaded to ~/.claude/hooks/ and configured in settings.
3433+ status_line: Optional dict with 'file' key for status line script path, optional
3434+ 'padding' key, and optional 'config' key for config file reference.
3435+ Both the script and config file are downloaded to ~/.claude/hooks/ and
3436+ the config path is appended as a command line argument.
34133437
34143438 Returns:
34153439 bool: True if successful, False otherwise.
@@ -3483,14 +3507,33 @@ def create_additional_settings(
34833507 hook_path = claude_user_dir / 'hooks' / filename
34843508 hook_path_str = hook_path .as_posix ()
34853509
3510+ # Extract optional config file reference
3511+ config = status_line .get ('config' )
3512+
34863513 # Determine command based on file extension
34873514 if filename .lower ().endswith (('.py' , '.pyw' )):
34883515 # Python script - use uv run
34893516 status_line_command = f'uv run --no-project --python 3.12 { hook_path_str } '
3517+
3518+ # Append config file path if specified
3519+ if config :
3520+ # Strip query parameters from config filename
3521+ clean_config = config .split ('?' )[0 ] if '?' in config else config
3522+ config_path = claude_user_dir / 'hooks' / Path (clean_config ).name
3523+ config_path_str = config_path .as_posix ()
3524+ status_line_command = f'{ status_line_command } { config_path_str } '
34903525 else :
34913526 # Other file - use path directly
34923527 status_line_command = hook_path_str
34933528
3529+ # Append config file path if specified
3530+ if config :
3531+ # Strip query parameters from config filename
3532+ clean_config = config .split ('?' )[0 ] if '?' in config else config
3533+ config_path = claude_user_dir / 'hooks' / Path (clean_config ).name
3534+ config_path_str = config_path .as_posix ()
3535+ status_line_command = f'{ status_line_command } { config_path_str } '
3536+
34943537 status_line_config : dict [str , Any ] = {
34953538 'type' : 'command' ,
34963539 'command' : status_line_command ,
@@ -4608,12 +4651,28 @@ def main() -> None:
46084651 # Step 11: Configure MCP servers
46094652 print ()
46104653 print (f'{ Colors .CYAN } Step 11: Configuring MCP servers...{ Colors .NC } ' )
4611- mcp_servers = config .get ('mcp-servers' , [])
4654+ mcp_servers_raw = config .get ('mcp-servers' , [])
4655+ # Convert to properly typed list for type safety
4656+ mcp_servers : list [dict [str , Any ]] = (
4657+ [cast (dict [str , Any ], s ) for s in cast (list [object ], mcp_servers_raw ) if isinstance (s , dict )]
4658+ if isinstance (mcp_servers_raw , list )
4659+ else []
4660+ )
4661+
4662+ # Refresh PATH from registry to pick up any installation changes
4663+ if platform .system () == 'Windows' :
4664+ refresh_path_from_registry ()
46124665
4613- # Verify Node.js is available before configuring MCP servers
4614- # This ensures Node.js PATH is properly set after MSI installation
4615- if mcp_servers and platform .system () == 'Windows' and not verify_nodejs_available ():
4616- warning ('Node.js not available - MCP server configuration may fail' )
4666+ # Check if any MCP server needs Node.js (npx-based stdio transport)
4667+ # HTTP/SSE transport servers do NOT require Node.js
4668+ needs_nodejs = any (
4669+ 'npx' in str (server .get ('command' , '' ))
4670+ for server in mcp_servers
4671+ if server .get ('command' )
4672+ )
4673+
4674+ if needs_nodejs and platform .system () == 'Windows' and not verify_nodejs_available ():
4675+ warning ('Node.js not available - npx-based MCP servers may fail' )
46174676 warning ('Please ensure Node.js is installed and in PATH' )
46184677 # Don't fail hard, let user see the issue
46194678
@@ -4690,6 +4749,8 @@ def main() -> None:
46904749 print (f' * Agents: { len (agents )} installed' )
46914750 print (f' * Slash commands: { len (commands )} installed' )
46924751 print (f' * Skills: { len (skills )} installed' )
4752+ if files_to_download :
4753+ print (f' * Files downloaded: { len (files_to_download )} processed' )
46934754 if system_prompt :
46944755 if mode == 'append' :
46954756 print (' * System prompt: appending to default' )
0 commit comments