code-puppy 0.0.134__py3-none-any.whl → 0.0.136__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.
- code_puppy/agent.py +15 -17
- code_puppy/agents/agent_manager.py +320 -9
- code_puppy/agents/base_agent.py +58 -2
- code_puppy/agents/runtime_manager.py +68 -42
- code_puppy/command_line/command_handler.py +82 -33
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/add_command.py +183 -0
- code_puppy/command_line/mcp/base.py +35 -0
- code_puppy/command_line/mcp/handler.py +133 -0
- code_puppy/command_line/mcp/help_command.py +146 -0
- code_puppy/command_line/mcp/install_command.py +176 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +126 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +92 -0
- code_puppy/command_line/mcp/search_command.py +117 -0
- code_puppy/command_line/mcp/start_all_command.py +126 -0
- code_puppy/command_line/mcp/start_command.py +98 -0
- code_puppy/command_line/mcp/status_command.py +185 -0
- code_puppy/command_line/mcp/stop_all_command.py +109 -0
- code_puppy/command_line/mcp/stop_command.py +79 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +259 -0
- code_puppy/command_line/model_picker_completion.py +21 -4
- code_puppy/command_line/prompt_toolkit_completion.py +9 -0
- code_puppy/main.py +23 -17
- code_puppy/mcp/__init__.py +42 -16
- code_puppy/mcp/async_lifecycle.py +51 -49
- code_puppy/mcp/blocking_startup.py +125 -113
- code_puppy/mcp/captured_stdio_server.py +63 -70
- code_puppy/mcp/circuit_breaker.py +63 -47
- code_puppy/mcp/config_wizard.py +169 -136
- code_puppy/mcp/dashboard.py +79 -71
- code_puppy/mcp/error_isolation.py +147 -100
- code_puppy/mcp/examples/retry_example.py +55 -42
- code_puppy/mcp/health_monitor.py +152 -141
- code_puppy/mcp/managed_server.py +100 -97
- code_puppy/mcp/manager.py +168 -156
- code_puppy/mcp/registry.py +148 -110
- code_puppy/mcp/retry_manager.py +63 -61
- code_puppy/mcp/server_registry_catalog.py +271 -225
- code_puppy/mcp/status_tracker.py +80 -80
- code_puppy/mcp/system_tools.py +47 -52
- code_puppy/messaging/message_queue.py +20 -13
- code_puppy/messaging/renderers.py +30 -15
- code_puppy/state_management.py +103 -0
- code_puppy/tui/app.py +64 -7
- code_puppy/tui/components/chat_view.py +3 -3
- code_puppy/tui/components/human_input_modal.py +12 -8
- code_puppy/tui/screens/__init__.py +2 -2
- code_puppy/tui/screens/mcp_install_wizard.py +208 -179
- code_puppy/tui/tests/test_agent_command.py +3 -3
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/RECORD +59 -41
- code_puppy/command_line/mcp_commands.py +0 -1789
- {code_puppy-0.0.134.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/licenses/LICENSE +0 -0
code_puppy/mcp/status_tracker.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Server Status Tracker for monitoring MCP server runtime status.
|
|
3
3
|
|
|
4
|
-
This module provides the ServerStatusTracker class that tracks the runtime
|
|
4
|
+
This module provides the ServerStatusTracker class that tracks the runtime
|
|
5
5
|
status of MCP servers including state, metrics, and events.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
9
|
import threading
|
|
10
|
-
from collections import
|
|
10
|
+
from collections import defaultdict, deque
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from datetime import datetime, timedelta
|
|
13
13
|
from typing import Any, Dict, List, Optional
|
|
@@ -21,6 +21,7 @@ logger = logging.getLogger(__name__)
|
|
|
21
21
|
@dataclass
|
|
22
22
|
class Event:
|
|
23
23
|
"""Data class representing a server event."""
|
|
24
|
+
|
|
24
25
|
timestamp: datetime
|
|
25
26
|
event_type: str # "started", "stopped", "error", "health_check", etc.
|
|
26
27
|
details: Dict
|
|
@@ -30,43 +31,43 @@ class Event:
|
|
|
30
31
|
class ServerStatusTracker:
|
|
31
32
|
"""
|
|
32
33
|
Tracks the runtime status of MCP servers including state, metrics, and events.
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
This class provides in-memory storage for server states, metadata, and events
|
|
35
36
|
with thread-safe operations using locks. Events are stored using collections.deque
|
|
36
37
|
for automatic size limiting.
|
|
37
|
-
|
|
38
|
+
|
|
38
39
|
Example usage:
|
|
39
40
|
tracker = ServerStatusTracker()
|
|
40
41
|
tracker.set_status("server1", ServerState.RUNNING)
|
|
41
42
|
tracker.record_event("server1", "started", {"message": "Server started successfully"})
|
|
42
43
|
events = tracker.get_events("server1", limit=10)
|
|
43
44
|
"""
|
|
44
|
-
|
|
45
|
+
|
|
45
46
|
def __init__(self):
|
|
46
47
|
"""Initialize the status tracker with thread-safe data structures."""
|
|
47
48
|
# Thread safety lock
|
|
48
49
|
self._lock = threading.RLock()
|
|
49
|
-
|
|
50
|
+
|
|
50
51
|
# Server states (server_id -> ServerState)
|
|
51
52
|
self._server_states: Dict[str, ServerState] = {}
|
|
52
|
-
|
|
53
|
+
|
|
53
54
|
# Server metadata (server_id -> key -> value)
|
|
54
55
|
self._server_metadata: Dict[str, Dict[str, Any]] = defaultdict(dict)
|
|
55
|
-
|
|
56
|
+
|
|
56
57
|
# Server events (server_id -> deque of events)
|
|
57
58
|
# Using deque with maxlen for automatic size limiting
|
|
58
59
|
self._server_events: Dict[str, deque] = defaultdict(lambda: deque(maxlen=1000))
|
|
59
|
-
|
|
60
|
+
|
|
60
61
|
# Server timing information
|
|
61
62
|
self._start_times: Dict[str, datetime] = {}
|
|
62
63
|
self._stop_times: Dict[str, datetime] = {}
|
|
63
|
-
|
|
64
|
+
|
|
64
65
|
logger.info("ServerStatusTracker initialized")
|
|
65
|
-
|
|
66
|
+
|
|
66
67
|
def set_status(self, server_id: str, state: ServerState) -> None:
|
|
67
68
|
"""
|
|
68
69
|
Set the current state of a server.
|
|
69
|
-
|
|
70
|
+
|
|
70
71
|
Args:
|
|
71
72
|
server_id: Unique identifier for the server
|
|
72
73
|
state: New server state
|
|
@@ -74,7 +75,7 @@ class ServerStatusTracker:
|
|
|
74
75
|
with self._lock:
|
|
75
76
|
old_state = self._server_states.get(server_id)
|
|
76
77
|
self._server_states[server_id] = state
|
|
77
|
-
|
|
78
|
+
|
|
78
79
|
# Record state change event
|
|
79
80
|
self.record_event(
|
|
80
81
|
server_id,
|
|
@@ -82,29 +83,29 @@ class ServerStatusTracker:
|
|
|
82
83
|
{
|
|
83
84
|
"old_state": old_state.value if old_state else None,
|
|
84
85
|
"new_state": state.value,
|
|
85
|
-
"message": f"State changed from {old_state.value if old_state else 'unknown'} to {state.value}"
|
|
86
|
-
}
|
|
86
|
+
"message": f"State changed from {old_state.value if old_state else 'unknown'} to {state.value}",
|
|
87
|
+
},
|
|
87
88
|
)
|
|
88
|
-
|
|
89
|
+
|
|
89
90
|
logger.debug(f"Server {server_id} state changed: {old_state} -> {state}")
|
|
90
|
-
|
|
91
|
+
|
|
91
92
|
def get_status(self, server_id: str) -> ServerState:
|
|
92
93
|
"""
|
|
93
94
|
Get the current state of a server.
|
|
94
|
-
|
|
95
|
+
|
|
95
96
|
Args:
|
|
96
97
|
server_id: Unique identifier for the server
|
|
97
|
-
|
|
98
|
+
|
|
98
99
|
Returns:
|
|
99
100
|
Current server state, defaults to STOPPED if not found
|
|
100
101
|
"""
|
|
101
102
|
with self._lock:
|
|
102
103
|
return self._server_states.get(server_id, ServerState.STOPPED)
|
|
103
|
-
|
|
104
|
+
|
|
104
105
|
def set_metadata(self, server_id: str, key: str, value: Any) -> None:
|
|
105
106
|
"""
|
|
106
107
|
Set metadata value for a server.
|
|
107
|
-
|
|
108
|
+
|
|
108
109
|
Args:
|
|
109
110
|
server_id: Unique identifier for the server
|
|
110
111
|
key: Metadata key
|
|
@@ -113,10 +114,10 @@ class ServerStatusTracker:
|
|
|
113
114
|
with self._lock:
|
|
114
115
|
if server_id not in self._server_metadata:
|
|
115
116
|
self._server_metadata[server_id] = {}
|
|
116
|
-
|
|
117
|
+
|
|
117
118
|
old_value = self._server_metadata[server_id].get(key)
|
|
118
119
|
self._server_metadata[server_id][key] = value
|
|
119
|
-
|
|
120
|
+
|
|
120
121
|
# Record metadata change event
|
|
121
122
|
self.record_event(
|
|
122
123
|
server_id,
|
|
@@ -125,30 +126,30 @@ class ServerStatusTracker:
|
|
|
125
126
|
"key": key,
|
|
126
127
|
"old_value": old_value,
|
|
127
128
|
"new_value": value,
|
|
128
|
-
"message": f"Metadata '{key}' updated"
|
|
129
|
-
}
|
|
129
|
+
"message": f"Metadata '{key}' updated",
|
|
130
|
+
},
|
|
130
131
|
)
|
|
131
|
-
|
|
132
|
+
|
|
132
133
|
logger.debug(f"Server {server_id} metadata updated: {key} = {value}")
|
|
133
|
-
|
|
134
|
+
|
|
134
135
|
def get_metadata(self, server_id: str, key: str) -> Any:
|
|
135
136
|
"""
|
|
136
137
|
Get metadata value for a server.
|
|
137
|
-
|
|
138
|
+
|
|
138
139
|
Args:
|
|
139
140
|
server_id: Unique identifier for the server
|
|
140
141
|
key: Metadata key
|
|
141
|
-
|
|
142
|
+
|
|
142
143
|
Returns:
|
|
143
144
|
Metadata value or None if not found
|
|
144
145
|
"""
|
|
145
146
|
with self._lock:
|
|
146
147
|
return self._server_metadata.get(server_id, {}).get(key)
|
|
147
|
-
|
|
148
|
+
|
|
148
149
|
def record_event(self, server_id: str, event_type: str, details: Dict) -> None:
|
|
149
150
|
"""
|
|
150
151
|
Record an event for a server.
|
|
151
|
-
|
|
152
|
+
|
|
152
153
|
Args:
|
|
153
154
|
server_id: Unique identifier for the server
|
|
154
155
|
event_type: Type of event (e.g., "started", "stopped", "error", "health_check")
|
|
@@ -158,37 +159,39 @@ class ServerStatusTracker:
|
|
|
158
159
|
event = Event(
|
|
159
160
|
timestamp=datetime.now(),
|
|
160
161
|
event_type=event_type,
|
|
161
|
-
details=details.copy()
|
|
162
|
-
|
|
162
|
+
details=details.copy()
|
|
163
|
+
if details
|
|
164
|
+
else {}, # Copy to prevent modification
|
|
165
|
+
server_id=server_id,
|
|
163
166
|
)
|
|
164
|
-
|
|
167
|
+
|
|
165
168
|
# Add to deque (automatically handles size limiting)
|
|
166
169
|
self._server_events[server_id].append(event)
|
|
167
|
-
|
|
170
|
+
|
|
168
171
|
logger.debug(f"Event recorded for server {server_id}: {event_type}")
|
|
169
|
-
|
|
172
|
+
|
|
170
173
|
def get_events(self, server_id: str, limit: int = 100) -> List[Event]:
|
|
171
174
|
"""
|
|
172
175
|
Get recent events for a server.
|
|
173
|
-
|
|
176
|
+
|
|
174
177
|
Args:
|
|
175
178
|
server_id: Unique identifier for the server
|
|
176
179
|
limit: Maximum number of events to return (default: 100)
|
|
177
|
-
|
|
180
|
+
|
|
178
181
|
Returns:
|
|
179
182
|
List of events ordered by timestamp (most recent first)
|
|
180
183
|
"""
|
|
181
184
|
with self._lock:
|
|
182
185
|
events = list(self._server_events.get(server_id, deque()))
|
|
183
|
-
|
|
186
|
+
|
|
184
187
|
# Return most recent events first, limited by count
|
|
185
188
|
events.reverse() # Most recent first
|
|
186
189
|
return events[:limit]
|
|
187
|
-
|
|
190
|
+
|
|
188
191
|
def clear_events(self, server_id: str) -> None:
|
|
189
192
|
"""
|
|
190
193
|
Clear all events for a server.
|
|
191
|
-
|
|
194
|
+
|
|
192
195
|
Args:
|
|
193
196
|
server_id: Unique identifier for the server
|
|
194
197
|
"""
|
|
@@ -196,14 +199,14 @@ class ServerStatusTracker:
|
|
|
196
199
|
if server_id in self._server_events:
|
|
197
200
|
self._server_events[server_id].clear()
|
|
198
201
|
logger.info(f"Cleared all events for server: {server_id}")
|
|
199
|
-
|
|
202
|
+
|
|
200
203
|
def get_uptime(self, server_id: str) -> Optional[timedelta]:
|
|
201
204
|
"""
|
|
202
205
|
Calculate uptime for a server based on start/stop times.
|
|
203
|
-
|
|
206
|
+
|
|
204
207
|
Args:
|
|
205
208
|
server_id: Unique identifier for the server
|
|
206
|
-
|
|
209
|
+
|
|
207
210
|
Returns:
|
|
208
211
|
Server uptime as timedelta, or None if server never started
|
|
209
212
|
"""
|
|
@@ -211,60 +214,57 @@ class ServerStatusTracker:
|
|
|
211
214
|
start_time = self._start_times.get(server_id)
|
|
212
215
|
if start_time is None:
|
|
213
216
|
return None
|
|
214
|
-
|
|
217
|
+
|
|
215
218
|
# If server is currently running, calculate from start time to now
|
|
216
219
|
current_state = self.get_status(server_id)
|
|
217
220
|
if current_state == ServerState.RUNNING:
|
|
218
221
|
return datetime.now() - start_time
|
|
219
|
-
|
|
222
|
+
|
|
220
223
|
# If server is stopped, calculate from start to stop time
|
|
221
224
|
stop_time = self._stop_times.get(server_id)
|
|
222
225
|
if stop_time is not None and stop_time > start_time:
|
|
223
226
|
return stop_time - start_time
|
|
224
|
-
|
|
227
|
+
|
|
225
228
|
# If we have start time but no valid stop time, assume currently running
|
|
226
229
|
return datetime.now() - start_time
|
|
227
|
-
|
|
230
|
+
|
|
228
231
|
def record_start_time(self, server_id: str) -> None:
|
|
229
232
|
"""
|
|
230
233
|
Record the start time for a server.
|
|
231
|
-
|
|
234
|
+
|
|
232
235
|
Args:
|
|
233
236
|
server_id: Unique identifier for the server
|
|
234
237
|
"""
|
|
235
238
|
with self._lock:
|
|
236
239
|
start_time = datetime.now()
|
|
237
240
|
self._start_times[server_id] = start_time
|
|
238
|
-
|
|
241
|
+
|
|
239
242
|
# Record start event
|
|
240
243
|
self.record_event(
|
|
241
244
|
server_id,
|
|
242
245
|
"started",
|
|
243
|
-
{
|
|
244
|
-
"start_time": start_time.isoformat(),
|
|
245
|
-
"message": "Server started"
|
|
246
|
-
}
|
|
246
|
+
{"start_time": start_time.isoformat(), "message": "Server started"},
|
|
247
247
|
)
|
|
248
|
-
|
|
248
|
+
|
|
249
249
|
logger.info(f"Recorded start time for server: {server_id}")
|
|
250
|
-
|
|
250
|
+
|
|
251
251
|
def record_stop_time(self, server_id: str) -> None:
|
|
252
252
|
"""
|
|
253
253
|
Record the stop time for a server.
|
|
254
|
-
|
|
254
|
+
|
|
255
255
|
Args:
|
|
256
256
|
server_id: Unique identifier for the server
|
|
257
257
|
"""
|
|
258
258
|
with self._lock:
|
|
259
259
|
stop_time = datetime.now()
|
|
260
260
|
self._stop_times[server_id] = stop_time
|
|
261
|
-
|
|
261
|
+
|
|
262
262
|
# Calculate final uptime
|
|
263
263
|
start_time = self._start_times.get(server_id)
|
|
264
264
|
uptime = None
|
|
265
265
|
if start_time:
|
|
266
266
|
uptime = stop_time - start_time
|
|
267
|
-
|
|
267
|
+
|
|
268
268
|
# Record stop event
|
|
269
269
|
self.record_event(
|
|
270
270
|
server_id,
|
|
@@ -272,16 +272,16 @@ class ServerStatusTracker:
|
|
|
272
272
|
{
|
|
273
273
|
"stop_time": stop_time.isoformat(),
|
|
274
274
|
"uptime_seconds": uptime.total_seconds() if uptime else None,
|
|
275
|
-
"message": "Server stopped"
|
|
276
|
-
}
|
|
275
|
+
"message": "Server stopped",
|
|
276
|
+
},
|
|
277
277
|
)
|
|
278
|
-
|
|
278
|
+
|
|
279
279
|
logger.info(f"Recorded stop time for server: {server_id}")
|
|
280
|
-
|
|
280
|
+
|
|
281
281
|
def get_all_server_ids(self) -> List[str]:
|
|
282
282
|
"""
|
|
283
283
|
Get all server IDs that have been tracked.
|
|
284
|
-
|
|
284
|
+
|
|
285
285
|
Returns:
|
|
286
286
|
List of all server IDs
|
|
287
287
|
"""
|
|
@@ -293,16 +293,16 @@ class ServerStatusTracker:
|
|
|
293
293
|
all_ids.update(self._server_events.keys())
|
|
294
294
|
all_ids.update(self._start_times.keys())
|
|
295
295
|
all_ids.update(self._stop_times.keys())
|
|
296
|
-
|
|
296
|
+
|
|
297
297
|
return sorted(list(all_ids))
|
|
298
|
-
|
|
298
|
+
|
|
299
299
|
def get_server_summary(self, server_id: str) -> Dict[str, Any]:
|
|
300
300
|
"""
|
|
301
301
|
Get comprehensive summary of server status.
|
|
302
|
-
|
|
302
|
+
|
|
303
303
|
Args:
|
|
304
304
|
server_id: Unique identifier for the server
|
|
305
|
-
|
|
305
|
+
|
|
306
306
|
Returns:
|
|
307
307
|
Dictionary containing current state, metadata, recent events, and uptime
|
|
308
308
|
"""
|
|
@@ -317,23 +317,24 @@ class ServerStatusTracker:
|
|
|
317
317
|
"stop_time": self._stop_times.get(server_id),
|
|
318
318
|
"last_event_time": (
|
|
319
319
|
list(self._server_events.get(server_id, deque()))[-1].timestamp
|
|
320
|
-
if server_id in self._server_events
|
|
320
|
+
if server_id in self._server_events
|
|
321
|
+
and len(self._server_events[server_id]) > 0
|
|
321
322
|
else None
|
|
322
|
-
)
|
|
323
|
+
),
|
|
323
324
|
}
|
|
324
|
-
|
|
325
|
+
|
|
325
326
|
def cleanup_old_data(self, days_to_keep: int = 7) -> None:
|
|
326
327
|
"""
|
|
327
328
|
Clean up old data to prevent memory bloat.
|
|
328
|
-
|
|
329
|
+
|
|
329
330
|
Args:
|
|
330
331
|
days_to_keep: Number of days of data to keep (default: 7)
|
|
331
332
|
"""
|
|
332
333
|
cutoff_time = datetime.now() - timedelta(days=days_to_keep)
|
|
333
|
-
|
|
334
|
+
|
|
334
335
|
with self._lock:
|
|
335
336
|
cleaned_servers = []
|
|
336
|
-
|
|
337
|
+
|
|
337
338
|
for server_id in list(self._server_events.keys()):
|
|
338
339
|
events = self._server_events[server_id]
|
|
339
340
|
if events:
|
|
@@ -341,15 +342,14 @@ class ServerStatusTracker:
|
|
|
341
342
|
original_count = len(events)
|
|
342
343
|
# Convert to list, filter, then create new deque
|
|
343
344
|
filtered_events = [
|
|
344
|
-
event for event in events
|
|
345
|
-
if event.timestamp >= cutoff_time
|
|
345
|
+
event for event in events if event.timestamp >= cutoff_time
|
|
346
346
|
]
|
|
347
|
-
|
|
347
|
+
|
|
348
348
|
# Replace the deque with filtered events
|
|
349
349
|
self._server_events[server_id] = deque(filtered_events, maxlen=1000)
|
|
350
|
-
|
|
350
|
+
|
|
351
351
|
if len(filtered_events) < original_count:
|
|
352
352
|
cleaned_servers.append(server_id)
|
|
353
|
-
|
|
353
|
+
|
|
354
354
|
if cleaned_servers:
|
|
355
|
-
logger.info(f"Cleaned old events for {len(cleaned_servers)} servers")
|
|
355
|
+
logger.info(f"Cleaned old events for {len(cleaned_servers)} servers")
|
code_puppy/mcp/system_tools.py
CHANGED
|
@@ -4,13 +4,14 @@ System tool detection and validation for MCP server requirements.
|
|
|
4
4
|
|
|
5
5
|
import shutil
|
|
6
6
|
import subprocess
|
|
7
|
-
from typing import Dict, List, Optional, Tuple
|
|
8
7
|
from dataclasses import dataclass
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@dataclass
|
|
12
12
|
class ToolInfo:
|
|
13
13
|
"""Information about a detected system tool."""
|
|
14
|
+
|
|
14
15
|
name: str
|
|
15
16
|
available: bool
|
|
16
17
|
version: Optional[str] = None
|
|
@@ -20,7 +21,7 @@ class ToolInfo:
|
|
|
20
21
|
|
|
21
22
|
class SystemToolDetector:
|
|
22
23
|
"""Detect and validate system tools required by MCP servers."""
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
# Tool version commands
|
|
25
26
|
VERSION_COMMANDS = {
|
|
26
27
|
"node": ["node", "--version"],
|
|
@@ -48,112 +49,105 @@ class SystemToolDetector:
|
|
|
48
49
|
"vim": ["vim", "--version"],
|
|
49
50
|
"emacs": ["emacs", "--version"],
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
+
|
|
52
53
|
@classmethod
|
|
53
54
|
def detect_tool(cls, tool_name: str) -> ToolInfo:
|
|
54
55
|
"""Detect if a tool is available and get its version."""
|
|
55
56
|
# First check if tool is in PATH
|
|
56
57
|
tool_path = shutil.which(tool_name)
|
|
57
|
-
|
|
58
|
+
|
|
58
59
|
if not tool_path:
|
|
59
60
|
return ToolInfo(
|
|
60
|
-
name=tool_name,
|
|
61
|
-
available=False,
|
|
62
|
-
error=f"{tool_name} not found in PATH"
|
|
61
|
+
name=tool_name, available=False, error=f"{tool_name} not found in PATH"
|
|
63
62
|
)
|
|
64
|
-
|
|
63
|
+
|
|
65
64
|
# Try to get version
|
|
66
65
|
version_cmd = cls.VERSION_COMMANDS.get(tool_name)
|
|
67
66
|
version = None
|
|
68
67
|
error = None
|
|
69
|
-
|
|
68
|
+
|
|
70
69
|
if version_cmd:
|
|
71
70
|
try:
|
|
72
71
|
# Run version command
|
|
73
72
|
result = subprocess.run(
|
|
74
|
-
version_cmd,
|
|
75
|
-
capture_output=True,
|
|
76
|
-
text=True,
|
|
77
|
-
timeout=10
|
|
73
|
+
version_cmd, capture_output=True, text=True, timeout=10
|
|
78
74
|
)
|
|
79
|
-
|
|
75
|
+
|
|
80
76
|
if result.returncode == 0:
|
|
81
77
|
# Parse version from output
|
|
82
78
|
output = result.stdout.strip() or result.stderr.strip()
|
|
83
79
|
version = cls._parse_version(tool_name, output)
|
|
84
80
|
else:
|
|
85
81
|
error = f"Version check failed: {result.stderr.strip()}"
|
|
86
|
-
|
|
82
|
+
|
|
87
83
|
except subprocess.TimeoutExpired:
|
|
88
84
|
error = "Version check timed out"
|
|
89
85
|
except Exception as e:
|
|
90
86
|
error = f"Version check error: {str(e)}"
|
|
91
|
-
|
|
87
|
+
|
|
92
88
|
return ToolInfo(
|
|
93
|
-
name=tool_name,
|
|
94
|
-
available=True,
|
|
95
|
-
version=version,
|
|
96
|
-
path=tool_path,
|
|
97
|
-
error=error
|
|
89
|
+
name=tool_name, available=True, version=version, path=tool_path, error=error
|
|
98
90
|
)
|
|
99
|
-
|
|
91
|
+
|
|
100
92
|
@classmethod
|
|
101
93
|
def detect_tools(cls, tool_names: List[str]) -> Dict[str, ToolInfo]:
|
|
102
94
|
"""Detect multiple tools."""
|
|
103
95
|
return {name: cls.detect_tool(name) for name in tool_names}
|
|
104
|
-
|
|
96
|
+
|
|
105
97
|
@classmethod
|
|
106
98
|
def _parse_version(cls, tool_name: str, output: str) -> Optional[str]:
|
|
107
99
|
"""Parse version string from command output."""
|
|
108
100
|
if not output:
|
|
109
101
|
return None
|
|
110
|
-
|
|
102
|
+
|
|
111
103
|
# Common version patterns
|
|
112
104
|
import re
|
|
113
|
-
|
|
105
|
+
|
|
114
106
|
# Try to find version pattern like "v1.2.3" or "1.2.3"
|
|
115
107
|
version_patterns = [
|
|
116
|
-
r
|
|
117
|
-
r
|
|
118
|
-
r
|
|
119
|
-
r
|
|
108
|
+
r"v?(\d+\.\d+\.\d+(?:\.\d+)?)", # Standard semver
|
|
109
|
+
r"(\d+\.\d+\.\d+)", # Simple version
|
|
110
|
+
r"version\s+v?(\d+\.\d+\.\d+)", # "version 1.2.3"
|
|
111
|
+
r"v?(\d+\.\d+)", # Major.minor only
|
|
120
112
|
]
|
|
121
|
-
|
|
113
|
+
|
|
122
114
|
for pattern in version_patterns:
|
|
123
115
|
match = re.search(pattern, output, re.IGNORECASE)
|
|
124
116
|
if match:
|
|
125
117
|
return match.group(1)
|
|
126
|
-
|
|
118
|
+
|
|
127
119
|
# If no pattern matches, return first line (common for many tools)
|
|
128
|
-
first_line = output.split(
|
|
120
|
+
first_line = output.split("\n")[0].strip()
|
|
129
121
|
if len(first_line) < 100: # Reasonable length for a version string
|
|
130
122
|
return first_line
|
|
131
|
-
|
|
123
|
+
|
|
132
124
|
return None
|
|
133
|
-
|
|
125
|
+
|
|
134
126
|
@classmethod
|
|
135
127
|
def check_package_dependencies(cls, packages: List[str]) -> Dict[str, bool]:
|
|
136
128
|
"""Check if package dependencies are available."""
|
|
137
129
|
results = {}
|
|
138
|
-
|
|
130
|
+
|
|
139
131
|
for package in packages:
|
|
140
132
|
available = False
|
|
141
|
-
|
|
133
|
+
|
|
142
134
|
# Try different package managers/methods
|
|
143
|
-
if package.startswith(
|
|
135
|
+
if package.startswith("@") or "/" in package:
|
|
144
136
|
# Likely npm package
|
|
145
137
|
available = cls._check_npm_package(package)
|
|
146
|
-
elif package in [
|
|
138
|
+
elif package in ["jupyter", "pandas", "numpy", "matplotlib"]:
|
|
147
139
|
# Python packages
|
|
148
140
|
available = cls._check_python_package(package)
|
|
149
141
|
else:
|
|
150
142
|
# Try both npm and python
|
|
151
|
-
available = cls._check_npm_package(
|
|
152
|
-
|
|
143
|
+
available = cls._check_npm_package(
|
|
144
|
+
package
|
|
145
|
+
) or cls._check_python_package(package)
|
|
146
|
+
|
|
153
147
|
results[package] = available
|
|
154
|
-
|
|
148
|
+
|
|
155
149
|
return results
|
|
156
|
-
|
|
150
|
+
|
|
157
151
|
@classmethod
|
|
158
152
|
def _check_npm_package(cls, package: str) -> bool:
|
|
159
153
|
"""Check if an npm package is available."""
|
|
@@ -162,53 +156,54 @@ class SystemToolDetector:
|
|
|
162
156
|
["npm", "list", "-g", package],
|
|
163
157
|
capture_output=True,
|
|
164
158
|
text=True,
|
|
165
|
-
timeout=10
|
|
159
|
+
timeout=10,
|
|
166
160
|
)
|
|
167
161
|
return result.returncode == 0
|
|
168
|
-
except:
|
|
162
|
+
except Exception:
|
|
169
163
|
return False
|
|
170
|
-
|
|
164
|
+
|
|
171
165
|
@classmethod
|
|
172
166
|
def _check_python_package(cls, package: str) -> bool:
|
|
173
167
|
"""Check if a Python package is available."""
|
|
174
168
|
try:
|
|
175
169
|
import importlib
|
|
170
|
+
|
|
176
171
|
importlib.import_module(package)
|
|
177
172
|
return True
|
|
178
173
|
except ImportError:
|
|
179
174
|
return False
|
|
180
|
-
|
|
175
|
+
|
|
181
176
|
@classmethod
|
|
182
177
|
def get_installation_suggestions(cls, tool_name: str) -> List[str]:
|
|
183
178
|
"""Get installation suggestions for a missing tool."""
|
|
184
179
|
suggestions = {
|
|
185
180
|
"node": [
|
|
186
181
|
"Install Node.js from https://nodejs.org",
|
|
187
|
-
"Or use package manager: brew install node (macOS) / sudo apt install nodejs (Ubuntu)"
|
|
182
|
+
"Or use package manager: brew install node (macOS) / sudo apt install nodejs (Ubuntu)",
|
|
188
183
|
],
|
|
189
184
|
"npm": ["Usually comes with Node.js - install Node.js first"],
|
|
190
185
|
"npx": ["Usually comes with npm 5.2+ - update npm: npm install -g npm"],
|
|
191
186
|
"python": [
|
|
192
187
|
"Install Python from https://python.org",
|
|
193
|
-
"Or use package manager: brew install python (macOS) / sudo apt install python3 (Ubuntu)"
|
|
188
|
+
"Or use package manager: brew install python (macOS) / sudo apt install python3 (Ubuntu)",
|
|
194
189
|
],
|
|
195
190
|
"python3": ["Same as python - install Python 3.x"],
|
|
196
191
|
"pip": ["Usually comes with Python - try: python -m ensurepip"],
|
|
197
192
|
"pip3": ["Usually comes with Python 3 - try: python3 -m ensurepip"],
|
|
198
193
|
"git": [
|
|
199
194
|
"Install Git from https://git-scm.com",
|
|
200
|
-
"Or use package manager: brew install git (macOS) / sudo apt install git (Ubuntu)"
|
|
195
|
+
"Or use package manager: brew install git (macOS) / sudo apt install git (Ubuntu)",
|
|
201
196
|
],
|
|
202
197
|
"docker": ["Install Docker from https://docker.com"],
|
|
203
198
|
"java": [
|
|
204
199
|
"Install OpenJDK from https://openjdk.java.net",
|
|
205
|
-
"Or use package manager: brew install openjdk (macOS) / sudo apt install default-jdk (Ubuntu)"
|
|
200
|
+
"Or use package manager: brew install openjdk (macOS) / sudo apt install default-jdk (Ubuntu)",
|
|
206
201
|
],
|
|
207
202
|
"jupyter": ["Install with pip: pip install jupyter"],
|
|
208
203
|
}
|
|
209
|
-
|
|
204
|
+
|
|
210
205
|
return suggestions.get(tool_name, [f"Please install {tool_name} manually"])
|
|
211
206
|
|
|
212
207
|
|
|
213
208
|
# Global detector instance
|
|
214
|
-
detector = SystemToolDetector()
|
|
209
|
+
detector = SystemToolDetector()
|