Skip to content

Commit 9b0ac11

Browse files
rysweetclaude
andauthored
feat: wire up interactive installation for missing prerequisites (#1673)
Connects existing InteractiveInstaller functionality to check_prerequisites() entry point, enabling automatic interactive installation prompts when tools are missing instead of just showing manual instructions. **Changes:** - Modified check_prerequisites() to call check_and_install(interactive=True) - Updated docstring to reflect new interactive behavior - Added comprehensive test for new wiring - Applied ruff-format line wrapping improvements **Testing:** - 36/36 unit/integration tests passing - New test verifies correct method call and return value handling - Test plan documented for manual QA verification **Notes:** - Pre-existing linting errors in file noted (duplicate InstallationResult class) - These errors exist in main branch and are not introduced by this change - Will be addressed in separate cleanup PR Closes #1668 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent ed6757d commit 9b0ac11

File tree

2 files changed

+53
-11
lines changed

2 files changed

+53
-11
lines changed

src/amplihack/utils/prerequisites.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,9 @@ def __init__(self, platform: Platform):
233233
platform: Target platform for installation
234234
"""
235235
self.platform = platform
236-
self.audit_log_path = Path.home() / ".claude" / "runtime" / "logs" / "installation_audit.jsonl"
236+
self.audit_log_path = (
237+
Path.home() / ".claude" / "runtime" / "logs" / "installation_audit.jsonl"
238+
)
237239

238240
def is_interactive_environment(self) -> bool:
239241
"""Check if running in an interactive environment.
@@ -274,7 +276,9 @@ def prompt_for_approval(self, tool: str, command: List[str]) -> bool:
274276
print(f"\n{'=' * 70}\n")
275277

276278
while True:
277-
response = input(f"Do you want to proceed with installing {tool}? [y/N]: ").strip().lower()
279+
response = (
280+
input(f"Do you want to proceed with installing {tool}? [y/N]: ").strip().lower()
281+
)
278282
if response in ["y", "yes"]:
279283
return True
280284
if response in ["n", "no", ""]:
@@ -879,20 +883,26 @@ def check_and_install(self, interactive: bool = True) -> PrerequisiteResult:
879883

880884
# Convenience function for quick prerequisite checking
881885
def check_prerequisites() -> bool:
882-
"""Quick prerequisite check with automatic reporting.
886+
"""Quick prerequisite check with automatic interactive installation.
887+
888+
In interactive environments (TTY), prompts user to install missing tools.
889+
In non-interactive environments (CI), prints manual instructions.
883890
884891
Returns:
885892
True if all prerequisites available, False otherwise
886893
887894
Side Effects:
888-
Prints detailed report to stdout if prerequisites are missing
895+
- In interactive mode: Prompts for user approval and installs missing tools
896+
- In non-interactive mode: Prints manual installation instructions
897+
- All installation attempts are logged to ~/.amplihack/installation_audit.json
889898
890899
Example:
891900
>>> if not check_prerequisites():
892901
... sys.exit(1)
893902
"""
894903
checker = PrerequisiteChecker()
895-
return checker.check_and_report()
904+
result = checker.check_and_install(interactive=True)
905+
return result.all_available
896906

897907

898908
__all__ = [

tests/test_prerequisites.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ def test_check_all_prerequisites_success(self):
104104
checker = PrerequisiteChecker()
105105
with patch("shutil.which", return_value="/usr/bin/tool"), patch(
106106
"amplihack.utils.prerequisites.get_claude_cli_path", return_value="/usr/bin/claude"
107-
), patch("subprocess.run", return_value=Mock(returncode=0, stdout="version 1.0", stderr="")):
107+
), patch(
108+
"subprocess.run", return_value=Mock(returncode=0, stdout="version 1.0", stderr="")
109+
):
108110
result = checker.check_all_prerequisites()
109111
assert result.all_available is True
110112
assert len(result.missing_tools) == 0
@@ -225,7 +227,9 @@ def test_full_check_workflow_all_present(self):
225227
checker = PrerequisiteChecker()
226228
with patch("shutil.which") as mock_which, patch(
227229
"amplihack.utils.prerequisites.get_claude_cli_path", return_value="/usr/bin/claude"
228-
), patch("subprocess.run", return_value=Mock(returncode=0, stdout="version 1.0", stderr="")):
230+
), patch(
231+
"subprocess.run", return_value=Mock(returncode=0, stdout="version 1.0", stderr="")
232+
):
229233
# Simulate all tools being available
230234
mock_which.side_effect = lambda x: f"/usr/bin/{x}"
231235
result = checker.check_all_prerequisites()
@@ -237,8 +241,9 @@ def test_full_check_workflow_all_present(self):
237241
def test_full_check_workflow_some_missing(self):
238242
"""Test complete prerequisite check with some tools missing."""
239243
checker = PrerequisiteChecker()
240-
with patch("shutil.which") as mock_which, \
241-
patch("subprocess.run", return_value=Mock(returncode=0, stdout="version 1.0", stderr="")):
244+
with patch("shutil.which") as mock_which, patch(
245+
"subprocess.run", return_value=Mock(returncode=0, stdout="version 1.0", stderr="")
246+
):
242247
# node, npm, rg, and claude missing; uv and git present
243248
mock_which.side_effect = lambda x: (f"/usr/bin/{x}" if x in ["uv", "git"] else None)
244249
result = checker.check_all_prerequisites()
@@ -334,6 +339,32 @@ def test_prerequisite_check_handles_path_edge_cases(self):
334339
assert result.path == "/path with spaces/tool"
335340

336341

342+
class TestCheckPrerequisitesConvenience:
343+
"""Integration tests for check_prerequisites() convenience function."""
344+
345+
def test_check_prerequisites_calls_check_and_install_interactive(self):
346+
"""Test that check_prerequisites() calls check_and_install(interactive=True)
347+
and returns correct boolean based on result.all_available."""
348+
from amplihack.utils.prerequisites import check_prerequisites
349+
350+
with patch("amplihack.utils.prerequisites.PrerequisiteChecker") as MockChecker:
351+
mock_checker_instance = Mock()
352+
mock_result = Mock()
353+
MockChecker.return_value = mock_checker_instance
354+
355+
# Test True case (all tools available)
356+
mock_result.all_available = True
357+
mock_checker_instance.check_and_install.return_value = mock_result
358+
assert check_prerequisites() is True
359+
mock_checker_instance.check_and_install.assert_called_with(interactive=True)
360+
361+
# Reset and test False case (tools missing)
362+
mock_checker_instance.check_and_install.reset_mock()
363+
mock_result.all_available = False
364+
assert check_prerequisites() is False
365+
mock_checker_instance.check_and_install.assert_called_with(interactive=True)
366+
367+
337368
# ============================================================================
338369
# E2E TESTS (10% - 3 tests)
339370
# ============================================================================
@@ -388,8 +419,9 @@ def test_e2e_partial_prerequisites_with_specific_guidance(self):
388419
):
389420
checker = PrerequisiteChecker()
390421

391-
with patch("shutil.which") as mock_which, \
392-
patch("subprocess.run", return_value=Mock(returncode=0, stdout="version 1.0", stderr="")):
422+
with patch("shutil.which") as mock_which, patch(
423+
"subprocess.run", return_value=Mock(returncode=0, stdout="version 1.0", stderr="")
424+
):
393425
# Only git and uv present
394426
mock_which.side_effect = lambda x: (f"/usr/bin/{x}" if x in ["git", "uv"] else None)
395427

0 commit comments

Comments
 (0)