Skip to content

Commit 9d4367a

Browse files
authored
Merge pull request #207 from alex-feel/alex-feel-dev
Add on-demand Node.js LTS installation via install-nodejs config
2 parents a612d63 + f6dd60e commit 9d4367a

File tree

2 files changed

+122
-23
lines changed

2 files changed

+122
-23
lines changed

scripts/setup_environment.py

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2155,6 +2155,44 @@ def set_all_os_env_variables(env_vars: dict[str, str | None]) -> bool:
21552155
return failed_count == 0
21562156

21572157

2158+
def install_nodejs_if_requested(config: dict[str, Any]) -> bool:
2159+
"""Install Node.js LTS if requested in configuration.
2160+
2161+
Checks the 'install-nodejs' config parameter and installs Node.js
2162+
if set to True. Uses the existing ensure_nodejs() function which:
2163+
- Checks if Node.js is already installed (prevents duplicate installation)
2164+
- Tries multiple installation methods with fallbacks
2165+
- Updates PATH after installation
2166+
2167+
Args:
2168+
config: Environment configuration dictionary
2169+
2170+
Returns:
2171+
True if Node.js is installed or not requested, False if installation fails.
2172+
"""
2173+
install_nodejs_flag = config.get('install-nodejs', False)
2174+
2175+
if not install_nodejs_flag:
2176+
info('Node.js installation not requested (install-nodejs: false or not set)')
2177+
return True
2178+
2179+
info('Node.js installation requested (install-nodejs: true)')
2180+
2181+
# Late import to avoid circular dependency (install_claude imports from setup_environment)
2182+
from install_claude import ensure_nodejs as _ensure_nodejs
2183+
2184+
if not _ensure_nodejs():
2185+
error('Node.js installation failed')
2186+
return False
2187+
2188+
# Refresh PATH from registry on Windows to pick up new installation
2189+
if platform.system() == 'Windows':
2190+
refresh_path_from_registry()
2191+
2192+
success('Node.js is available')
2193+
return True
2194+
2195+
21582196
def install_dependencies(dependencies: dict[str, list[str]] | None) -> bool:
21592197
"""Install dependencies from configuration."""
21602198
if not dependencies:
@@ -4165,35 +4203,41 @@ def main() -> None:
41654203
else:
41664204
info('No custom files to download')
41674205

4168-
# Step 4: Install dependencies (after Claude Code which provides tools)
4206+
# Step 4: Install Node.js if requested (before dependencies)
4207+
print()
4208+
print(f'{Colors.CYAN}Step 4: Checking Node.js installation...{Colors.NC}')
4209+
if not install_nodejs_if_requested(config):
4210+
raise Exception('Node.js installation failed')
4211+
4212+
# Step 5: Install dependencies (after Claude Code which provides tools)
41694213
print()
4170-
print(f'{Colors.CYAN}Step 4: Installing dependencies...{Colors.NC}')
4214+
print(f'{Colors.CYAN}Step 5: Installing dependencies...{Colors.NC}')
41714215
dependencies = config.get('dependencies', {})
41724216
install_dependencies(dependencies)
41734217

4174-
# Step 5: Set OS environment variables
4218+
# Step 6: Set OS environment variables
41754219
print()
4176-
print(f'{Colors.CYAN}Step 5: Setting OS environment variables...{Colors.NC}')
4220+
print(f'{Colors.CYAN}Step 6: Setting OS environment variables...{Colors.NC}')
41774221
if os_env_variables:
41784222
set_all_os_env_variables(os_env_variables)
41794223
else:
41804224
info('No OS environment variables to configure')
41814225

4182-
# Step 6: Process agents
4226+
# Step 7: Process agents
41834227
print()
4184-
print(f'{Colors.CYAN}Step 6: Processing agents...{Colors.NC}')
4228+
print(f'{Colors.CYAN}Step 7: Processing agents...{Colors.NC}')
41854229
agents = config.get('agents', [])
41864230
process_resources(agents, agents_dir, 'agents', config_source, base_url, args.auth)
41874231

4188-
# Step 7: Process slash commands
4232+
# Step 8: Process slash commands
41894233
print()
4190-
print(f'{Colors.CYAN}Step 7: Processing slash commands...{Colors.NC}')
4234+
print(f'{Colors.CYAN}Step 8: Processing slash commands...{Colors.NC}')
41914235
commands = config.get('slash-commands', [])
41924236
process_resources(commands, commands_dir, 'slash commands', config_source, base_url, args.auth)
41934237

4194-
# Step 8: Process skills
4238+
# Step 9: Process skills
41954239
print()
4196-
print(f'{Colors.CYAN}Step 8: Processing skills...{Colors.NC}')
4240+
print(f'{Colors.CYAN}Step 9: Processing skills...{Colors.NC}')
41974241
skills_raw = config.get('skills', [])
41984242
# Convert to properly typed list using cast and list comprehension
41994243
skills: list[dict[str, Any]] = (
@@ -4203,9 +4247,9 @@ def main() -> None:
42034247
)
42044248
process_skills(skills, skills_dir, config_source, args.auth)
42054249

4206-
# Step 9: Process system prompt (if specified)
4250+
# Step 10: Process system prompt (if specified)
42074251
print()
4208-
print(f'{Colors.CYAN}Step 9: Processing system prompt...{Colors.NC}')
4252+
print(f'{Colors.CYAN}Step 10: Processing system prompt...{Colors.NC}')
42094253
prompt_path = None
42104254
if system_prompt:
42114255
# Strip query parameters from URL to get clean filename
@@ -4216,9 +4260,9 @@ def main() -> None:
42164260
else:
42174261
info('No additional system prompt configured')
42184262

4219-
# Step 10: Configure MCP servers
4263+
# Step 11: Configure MCP servers
42204264
print()
4221-
print(f'{Colors.CYAN}Step 10: Configuring MCP servers...{Colors.NC}')
4265+
print(f'{Colors.CYAN}Step 11: Configuring MCP servers...{Colors.NC}')
42224266
mcp_servers = config.get('mcp-servers', [])
42234267

42244268
# Verify Node.js is available before configuring MCP servers
@@ -4232,15 +4276,15 @@ def main() -> None:
42324276

42334277
# Check if command creation is needed
42344278
if command_name:
4235-
# Step 11: Download hooks
4279+
# Step 12: Download hooks
42364280
print()
4237-
print(f'{Colors.CYAN}Step 11: Downloading hooks...{Colors.NC}')
4281+
print(f'{Colors.CYAN}Step 12: Downloading hooks...{Colors.NC}')
42384282
hooks = config.get('hooks', {})
42394283
download_hook_files(hooks, claude_user_dir, config_source, base_url, args.auth)
42404284

4241-
# Step 12: Configure settings
4285+
# Step 13: Configure settings
42424286
print()
4243-
print(f'{Colors.CYAN}Step 12: Configuring settings...{Colors.NC}')
4287+
print(f'{Colors.CYAN}Step 13: Configuring settings...{Colors.NC}')
42444288
create_additional_settings(
42454289
hooks,
42464290
claude_user_dir,
@@ -4252,27 +4296,27 @@ def main() -> None:
42524296
always_thinking_enabled,
42534297
)
42544298

4255-
# Step 13: Create launcher script
4299+
# Step 14: Create launcher script
42564300
print()
4257-
print(f'{Colors.CYAN}Step 13: Creating launcher script...{Colors.NC}')
4301+
print(f'{Colors.CYAN}Step 14: Creating launcher script...{Colors.NC}')
42584302
# Strip query parameters from system prompt filename (must match download logic)
42594303
prompt_filename: str | None = None
42604304
if system_prompt:
42614305
clean_prompt = system_prompt.split('?')[0] if '?' in system_prompt else system_prompt
42624306
prompt_filename = Path(clean_prompt).name
42634307
launcher_path = create_launcher_script(claude_user_dir, command_name, prompt_filename, mode)
42644308

4265-
# Step 14: Register global command
4309+
# Step 15: Register global command
42664310
if launcher_path:
42674311
print()
4268-
print(f'{Colors.CYAN}Step 14: Registering global {command_name} command...{Colors.NC}')
4312+
print(f'{Colors.CYAN}Step 15: Registering global {command_name} command...{Colors.NC}')
42694313
register_global_command(launcher_path, command_name)
42704314
else:
42714315
warning('Launcher script was not created')
42724316
else:
42734317
# Skip command creation
42744318
print()
4275-
print(f'{Colors.CYAN}Steps 11-14: Skipping command creation (no command-name specified)...{Colors.NC}')
4319+
print(f'{Colors.CYAN}Steps 12-15: Skipping command creation (no command-name specified)...{Colors.NC}')
42764320
info('Environment configuration completed successfully')
42774321
info('To create a custom command, add "command-name: your-command-name" to your config')
42784322

tests/test_setup_environment.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,61 @@ def test_install_dependencies_uv_tool_windows(self, mock_run, mock_system):
754754
mock_run.assert_called_with(['uv', 'tool', 'install', '--force', 'ruff'], capture_output=False)
755755

756756

757+
class TestInstallNodejsIfRequested:
758+
"""Test install_nodejs_if_requested() function."""
759+
760+
def test_not_requested_returns_true(self):
761+
"""Test that function returns True when install-nodejs is not set."""
762+
config: dict = {'name': 'Test'}
763+
result = setup_environment.install_nodejs_if_requested(config)
764+
assert result is True
765+
766+
def test_false_returns_true(self):
767+
"""Test that function returns True when install-nodejs is False."""
768+
config = {'install-nodejs': False}
769+
result = setup_environment.install_nodejs_if_requested(config)
770+
assert result is True
771+
772+
@patch('install_claude.ensure_nodejs')
773+
def test_true_calls_ensure_nodejs(self, mock_ensure):
774+
"""Test that function calls ensure_nodejs when install-nodejs is True."""
775+
mock_ensure.return_value = True
776+
config = {'install-nodejs': True}
777+
result = setup_environment.install_nodejs_if_requested(config)
778+
assert result is True
779+
mock_ensure.assert_called_once()
780+
781+
@patch('install_claude.ensure_nodejs')
782+
def test_installation_failure_returns_false(self, mock_ensure):
783+
"""Test that function returns False when ensure_nodejs fails."""
784+
mock_ensure.return_value = False
785+
config = {'install-nodejs': True}
786+
result = setup_environment.install_nodejs_if_requested(config)
787+
assert result is False
788+
789+
@pytest.mark.skipif(sys.platform != 'win32', reason='Windows-specific')
790+
@patch('install_claude.ensure_nodejs')
791+
@patch('setup_environment.refresh_path_from_registry')
792+
def test_windows_refreshes_path(self, mock_refresh, mock_ensure):
793+
"""Test that PATH is refreshed on Windows after installation."""
794+
mock_ensure.return_value = True
795+
config = {'install-nodejs': True}
796+
setup_environment.install_nodejs_if_requested(config)
797+
mock_refresh.assert_called_once()
798+
799+
@patch('platform.system', return_value='Linux')
800+
@patch('install_claude.ensure_nodejs')
801+
@patch('setup_environment.refresh_path_from_registry')
802+
def test_non_windows_skips_path_refresh(self, mock_refresh, mock_ensure, mock_system):
803+
"""Test that PATH refresh is skipped on non-Windows platforms."""
804+
# Verify mock configuration
805+
assert mock_system.return_value == 'Linux'
806+
mock_ensure.return_value = True
807+
config = {'install-nodejs': True}
808+
setup_environment.install_nodejs_if_requested(config)
809+
mock_refresh.assert_not_called()
810+
811+
757812
class TestFetchUrlWithAuth:
758813
"""Test URL fetching with authentication."""
759814

0 commit comments

Comments
 (0)