Coverage for integrations / coding_agent / installer.py: 52.1%
73 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2Coding Agent Tool Installer — Detection and installation of external CLI coding tools.
4Detects and installs KiloCode, Claude Code, and OpenCode.
5All tools are installed via npm on the user's machine (never bundled/redistributed).
7Licenses:
8 KiloCode — Apache 2.0 (npm: @kilocode/cli)
9 Claude Code — Proprietary/Anthropic Commercial ToS (npm: @anthropic-ai/claude-code)
10 OpenCode — MIT (npm: opencode-ai)
11"""
12import logging
13import os
14import shutil
15import subprocess
16from typing import Dict, Optional
18logger = logging.getLogger('hevolve.coding_agent')
20# Tool registry: name → (binary_name, package, license)
21# binary_name='' means in-process (no external binary)
22TOOL_REGISTRY = {
23 'kilocode': ('kilocode', '@kilocode/cli', 'Apache-2.0'),
24 'claude_code': ('claude', '@anthropic-ai/claude-code', 'Proprietary'),
25 'opencode': ('opencode', 'opencode-ai', 'MIT'),
26 'aider_native': ('', 'tree-sitter tree-sitter-language-pack grep-ast diskcache diff-match-patch gitpython', 'Apache-2.0'),
27}
30def detect_installed() -> Dict[str, bool]:
31 """Check which coding tools are available."""
32 result = {}
33 for name, (binary, _, _) in TOOL_REGISTRY.items():
34 if not binary:
35 # In-process backend — check Python import
36 try:
37 from .aider_native_backend import _check_aider_core
38 result[name] = _check_aider_core()
39 except ImportError:
40 result[name] = False
41 else:
42 result[name] = shutil.which(binary) is not None
43 return result
46def get_versions() -> Dict[str, Optional[str]]:
47 """Get version strings for installed tools."""
48 versions = {}
49 for name, (binary, _, _) in TOOL_REGISTRY.items():
50 if not shutil.which(binary):
51 versions[name] = None
52 continue
53 try:
54 result = subprocess.run(
55 [binary, '--version'],
56 capture_output=True, text=True, timeout=10,
57 )
58 versions[name] = result.stdout.strip() or result.stderr.strip() or 'unknown'
59 except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
60 versions[name] = 'installed (version unknown)'
61 return versions
64def install(tool_name: str) -> Dict:
65 """Install a coding tool via npm install -g.
67 The user is installing the tool on their own machine.
68 HARTOS never bundles or redistributes these tools.
69 """
70 if tool_name not in TOOL_REGISTRY:
71 return {'success': False, 'error': f'Unknown tool: {tool_name}'}
73 binary, package, license_type = TOOL_REGISTRY[tool_name]
75 # Check npm availability
76 if not shutil.which('npm'):
77 return {
78 'success': False,
79 'error': 'npm not found. Install Node.js first: https://nodejs.org/',
80 }
82 # Already installed?
83 if shutil.which(binary):
84 return {'success': True, 'message': f'{tool_name} already installed'}
86 logger.info(f"Installing {tool_name} ({package}, license: {license_type})")
87 try:
88 result = subprocess.run(
89 ['npm', 'install', '-g', package],
90 capture_output=True, text=True, timeout=120,
91 )
92 if result.returncode == 0:
93 return {'success': True, 'message': f'{tool_name} installed successfully'}
94 else:
95 return {'success': False, 'error': result.stderr.strip()}
96 except subprocess.TimeoutExpired:
97 return {'success': False, 'error': 'Installation timed out (120s)'}
98 except OSError as e:
99 return {'success': False, 'error': str(e)}
102def pip_install(packages: str) -> Dict:
103 """Install Python packages via pip.
105 Used for in-process backends (aider_native) that need pip dependencies
106 rather than npm.
107 """
108 import sys
109 pkg_list = packages.split()
110 logger.info(f"pip installing: {pkg_list}")
111 try:
112 result = subprocess.run(
113 [sys.executable, '-m', 'pip', 'install'] + pkg_list,
114 capture_output=True, text=True, timeout=120,
115 )
116 if result.returncode == 0:
117 return {'success': True, 'message': f'Installed: {", ".join(pkg_list)}'}
118 else:
119 return {'success': False, 'error': result.stderr.strip()}
120 except subprocess.TimeoutExpired:
121 return {'success': False, 'error': 'pip install timed out (120s)'}
122 except OSError as e:
123 return {'success': False, 'error': str(e)}
126def install_tool(tool_name: str) -> Dict:
127 """Install a coding tool — routes to npm or pip based on tool type."""
128 if tool_name not in TOOL_REGISTRY:
129 return {'success': False, 'error': f'Unknown tool: {tool_name}'}
131 binary, package, _ = TOOL_REGISTRY[tool_name]
132 if not binary:
133 # In-process tool — use pip
134 return pip_install(package)
135 else:
136 # CLI tool — use npm
137 return install(tool_name)
140def get_tool_info() -> Dict:
141 """Full tool information for API / Nunba settings UI."""
142 installed = detect_installed()
143 versions = get_versions()
144 info = {}
145 for name, (binary, package, license_type) in TOOL_REGISTRY.items():
146 info[name] = {
147 'installed': installed.get(name, False),
148 'version': versions.get(name),
149 'binary': binary or '(in-process)',
150 'package': package,
151 'license': license_type,
152 'type': 'native' if not binary else 'subprocess',
153 }
154 return info