claude-mpm 4.4.4__py3-none-any.whl → 4.4.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/mcp_external_commands.py +7 -7
- claude_mpm/cli/commands/mcp_install_commands.py +9 -9
- claude_mpm/cli/commands/mcp_setup_external.py +6 -6
- claude_mpm/hooks/kuzu_memory_hook.py +4 -2
- claude_mpm/services/diagnostics/checks/__init__.py +2 -2
- claude_mpm/services/diagnostics/checks/{claude_desktop_check.py → claude_code_check.py} +95 -112
- claude_mpm/services/diagnostics/checks/mcp_check.py +6 -6
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +97 -22
- claude_mpm/services/diagnostics/diagnostic_runner.py +5 -5
- claude_mpm/services/diagnostics/doctor_reporter.py +4 -4
- claude_mpm/services/mcp_config_manager.py +314 -47
- claude_mpm/services/mcp_gateway/core/process_pool.py +11 -8
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +4 -4
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +8 -4
- claude_mpm/services/project/project_organizer.py +8 -1
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +1 -2
- claude_mpm/services/unified/config_strategies/context_strategy.py +1 -3
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +3 -1
- claude_mpm/validation/frontmatter_validator.py +1 -1
- {claude_mpm-4.4.4.dist-info → claude_mpm-4.4.6.dist-info}/METADATA +35 -18
- {claude_mpm-4.4.4.dist-info → claude_mpm-4.4.6.dist-info}/RECORD +26 -26
- {claude_mpm-4.4.4.dist-info → claude_mpm-4.4.6.dist-info}/WHEEL +0 -0
- {claude_mpm-4.4.4.dist-info → claude_mpm-4.4.6.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.4.4.dist-info → claude_mpm-4.4.6.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.4.4.dist-info → claude_mpm-4.4.6.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,7 @@
|
|
2
2
|
Check MCP external services installation and health.
|
3
3
|
|
4
4
|
WHY: Verify that MCP services (mcp-vector-search, mcp-browser, mcp-ticketer, kuzu-memory)
|
5
|
-
are properly installed and accessible for enhanced Claude
|
5
|
+
are properly installed and accessible for enhanced Claude Code capabilities.
|
6
6
|
"""
|
7
7
|
|
8
8
|
import json
|
@@ -21,30 +21,38 @@ class MCPServicesCheck(BaseDiagnosticCheck):
|
|
21
21
|
MCP_SERVICES = {
|
22
22
|
"mcp-vector-search": {
|
23
23
|
"package": "mcp-vector-search",
|
24
|
-
"command": [
|
24
|
+
"command": [
|
25
|
+
"mcp-vector-search",
|
26
|
+
"--version",
|
27
|
+
], # Use --version for proper check
|
25
28
|
"description": "Vector search for semantic code navigation",
|
26
29
|
"check_health": True,
|
27
30
|
"health_command": ["mcp-vector-search", "--version"],
|
31
|
+
"pipx_run_command": ["pipx", "run", "mcp-vector-search", "--version"],
|
28
32
|
},
|
29
33
|
"mcp-browser": {
|
30
34
|
"package": "mcp-browser",
|
31
|
-
"command": ["mcp-browser", "--
|
35
|
+
"command": ["mcp-browser", "--version"], # Use --version for proper check
|
32
36
|
"description": "Browser automation and web interaction",
|
33
37
|
"check_health": True,
|
34
38
|
"health_command": ["mcp-browser", "--version"],
|
39
|
+
"pipx_run_command": ["pipx", "run", "mcp-browser", "--version"],
|
35
40
|
},
|
36
41
|
"mcp-ticketer": {
|
37
42
|
"package": "mcp-ticketer",
|
38
|
-
"command": ["mcp-ticketer", "--
|
43
|
+
"command": ["mcp-ticketer", "--version"], # Use --version for proper check
|
39
44
|
"description": "Ticket and task management",
|
40
45
|
"check_health": True,
|
41
46
|
"health_command": ["mcp-ticketer", "--version"],
|
47
|
+
"pipx_run_command": ["pipx", "run", "mcp-ticketer", "--version"],
|
42
48
|
},
|
43
49
|
"kuzu-memory": {
|
44
50
|
"package": "kuzu-memory",
|
45
|
-
"command": ["kuzu-memory", "--
|
51
|
+
"command": ["kuzu-memory", "--version"], # Use --version for proper check
|
46
52
|
"description": "Graph-based memory system",
|
47
|
-
"check_health":
|
53
|
+
"check_health": True, # v1.1.0+ has version command
|
54
|
+
"health_command": ["kuzu-memory", "--version"],
|
55
|
+
"pipx_run_command": ["pipx", "run", "kuzu-memory", "--version"],
|
48
56
|
},
|
49
57
|
}
|
50
58
|
|
@@ -143,6 +151,13 @@ class MCPServicesCheck(BaseDiagnosticCheck):
|
|
143
151
|
if command_path:
|
144
152
|
details["command_path"] = command_path
|
145
153
|
|
154
|
+
# If not directly accessible, try pipx run command
|
155
|
+
if not accessible and "pipx_run_command" in config:
|
156
|
+
if self._verify_command_works(config["pipx_run_command"]):
|
157
|
+
accessible = True
|
158
|
+
details["accessible_via_pipx_run"] = True
|
159
|
+
details["pipx_run_available"] = True
|
160
|
+
|
146
161
|
# Check for installation in various locations
|
147
162
|
if not pipx_installed and not accessible:
|
148
163
|
# Try common installation locations
|
@@ -157,11 +172,19 @@ class MCPServicesCheck(BaseDiagnosticCheck):
|
|
157
172
|
|
158
173
|
# Check service health/version if accessible
|
159
174
|
if accessible and config.get("check_health"):
|
160
|
-
version
|
161
|
-
|
162
|
-
)
|
163
|
-
|
164
|
-
|
175
|
+
# Try different version commands in order of preference
|
176
|
+
version_commands = []
|
177
|
+
if details.get("accessible_via_pipx_run") and "pipx_run_command" in config:
|
178
|
+
version_commands.append(config["pipx_run_command"])
|
179
|
+
if "health_command" in config:
|
180
|
+
version_commands.append(config["health_command"])
|
181
|
+
version_commands.append(config["command"])
|
182
|
+
|
183
|
+
for cmd in version_commands:
|
184
|
+
version = self._get_service_version(cmd)
|
185
|
+
if version:
|
186
|
+
details["version"] = version
|
187
|
+
break
|
165
188
|
|
166
189
|
# Determine status
|
167
190
|
if not (pipx_installed or accessible):
|
@@ -175,6 +198,14 @@ class MCPServicesCheck(BaseDiagnosticCheck):
|
|
175
198
|
)
|
176
199
|
|
177
200
|
if pipx_installed and not accessible:
|
201
|
+
# Check if pipx run works
|
202
|
+
if details.get("pipx_run_available"):
|
203
|
+
return DiagnosticResult(
|
204
|
+
category=f"MCP Service: {service_name}",
|
205
|
+
status=DiagnosticStatus.OK,
|
206
|
+
message="Installed via pipx (use 'pipx run' to execute)",
|
207
|
+
details=details,
|
208
|
+
)
|
178
209
|
return DiagnosticResult(
|
179
210
|
category=f"MCP Service: {service_name}",
|
180
211
|
status=DiagnosticStatus.WARNING,
|
@@ -247,29 +278,73 @@ class MCPServicesCheck(BaseDiagnosticCheck):
|
|
247
278
|
|
248
279
|
if result.returncode == 0:
|
249
280
|
path = result.stdout.strip()
|
250
|
-
|
281
|
+
# Verify the command actually works with --version
|
282
|
+
if self._verify_command_works(command):
|
283
|
+
return True, path
|
284
|
+
return False, path
|
251
285
|
except (subprocess.SubprocessError, FileNotFoundError):
|
252
286
|
pass
|
253
287
|
|
254
|
-
# Try direct execution
|
288
|
+
# Try direct execution with --version
|
289
|
+
if self._verify_command_works(command):
|
290
|
+
return True, None
|
291
|
+
|
292
|
+
return False, None
|
293
|
+
|
294
|
+
def _verify_command_works(self, command: List[str]) -> bool:
|
295
|
+
"""Verify a command actually works by checking its --version output."""
|
255
296
|
try:
|
256
297
|
result = subprocess.run(
|
257
298
|
command,
|
258
299
|
capture_output=True,
|
259
300
|
text=True,
|
260
|
-
timeout=
|
301
|
+
timeout=5,
|
261
302
|
check=False,
|
262
303
|
)
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
304
|
+
|
305
|
+
# Check for successful execution or version output
|
306
|
+
# Don't accept error messages containing "help" or "usage" as success
|
307
|
+
if result.returncode == 0:
|
308
|
+
# Look for actual version information
|
309
|
+
output = (result.stdout + result.stderr).lower()
|
310
|
+
# Check for version indicators
|
311
|
+
if any(
|
312
|
+
keyword in output
|
313
|
+
for keyword in ["version", "v1.", "v0.", "1.", "0."]
|
314
|
+
):
|
315
|
+
# But reject if it's an error message
|
316
|
+
if not any(
|
317
|
+
error in output
|
318
|
+
for error in [
|
319
|
+
"error",
|
320
|
+
"not found",
|
321
|
+
"no such",
|
322
|
+
"command not found",
|
323
|
+
]
|
324
|
+
):
|
325
|
+
return True
|
326
|
+
|
327
|
+
# For some tools, non-zero return code is OK if version is shown
|
328
|
+
elif "--version" in command or "--help" in command:
|
329
|
+
output = (result.stdout + result.stderr).lower()
|
330
|
+
# Must have version info and no error indicators
|
331
|
+
if "version" in output or "v1." in output or "v0." in output:
|
332
|
+
if not any(
|
333
|
+
error in output
|
334
|
+
for error in [
|
335
|
+
"error",
|
336
|
+
"not found",
|
337
|
+
"no such",
|
338
|
+
"command not found",
|
339
|
+
"traceback",
|
340
|
+
]
|
341
|
+
):
|
342
|
+
return True
|
343
|
+
|
344
|
+
except (subprocess.SubprocessError, FileNotFoundError, OSError):
|
270
345
|
pass
|
271
346
|
|
272
|
-
return False
|
347
|
+
return False
|
273
348
|
|
274
349
|
def _check_alternative_installations(
|
275
350
|
self, service_name: str
|
@@ -14,7 +14,7 @@ from claude_mpm.core.logging_utils import get_logger
|
|
14
14
|
from .checks import (
|
15
15
|
AgentCheck,
|
16
16
|
BaseDiagnosticCheck,
|
17
|
-
|
17
|
+
ClaudeCodeCheck,
|
18
18
|
CommonIssuesCheck,
|
19
19
|
ConfigurationCheck,
|
20
20
|
FilesystemCheck,
|
@@ -53,7 +53,7 @@ class DiagnosticRunner:
|
|
53
53
|
ConfigurationCheck,
|
54
54
|
FilesystemCheck,
|
55
55
|
InstructionsCheck, # Check instruction files early
|
56
|
-
|
56
|
+
ClaudeCodeCheck,
|
57
57
|
AgentCheck,
|
58
58
|
MCPCheck,
|
59
59
|
MCPServicesCheck, # Check external MCP services
|
@@ -121,7 +121,7 @@ class DiagnosticRunner:
|
|
121
121
|
]
|
122
122
|
# Level 2: May depend on level 1
|
123
123
|
level2 = [
|
124
|
-
|
124
|
+
ClaudeCodeCheck,
|
125
125
|
AgentCheck,
|
126
126
|
MCPCheck,
|
127
127
|
MCPServicesCheck,
|
@@ -209,8 +209,8 @@ class DiagnosticRunner:
|
|
209
209
|
"config": ConfigurationCheck,
|
210
210
|
"filesystem": FilesystemCheck,
|
211
211
|
"fs": FilesystemCheck,
|
212
|
-
"claude":
|
213
|
-
"
|
212
|
+
"claude": ClaudeCodeCheck,
|
213
|
+
"claude_code": ClaudeCodeCheck,
|
214
214
|
"agents": AgentCheck,
|
215
215
|
"agent": AgentCheck,
|
216
216
|
"mcp": MCPCheck,
|
@@ -385,9 +385,9 @@ class DoctorReporter:
|
|
385
385
|
print(
|
386
386
|
f"| Installation Method | {result.details['installation_method']} |"
|
387
387
|
)
|
388
|
-
elif result.category == "Claude
|
388
|
+
elif result.category == "Claude Code":
|
389
389
|
if result.details.get("version"):
|
390
|
-
print(f"| Claude
|
390
|
+
print(f"| Claude Code (CLI) | {result.details['version']} |")
|
391
391
|
|
392
392
|
def _print_mcp_services_markdown(self, summary: DiagnosticSummary):
|
393
393
|
"""Print MCP services status table in markdown."""
|
@@ -533,11 +533,11 @@ class DoctorReporter:
|
|
533
533
|
)
|
534
534
|
|
535
535
|
if (
|
536
|
-
result.category == "Claude
|
536
|
+
result.category == "Claude Code"
|
537
537
|
and result.status == DiagnosticStatus.WARNING
|
538
538
|
):
|
539
539
|
recommendations.append(
|
540
|
-
"Update Claude
|
540
|
+
"Update Claude Code (CLI) to the latest version for best compatibility"
|
541
541
|
)
|
542
542
|
|
543
543
|
if recommendations:
|
@@ -11,6 +11,7 @@ MCP service installations.
|
|
11
11
|
|
12
12
|
import json
|
13
13
|
import subprocess
|
14
|
+
import sys
|
14
15
|
from datetime import datetime, timezone
|
15
16
|
from enum import Enum
|
16
17
|
from pathlib import Path
|
@@ -85,7 +86,11 @@ class MCPConfigManager:
|
|
85
86
|
for path in candidates:
|
86
87
|
try:
|
87
88
|
result = subprocess.run(
|
88
|
-
[path, "--help"],
|
89
|
+
[path, "--help"],
|
90
|
+
capture_output=True,
|
91
|
+
text=True,
|
92
|
+
timeout=5,
|
93
|
+
check=False,
|
89
94
|
)
|
90
95
|
# Check if this version has MCP support
|
91
96
|
if "claude" in result.stdout or "mcp" in result.stdout:
|
@@ -194,55 +199,172 @@ class MCPConfigManager:
|
|
194
199
|
"""
|
195
200
|
Generate configuration for a specific MCP service.
|
196
201
|
|
202
|
+
Prefers 'pipx run' or 'uvx' commands over direct execution for better isolation.
|
203
|
+
|
197
204
|
Args:
|
198
205
|
service_name: Name of the MCP service
|
199
206
|
|
200
207
|
Returns:
|
201
208
|
Service configuration dict or None if service not found
|
202
209
|
"""
|
203
|
-
|
204
|
-
if not service_path:
|
205
|
-
return None
|
210
|
+
import shutil
|
206
211
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
212
|
+
# Check for pipx run first (preferred for isolation)
|
213
|
+
use_pipx_run = False
|
214
|
+
use_uvx = False
|
215
|
+
|
216
|
+
# Try pipx run test
|
217
|
+
if shutil.which("pipx"):
|
218
|
+
try:
|
219
|
+
result = subprocess.run(
|
220
|
+
["pipx", "run", service_name, "--version"],
|
221
|
+
capture_output=True,
|
222
|
+
text=True,
|
223
|
+
timeout=5,
|
224
|
+
check=False,
|
225
|
+
)
|
226
|
+
if result.returncode == 0 or "version" in result.stdout.lower():
|
227
|
+
use_pipx_run = True
|
228
|
+
self.logger.debug(f"Will use 'pipx run' for {service_name}")
|
229
|
+
except:
|
230
|
+
pass
|
231
|
+
|
232
|
+
# Try uvx if pipx run not available
|
233
|
+
if not use_pipx_run and shutil.which("uvx"):
|
234
|
+
try:
|
235
|
+
result = subprocess.run(
|
236
|
+
["uvx", service_name, "--version"],
|
237
|
+
capture_output=True,
|
238
|
+
text=True,
|
239
|
+
timeout=5,
|
240
|
+
check=False,
|
241
|
+
)
|
242
|
+
if result.returncode == 0 or "version" in result.stdout.lower():
|
243
|
+
use_uvx = True
|
244
|
+
self.logger.debug(f"Will use 'uvx' for {service_name}")
|
245
|
+
except:
|
246
|
+
pass
|
247
|
+
|
248
|
+
# If neither work, try to find direct path
|
249
|
+
service_path = None
|
250
|
+
if not use_pipx_run and not use_uvx:
|
251
|
+
service_path = self.detect_service_path(service_name)
|
252
|
+
if not service_path:
|
253
|
+
return None
|
254
|
+
|
255
|
+
# Build configuration
|
256
|
+
config = {"type": "stdio"}
|
211
257
|
|
212
258
|
# Service-specific configurations
|
213
259
|
if service_name == "mcp-vector-search":
|
214
|
-
|
215
|
-
"
|
216
|
-
"
|
217
|
-
|
218
|
-
|
260
|
+
if use_pipx_run:
|
261
|
+
config["command"] = "pipx"
|
262
|
+
config["args"] = [
|
263
|
+
"run",
|
264
|
+
"mcp-vector-search",
|
265
|
+
"-m",
|
266
|
+
"mcp_vector_search.mcp.server",
|
267
|
+
str(self.project_root),
|
268
|
+
]
|
269
|
+
elif use_uvx:
|
270
|
+
config["command"] = "uvx"
|
271
|
+
config["args"] = [
|
272
|
+
"mcp-vector-search",
|
273
|
+
"-m",
|
274
|
+
"mcp_vector_search.mcp.server",
|
275
|
+
str(self.project_root),
|
276
|
+
]
|
277
|
+
else:
|
278
|
+
config["command"] = service_path
|
279
|
+
config["args"] = [
|
280
|
+
"-m",
|
281
|
+
"mcp_vector_search.mcp.server",
|
282
|
+
str(self.project_root),
|
283
|
+
]
|
219
284
|
config["env"] = {}
|
285
|
+
|
220
286
|
elif service_name == "mcp-browser":
|
221
|
-
|
287
|
+
if use_pipx_run:
|
288
|
+
config["command"] = "pipx"
|
289
|
+
config["args"] = ["run", "mcp-browser", "mcp"]
|
290
|
+
elif use_uvx:
|
291
|
+
config["command"] = "uvx"
|
292
|
+
config["args"] = ["mcp-browser", "mcp"]
|
293
|
+
else:
|
294
|
+
config["command"] = service_path
|
295
|
+
config["args"] = ["mcp"]
|
222
296
|
config["env"] = {"MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser")}
|
297
|
+
|
223
298
|
elif service_name == "mcp-ticketer":
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
299
|
+
if use_pipx_run:
|
300
|
+
config["command"] = "pipx"
|
301
|
+
config["args"] = ["run", "mcp-ticketer", "mcp"]
|
302
|
+
elif use_uvx:
|
303
|
+
config["command"] = "uvx"
|
304
|
+
config["args"] = ["mcp-ticketer", "mcp"]
|
305
|
+
else:
|
306
|
+
config["command"] = service_path
|
307
|
+
config["args"] = ["mcp"]
|
229
308
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
309
|
+
elif service_name == "kuzu-memory":
|
310
|
+
# Determine kuzu-memory command version
|
311
|
+
kuzu_args = ["mcp", "serve"] # Default to the correct modern format
|
312
|
+
test_cmd = None
|
313
|
+
|
314
|
+
if use_pipx_run:
|
315
|
+
test_cmd = ["pipx", "run", "kuzu-memory", "--help"]
|
316
|
+
elif use_uvx:
|
317
|
+
test_cmd = ["uvx", "kuzu-memory", "--help"]
|
318
|
+
elif service_path:
|
319
|
+
test_cmd = [service_path, "--help"]
|
320
|
+
|
321
|
+
if test_cmd:
|
322
|
+
try:
|
323
|
+
result = subprocess.run(
|
324
|
+
test_cmd,
|
325
|
+
capture_output=True,
|
326
|
+
text=True,
|
327
|
+
timeout=10,
|
328
|
+
check=False,
|
329
|
+
)
|
330
|
+
# Check for MCP support in help output
|
331
|
+
help_output = result.stdout.lower() + result.stderr.lower()
|
332
|
+
|
333
|
+
# Modern version detection - look for "mcp serve" command
|
334
|
+
if "mcp serve" in help_output or ("mcp" in help_output and "serve" in help_output):
|
335
|
+
# Modern version with mcp serve command
|
336
|
+
kuzu_args = ["mcp", "serve"]
|
337
|
+
# Legacy version detection - only "serve" without "mcp"
|
338
|
+
elif "serve" in help_output and "mcp" not in help_output:
|
339
|
+
# Very old version that only has serve command
|
340
|
+
kuzu_args = ["serve"]
|
341
|
+
# Note: "claude mcp-server" format is deprecated and not used
|
342
|
+
else:
|
343
|
+
# Default to the correct modern format
|
344
|
+
kuzu_args = ["mcp", "serve"]
|
345
|
+
except Exception:
|
346
|
+
# Default to the correct mcp serve command on any error
|
347
|
+
kuzu_args = ["mcp", "serve"]
|
348
|
+
|
349
|
+
if use_pipx_run:
|
350
|
+
config["command"] = "pipx"
|
351
|
+
config["args"] = ["run", "kuzu-memory"] + kuzu_args
|
352
|
+
elif use_uvx:
|
353
|
+
config["command"] = "uvx"
|
354
|
+
config["args"] = ["kuzu-memory"] + kuzu_args
|
355
|
+
else:
|
356
|
+
config["command"] = service_path
|
357
|
+
config["args"] = kuzu_args
|
358
|
+
|
359
|
+
# Generic config for unknown services
|
360
|
+
elif use_pipx_run:
|
361
|
+
config["command"] = "pipx"
|
362
|
+
config["args"] = ["run", service_name]
|
363
|
+
elif use_uvx:
|
364
|
+
config["command"] = "uvx"
|
365
|
+
config["args"] = [service_name]
|
244
366
|
else:
|
245
|
-
|
367
|
+
config["command"] = service_path
|
246
368
|
config["args"] = []
|
247
369
|
|
248
370
|
return config
|
@@ -464,7 +586,7 @@ class MCPConfigManager:
|
|
464
586
|
|
465
587
|
def install_missing_services(self) -> Tuple[bool, str]:
|
466
588
|
"""
|
467
|
-
Install missing MCP services via pipx.
|
589
|
+
Install missing MCP services via pipx with verification and fallbacks.
|
468
590
|
|
469
591
|
Returns:
|
470
592
|
Tuple of (success, message)
|
@@ -481,22 +603,167 @@ class MCPConfigManager:
|
|
481
603
|
failed = []
|
482
604
|
|
483
605
|
for service_name in missing:
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
check=True,
|
491
|
-
)
|
492
|
-
installed.append(service_name)
|
493
|
-
self.logger.info(f"Successfully installed {service_name}")
|
494
|
-
except subprocess.CalledProcessError as e:
|
606
|
+
# Try pipx install first
|
607
|
+
success, method = self._install_service_with_fallback(service_name)
|
608
|
+
if success:
|
609
|
+
installed.append(f"{service_name} ({method})")
|
610
|
+
self.logger.info(f"Successfully installed {service_name} via {method}")
|
611
|
+
else:
|
495
612
|
failed.append(service_name)
|
496
|
-
self.logger.error(f"Failed to install {service_name}
|
613
|
+
self.logger.error(f"Failed to install {service_name}")
|
497
614
|
|
498
615
|
if failed:
|
499
616
|
return False, f"Failed to install: {', '.join(failed)}"
|
500
617
|
if installed:
|
501
618
|
return True, f"Successfully installed: {', '.join(installed)}"
|
502
619
|
return True, "No services needed installation"
|
620
|
+
|
621
|
+
def _install_service_with_fallback(self, service_name: str) -> Tuple[bool, str]:
|
622
|
+
"""
|
623
|
+
Install a service with multiple fallback methods.
|
624
|
+
|
625
|
+
Returns:
|
626
|
+
Tuple of (success, installation_method)
|
627
|
+
"""
|
628
|
+
import shutil
|
629
|
+
|
630
|
+
# Method 1: Try pipx install
|
631
|
+
if shutil.which("pipx"):
|
632
|
+
try:
|
633
|
+
self.logger.debug(f"Attempting to install {service_name} via pipx...")
|
634
|
+
result = subprocess.run(
|
635
|
+
["pipx", "install", service_name],
|
636
|
+
capture_output=True,
|
637
|
+
text=True,
|
638
|
+
timeout=120, # 2 minute timeout
|
639
|
+
check=False,
|
640
|
+
)
|
641
|
+
|
642
|
+
if result.returncode == 0:
|
643
|
+
# Verify installation worked
|
644
|
+
if self._verify_service_installed(service_name, "pipx"):
|
645
|
+
return True, "pipx"
|
646
|
+
|
647
|
+
self.logger.warning(
|
648
|
+
f"pipx install succeeded but verification failed for {service_name}"
|
649
|
+
)
|
650
|
+
else:
|
651
|
+
self.logger.debug(f"pipx install failed: {result.stderr}")
|
652
|
+
except subprocess.TimeoutExpired:
|
653
|
+
self.logger.warning(f"pipx install timed out for {service_name}")
|
654
|
+
except Exception as e:
|
655
|
+
self.logger.debug(f"pipx install error: {e}")
|
656
|
+
|
657
|
+
# Method 2: Try uvx (if available)
|
658
|
+
if shutil.which("uvx"):
|
659
|
+
try:
|
660
|
+
self.logger.debug(f"Attempting to install {service_name} via uvx...")
|
661
|
+
result = subprocess.run(
|
662
|
+
["uvx", "install", service_name],
|
663
|
+
capture_output=True,
|
664
|
+
text=True,
|
665
|
+
timeout=120,
|
666
|
+
check=False,
|
667
|
+
)
|
668
|
+
|
669
|
+
if result.returncode == 0:
|
670
|
+
if self._verify_service_installed(service_name, "uvx"):
|
671
|
+
return True, "uvx"
|
672
|
+
except Exception as e:
|
673
|
+
self.logger.debug(f"uvx install error: {e}")
|
674
|
+
|
675
|
+
# Method 3: Try pip install --user
|
676
|
+
try:
|
677
|
+
self.logger.debug(f"Attempting to install {service_name} via pip --user...")
|
678
|
+
result = subprocess.run(
|
679
|
+
[sys.executable, "-m", "pip", "install", "--user", service_name],
|
680
|
+
capture_output=True,
|
681
|
+
text=True,
|
682
|
+
timeout=120,
|
683
|
+
check=False,
|
684
|
+
)
|
685
|
+
|
686
|
+
if result.returncode == 0:
|
687
|
+
if self._verify_service_installed(service_name, "pip"):
|
688
|
+
return True, "pip --user"
|
689
|
+
|
690
|
+
self.logger.warning(
|
691
|
+
f"pip install succeeded but verification failed for {service_name}"
|
692
|
+
)
|
693
|
+
except Exception as e:
|
694
|
+
self.logger.debug(f"pip install error: {e}")
|
695
|
+
|
696
|
+
return False, "none"
|
697
|
+
|
698
|
+
def _verify_service_installed(self, service_name: str, method: str) -> bool:
|
699
|
+
"""
|
700
|
+
Verify that a service was successfully installed and is functional.
|
701
|
+
|
702
|
+
Args:
|
703
|
+
service_name: Name of the service
|
704
|
+
method: Installation method used
|
705
|
+
|
706
|
+
Returns:
|
707
|
+
True if service is installed and functional
|
708
|
+
"""
|
709
|
+
import time
|
710
|
+
|
711
|
+
# Give the installation a moment to settle
|
712
|
+
time.sleep(1)
|
713
|
+
|
714
|
+
# Check if we can find the service
|
715
|
+
service_path = self.detect_service_path(service_name)
|
716
|
+
if not service_path:
|
717
|
+
# Try pipx run as fallback for pipx installations
|
718
|
+
if method == "pipx":
|
719
|
+
try:
|
720
|
+
result = subprocess.run(
|
721
|
+
["pipx", "run", service_name, "--version"],
|
722
|
+
capture_output=True,
|
723
|
+
text=True,
|
724
|
+
timeout=10,
|
725
|
+
check=False,
|
726
|
+
)
|
727
|
+
if result.returncode == 0 or "version" in result.stdout.lower():
|
728
|
+
self.logger.debug(f"{service_name} accessible via 'pipx run'")
|
729
|
+
return True
|
730
|
+
except:
|
731
|
+
pass
|
732
|
+
return False
|
733
|
+
|
734
|
+
# Try to verify it works
|
735
|
+
try:
|
736
|
+
# Different services may need different verification
|
737
|
+
test_commands = [
|
738
|
+
[service_path, "--version"],
|
739
|
+
[service_path, "--help"],
|
740
|
+
]
|
741
|
+
|
742
|
+
for cmd in test_commands:
|
743
|
+
result = subprocess.run(
|
744
|
+
cmd,
|
745
|
+
capture_output=True,
|
746
|
+
text=True,
|
747
|
+
timeout=10,
|
748
|
+
check=False,
|
749
|
+
)
|
750
|
+
|
751
|
+
output = (result.stdout + result.stderr).lower()
|
752
|
+
# Check for signs of success
|
753
|
+
if result.returncode == 0:
|
754
|
+
return True
|
755
|
+
# Some tools return non-zero but still work
|
756
|
+
if any(
|
757
|
+
indicator in output
|
758
|
+
for indicator in ["version", "usage", "help", service_name.lower()]
|
759
|
+
):
|
760
|
+
# Make sure it's not an error message
|
761
|
+
if not any(
|
762
|
+
error in output
|
763
|
+
for error in ["error", "not found", "traceback", "no such"]
|
764
|
+
):
|
765
|
+
return True
|
766
|
+
except Exception as e:
|
767
|
+
self.logger.debug(f"Verification error for {service_name}: {e}")
|
768
|
+
|
769
|
+
return False
|