Skip to content

Commit c28a9ca

Browse files
authored
Merge pull request #222 from alex-feel/alex-feel-dev
Add config file support for status-line hook and some improvements
2 parents e03cd45 + da1407d commit c28a9ca

File tree

3 files changed

+447
-51
lines changed

3 files changed

+447
-51
lines changed

scripts/setup_environment.py

Lines changed: 110 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import argparse
1313
import contextlib
14+
import glob as glob_module
1415
import json
1516
import os
1617
import 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:
30453074
def 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

Comments
 (0)