crackerjack 0.30.3__py3-none-any.whl → 0.31.7__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.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +227 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +170 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +657 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +409 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +618 -928
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +585 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +826 -0
- crackerjack/dynamic_config.py +94 -103
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +433 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +443 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +114 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +621 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +372 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +217 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +565 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/coverage_improvement.py +223 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +358 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +356 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +421 -0
- crackerjack/services/git.py +176 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +873 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.7.dist-info/METADATA +742 -0
- crackerjack-0.31.7.dist-info/RECORD +149 -0
- crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JobDataCollector:
|
|
15
|
+
def __init__(self, progress_dir: Path, websocket_url: str) -> None:
|
|
16
|
+
self.progress_dir = progress_dir
|
|
17
|
+
self.websocket_url = websocket_url
|
|
18
|
+
self.console = Console()
|
|
19
|
+
|
|
20
|
+
async def discover_jobs(self) -> dict[str, Any]:
|
|
21
|
+
jobs_data = self._init_jobs_data()
|
|
22
|
+
|
|
23
|
+
websocket_jobs = await self._discover_jobs_websocket()
|
|
24
|
+
if websocket_jobs and websocket_jobs["total"] > 0:
|
|
25
|
+
return {"method": "WebSocket", "data": websocket_jobs}
|
|
26
|
+
|
|
27
|
+
filesystem_jobs = await self._discover_jobs_filesystem(jobs_data)
|
|
28
|
+
return {"method": "File", "data": filesystem_jobs}
|
|
29
|
+
|
|
30
|
+
def _init_jobs_data(self) -> dict[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"active": 0,
|
|
33
|
+
"completed": 0,
|
|
34
|
+
"failed": 0,
|
|
35
|
+
"total": 0,
|
|
36
|
+
"individual_jobs": [],
|
|
37
|
+
"total_issues": 0,
|
|
38
|
+
"errors_fixed": 0,
|
|
39
|
+
"errors_failed": 0,
|
|
40
|
+
"current_errors": 0,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async def _discover_jobs_filesystem(
|
|
44
|
+
self,
|
|
45
|
+
jobs_data: dict[str, Any],
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
with suppress(Exception):
|
|
48
|
+
if not self.progress_dir.exists():
|
|
49
|
+
return jobs_data
|
|
50
|
+
|
|
51
|
+
for progress_file in self.progress_dir.glob("job -* .json"):
|
|
52
|
+
self._process_progress_file(progress_file, jobs_data)
|
|
53
|
+
|
|
54
|
+
return jobs_data
|
|
55
|
+
|
|
56
|
+
def _process_progress_file(
|
|
57
|
+
self,
|
|
58
|
+
progress_file: Path,
|
|
59
|
+
jobs_data: dict[str, Any],
|
|
60
|
+
) -> None:
|
|
61
|
+
with suppress(json.JSONDecodeError, OSError):
|
|
62
|
+
with progress_file.open() as f:
|
|
63
|
+
data = json.load(f)
|
|
64
|
+
|
|
65
|
+
job_id = progress_file.stem.replace("job - ", "")
|
|
66
|
+
self._update_job_counters(data, jobs_data)
|
|
67
|
+
self._aggregate_error_metrics(data, jobs_data)
|
|
68
|
+
self._add_individual_job(job_id, data, jobs_data)
|
|
69
|
+
|
|
70
|
+
def _update_job_counters(
|
|
71
|
+
self,
|
|
72
|
+
data: dict[str, Any],
|
|
73
|
+
jobs_data: dict[str, Any],
|
|
74
|
+
) -> None:
|
|
75
|
+
status = data.get("status", "unknown")
|
|
76
|
+
if status == "running":
|
|
77
|
+
jobs_data["active"] += 1
|
|
78
|
+
elif status == "completed":
|
|
79
|
+
jobs_data["completed"] += 1
|
|
80
|
+
elif status == "failed":
|
|
81
|
+
jobs_data["failed"] += 1
|
|
82
|
+
jobs_data["total"] += 1
|
|
83
|
+
|
|
84
|
+
def _aggregate_error_metrics(
|
|
85
|
+
self,
|
|
86
|
+
data: dict[str, Any],
|
|
87
|
+
jobs_data: dict[str, Any],
|
|
88
|
+
) -> None:
|
|
89
|
+
jobs_data["total_issues"] += data.get("total_issues", 0)
|
|
90
|
+
jobs_data["errors_fixed"] += data.get("errors_fixed", 0)
|
|
91
|
+
jobs_data["errors_failed"] += data.get("errors_failed", 0)
|
|
92
|
+
|
|
93
|
+
current_errors_data = data.get("current_errors", {})
|
|
94
|
+
current_errors = (
|
|
95
|
+
current_errors_data.get("total", 0)
|
|
96
|
+
if isinstance(current_errors_data, dict)
|
|
97
|
+
else 0
|
|
98
|
+
)
|
|
99
|
+
jobs_data["current_errors"] += current_errors
|
|
100
|
+
|
|
101
|
+
def _add_individual_job(
|
|
102
|
+
self,
|
|
103
|
+
job_id: str,
|
|
104
|
+
data: dict[str, Any],
|
|
105
|
+
jobs_data: dict[str, Any],
|
|
106
|
+
) -> None:
|
|
107
|
+
status = data.get("status", "unknown")
|
|
108
|
+
stage = data.get("current_stage", "Unknown")
|
|
109
|
+
iteration = data.get("iteration", 0)
|
|
110
|
+
max_iterations = data.get("max_iterations", 10)
|
|
111
|
+
|
|
112
|
+
status_emoji = {
|
|
113
|
+
"running": "🚀 Running",
|
|
114
|
+
"completed": "✅ Done",
|
|
115
|
+
}.get(status, "❌ Error")
|
|
116
|
+
|
|
117
|
+
jobs_data["individual_jobs"].append(
|
|
118
|
+
{
|
|
119
|
+
"job_id": job_id[:8],
|
|
120
|
+
"full_job_id": job_id,
|
|
121
|
+
"project": data.get("project_name", "crackerjack"),
|
|
122
|
+
"stage": stage,
|
|
123
|
+
"progress": f"{iteration} / {max_iterations}",
|
|
124
|
+
"status": status_emoji,
|
|
125
|
+
"iteration": iteration,
|
|
126
|
+
"max_iterations": max_iterations,
|
|
127
|
+
"message": data.get("message", "Processing job..."),
|
|
128
|
+
"errors": data.get("errors", []),
|
|
129
|
+
"hook_failures": data.get("hook_failures", []),
|
|
130
|
+
"test_failures": data.get("test_failures", []),
|
|
131
|
+
"total_issues": data.get("total_issues", 0),
|
|
132
|
+
"errors_fixed": data.get("errors_fixed", 0),
|
|
133
|
+
"errors_failed": data.get("errors_failed", 0),
|
|
134
|
+
"current_errors": data.get("current_errors", {}),
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
async def _discover_jobs_websocket(self) -> dict[str, Any]:
|
|
139
|
+
jobs_data = {
|
|
140
|
+
"active": 0,
|
|
141
|
+
"completed": 0,
|
|
142
|
+
"failed": 0,
|
|
143
|
+
"total": 0,
|
|
144
|
+
"individual_jobs": [],
|
|
145
|
+
"total_issues": 0,
|
|
146
|
+
"errors_fixed": 0,
|
|
147
|
+
"errors_failed": 0,
|
|
148
|
+
"current_errors": 0,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
with suppress(Exception):
|
|
152
|
+
websocket_base = self.websocket_url.replace("ws: // ", "http: // ").replace(
|
|
153
|
+
"wss: // ",
|
|
154
|
+
"https: // ",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
async with (
|
|
158
|
+
aiohttp.ClientSession(
|
|
159
|
+
timeout=aiohttp.ClientTimeout(total=3),
|
|
160
|
+
) as session,
|
|
161
|
+
session.get(f"{websocket_base} / ") as response,
|
|
162
|
+
):
|
|
163
|
+
if response.status == 200:
|
|
164
|
+
data = await response.json()
|
|
165
|
+
|
|
166
|
+
active_jobs = data.get("active_jobs_detailed", [])
|
|
167
|
+
|
|
168
|
+
for job in active_jobs:
|
|
169
|
+
job_id = job.get("job_id", "unknown")
|
|
170
|
+
status = job.get("status", "unknown")
|
|
171
|
+
|
|
172
|
+
if status == "running":
|
|
173
|
+
jobs_data["active"] += 1
|
|
174
|
+
elif status == "completed":
|
|
175
|
+
jobs_data["completed"] += 1
|
|
176
|
+
elif status == "failed":
|
|
177
|
+
jobs_data["failed"] += 1
|
|
178
|
+
|
|
179
|
+
jobs_data["total"] += 1
|
|
180
|
+
|
|
181
|
+
job_entry = {
|
|
182
|
+
"job_id": job_id,
|
|
183
|
+
"status": status,
|
|
184
|
+
"iteration": job.get("iteration", 1),
|
|
185
|
+
"max_iterations": job.get("max_iterations", 10),
|
|
186
|
+
"current_stage": job.get("current_stage", "unknown"),
|
|
187
|
+
"message": job.get("message", "Processing..."),
|
|
188
|
+
"project": job.get("project", "crackerjack"),
|
|
189
|
+
"total_issues": job.get("total_issues", 0),
|
|
190
|
+
"errors_fixed": job.get("errors_fixed", 0),
|
|
191
|
+
"errors_failed": job.get("errors_failed", 0),
|
|
192
|
+
"current_errors": job.get("current_errors", 0),
|
|
193
|
+
"overall_progress": job.get("overall_progress", 0.0),
|
|
194
|
+
"stage_progress": job.get("stage_progress", 0.0),
|
|
195
|
+
}
|
|
196
|
+
jobs_data["individual_jobs"].append(job_entry)
|
|
197
|
+
|
|
198
|
+
jobs_data["total_issues"] += job.get("total_issues", 0)
|
|
199
|
+
jobs_data["errors_fixed"] += job.get("errors_fixed", 0)
|
|
200
|
+
jobs_data["errors_failed"] += job.get("errors_failed", 0)
|
|
201
|
+
|
|
202
|
+
return jobs_data
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class ServiceHealthChecker:
|
|
206
|
+
def __init__(self) -> None:
|
|
207
|
+
self.console = Console()
|
|
208
|
+
|
|
209
|
+
async def collect_services_data(self) -> list[tuple[str, str, str]]:
|
|
210
|
+
services = []
|
|
211
|
+
|
|
212
|
+
services.extend(
|
|
213
|
+
(
|
|
214
|
+
await self._check_websocket_server(),
|
|
215
|
+
self._check_mcp_server(),
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
services.extend(
|
|
220
|
+
(
|
|
221
|
+
self._check_service_watchdog(),
|
|
222
|
+
("File Monitor", "🟢 Watching", "0"),
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return services
|
|
227
|
+
|
|
228
|
+
async def _check_websocket_server(self) -> tuple[str, str, str]:
|
|
229
|
+
try:
|
|
230
|
+
async with (
|
|
231
|
+
aiohttp.ClientSession(
|
|
232
|
+
timeout=aiohttp.ClientTimeout(total=2),
|
|
233
|
+
) as session,
|
|
234
|
+
session.get("http: // localhost: 8675 / ") as response,
|
|
235
|
+
):
|
|
236
|
+
if response.status == 200:
|
|
237
|
+
data = await response.json()
|
|
238
|
+
connections = data.get("total_connections", 0)
|
|
239
|
+
len(data.get("active_jobs", []))
|
|
240
|
+
return (
|
|
241
|
+
"WebSocket Server",
|
|
242
|
+
f"🟢 Active ({connections} conn)",
|
|
243
|
+
"0",
|
|
244
|
+
)
|
|
245
|
+
return ("WebSocket Server", "🔴 HTTP Error", "1")
|
|
246
|
+
except Exception:
|
|
247
|
+
return ("WebSocket Server", "🔴 Connection Failed", "1")
|
|
248
|
+
|
|
249
|
+
def _check_mcp_server(self) -> tuple[str, str, str]:
|
|
250
|
+
try:
|
|
251
|
+
result = subprocess.run(
|
|
252
|
+
["pgrep", "-f", "crackerjack.*mcp"],
|
|
253
|
+
check=False,
|
|
254
|
+
capture_output=True,
|
|
255
|
+
text=True,
|
|
256
|
+
)
|
|
257
|
+
if result.returncode == 0:
|
|
258
|
+
return ("MCP Server", "🟢 Process Active", "0")
|
|
259
|
+
return ("MCP Server", "🔴 No Process", "0")
|
|
260
|
+
except Exception:
|
|
261
|
+
return ("MCP Server", "🔴 Check Failed", "0")
|
|
262
|
+
|
|
263
|
+
def _check_service_watchdog(self) -> tuple[str, str, str]:
|
|
264
|
+
try:
|
|
265
|
+
result = subprocess.run(
|
|
266
|
+
["pgrep", "-f", "crackerjack.*watchdog"],
|
|
267
|
+
check=False,
|
|
268
|
+
capture_output=True,
|
|
269
|
+
text=True,
|
|
270
|
+
)
|
|
271
|
+
if result.returncode == 0:
|
|
272
|
+
return ("Service Watchdog", "🟢 Active", "0")
|
|
273
|
+
return ("Service Watchdog", "🔴 Inactive", "0")
|
|
274
|
+
except Exception:
|
|
275
|
+
return ("Service Watchdog", "🔴 Check Failed", "0")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class ErrorCollector:
|
|
279
|
+
def __init__(self) -> None:
|
|
280
|
+
self.console = Console()
|
|
281
|
+
|
|
282
|
+
async def collect_recent_errors(self) -> list[tuple[str, str, str, str]]:
|
|
283
|
+
errors = []
|
|
284
|
+
|
|
285
|
+
errors.extend(self._check_debug_logs())
|
|
286
|
+
|
|
287
|
+
errors.extend(self._check_crackerjack_logs())
|
|
288
|
+
|
|
289
|
+
if not errors:
|
|
290
|
+
errors = [
|
|
291
|
+
(
|
|
292
|
+
time.strftime(" % H: % M: % S"),
|
|
293
|
+
"system",
|
|
294
|
+
"No recent errors found",
|
|
295
|
+
"Clean",
|
|
296
|
+
),
|
|
297
|
+
(" -- : -- : -- ", "monitor", "System monitoring active", "Status"),
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
return errors[-5:]
|
|
301
|
+
|
|
302
|
+
def _check_debug_logs(self) -> list[tuple[str, str, str, str]]:
|
|
303
|
+
"""Check debug logs for recent errors."""
|
|
304
|
+
errors = []
|
|
305
|
+
|
|
306
|
+
with suppress(Exception):
|
|
307
|
+
debug_log = Path(tempfile.gettempdir()) / "tui_debug.log"
|
|
308
|
+
if debug_log.exists():
|
|
309
|
+
errors = self._extract_debug_log_errors(debug_log)
|
|
310
|
+
|
|
311
|
+
return errors
|
|
312
|
+
|
|
313
|
+
def _extract_debug_log_errors(
|
|
314
|
+
self,
|
|
315
|
+
debug_log: Path,
|
|
316
|
+
) -> list[tuple[str, str, str, str]]:
|
|
317
|
+
"""Extract error entries from debug log file."""
|
|
318
|
+
errors = []
|
|
319
|
+
|
|
320
|
+
with debug_log.open() as f:
|
|
321
|
+
lines = f.readlines()[-10:]
|
|
322
|
+
|
|
323
|
+
for line in lines:
|
|
324
|
+
if self._is_debug_error_line(line):
|
|
325
|
+
error_entry = self._parse_debug_error_line(line)
|
|
326
|
+
if error_entry:
|
|
327
|
+
errors.append(error_entry)
|
|
328
|
+
|
|
329
|
+
return errors
|
|
330
|
+
|
|
331
|
+
def _is_debug_error_line(self, line: str) -> bool:
|
|
332
|
+
"""Check if debug log line contains error indicators."""
|
|
333
|
+
return any(indicator in line for indicator in ("ERROR", "Exception", "Failed"))
|
|
334
|
+
|
|
335
|
+
def _parse_debug_error_line(self, line: str) -> tuple[str, str, str, str] | None:
|
|
336
|
+
"""Parse debug error line into error entry tuple."""
|
|
337
|
+
parts = line.strip().split(" ", 2)
|
|
338
|
+
if len(parts) < 3:
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
timestamp = parts[0]
|
|
342
|
+
error_msg = self._truncate_debug_message(parts[2])
|
|
343
|
+
return (timestamp, "debug", error_msg, "System")
|
|
344
|
+
|
|
345
|
+
def _truncate_debug_message(self, message: str) -> str:
|
|
346
|
+
"""Truncate debug message to reasonable length."""
|
|
347
|
+
return message[:40] + "..." if len(message) > 40 else message
|
|
348
|
+
|
|
349
|
+
def _check_crackerjack_logs(self) -> list[tuple[str, str, str, str]]:
|
|
350
|
+
"""Check crackerjack debug logs for recent errors."""
|
|
351
|
+
errors = []
|
|
352
|
+
|
|
353
|
+
with suppress(Exception):
|
|
354
|
+
for log_file in Path(tempfile.gettempdir()).glob("crackerjack-debug-*.log"):
|
|
355
|
+
if self._is_log_file_recent(log_file):
|
|
356
|
+
errors.extend(self._extract_errors_from_log_file(log_file))
|
|
357
|
+
|
|
358
|
+
return errors
|
|
359
|
+
|
|
360
|
+
def _is_log_file_recent(self, log_file: Path) -> bool:
|
|
361
|
+
"""Check if log file was modified within the last hour."""
|
|
362
|
+
return time.time() - log_file.stat().st_mtime < 3600
|
|
363
|
+
|
|
364
|
+
def _extract_errors_from_log_file(
|
|
365
|
+
self,
|
|
366
|
+
log_file: Path,
|
|
367
|
+
) -> list[tuple[str, str, str, str]]:
|
|
368
|
+
"""Extract error entries from a single log file."""
|
|
369
|
+
errors = []
|
|
370
|
+
|
|
371
|
+
with log_file.open() as f:
|
|
372
|
+
lines = f.readlines()[-5:]
|
|
373
|
+
|
|
374
|
+
for line in lines:
|
|
375
|
+
if self._is_error_line(line):
|
|
376
|
+
error_entry = self._create_error_entry(line, log_file)
|
|
377
|
+
errors.append(error_entry)
|
|
378
|
+
|
|
379
|
+
return errors
|
|
380
|
+
|
|
381
|
+
def _is_error_line(self, line: str) -> bool:
|
|
382
|
+
"""Check if a log line contains error keywords."""
|
|
383
|
+
return any(
|
|
384
|
+
keyword in line.lower() for keyword in ("error", "failed", "exception")
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def _create_error_entry(
|
|
388
|
+
self,
|
|
389
|
+
line: str,
|
|
390
|
+
log_file: Path,
|
|
391
|
+
) -> tuple[str, str, str, str]:
|
|
392
|
+
"""Create error entry tuple from log line and file."""
|
|
393
|
+
timestamp = time.strftime(
|
|
394
|
+
" % H: % M: % S",
|
|
395
|
+
time.localtime(log_file.stat().st_mtime),
|
|
396
|
+
)
|
|
397
|
+
error_msg = self._truncate_error_message(line.strip())
|
|
398
|
+
return (timestamp, "job", error_msg, "Crackerjack")
|
|
399
|
+
|
|
400
|
+
def _truncate_error_message(self, message: str) -> str:
|
|
401
|
+
"""Truncate error message to reasonable length."""
|
|
402
|
+
return message[:50] + "..." if len(message) > 50 else message
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class ServiceManager:
|
|
406
|
+
def __init__(self) -> None:
|
|
407
|
+
self.started_services: list[tuple[str, subprocess.Popen]] = []
|
|
408
|
+
self.console = Console()
|
|
409
|
+
|
|
410
|
+
async def ensure_services_running(self) -> None:
|
|
411
|
+
with suppress(Exception):
|
|
412
|
+
websocket_running = await self._check_websocket_server()
|
|
413
|
+
mcp_running = self._check_mcp_server()
|
|
414
|
+
|
|
415
|
+
self._cleanup_dead_services()
|
|
416
|
+
|
|
417
|
+
if not websocket_running:
|
|
418
|
+
await self._start_websocket_server()
|
|
419
|
+
|
|
420
|
+
if not mcp_running:
|
|
421
|
+
await self._start_mcp_server()
|
|
422
|
+
|
|
423
|
+
await self._start_service_watchdog()
|
|
424
|
+
|
|
425
|
+
def _cleanup_dead_services(self) -> None:
|
|
426
|
+
self.started_services = [
|
|
427
|
+
(service_name, process)
|
|
428
|
+
for service_name, process in self.started_services
|
|
429
|
+
if process.poll() is None
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
async def _check_websocket_server(self) -> bool:
|
|
433
|
+
with suppress(Exception):
|
|
434
|
+
async with aiohttp.ClientSession(
|
|
435
|
+
timeout=aiohttp.ClientTimeout(total=2),
|
|
436
|
+
) as session:
|
|
437
|
+
async with session.get("http: // localhost: 8675 / ") as response:
|
|
438
|
+
return response.status == 200
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
def _check_mcp_server(self) -> bool:
|
|
442
|
+
with suppress(Exception):
|
|
443
|
+
result = subprocess.run(
|
|
444
|
+
["pgrep", "-f", "crackerjack.*mcp"],
|
|
445
|
+
check=False,
|
|
446
|
+
capture_output=True,
|
|
447
|
+
text=True,
|
|
448
|
+
)
|
|
449
|
+
return result.returncode == 0
|
|
450
|
+
return False
|
|
451
|
+
|
|
452
|
+
async def _start_websocket_server(self) -> None:
|
|
453
|
+
with suppress(Exception):
|
|
454
|
+
process = subprocess.Popen(
|
|
455
|
+
["python", "-m", "crackerjack", "--start-websocket-server"],
|
|
456
|
+
stdout=subprocess.PIPE,
|
|
457
|
+
stderr=subprocess.PIPE,
|
|
458
|
+
start_new_session=True,
|
|
459
|
+
)
|
|
460
|
+
self.started_services.append(("websocket", process))
|
|
461
|
+
await asyncio.sleep(2)
|
|
462
|
+
|
|
463
|
+
async def _start_mcp_server(self) -> None:
|
|
464
|
+
with suppress(Exception):
|
|
465
|
+
process = subprocess.Popen(
|
|
466
|
+
["python", "-m", "crackerjack", "--start-mcp-server"],
|
|
467
|
+
stdout=subprocess.PIPE,
|
|
468
|
+
stderr=subprocess.PIPE,
|
|
469
|
+
start_new_session=True,
|
|
470
|
+
)
|
|
471
|
+
self.started_services.append(("mcp", process))
|
|
472
|
+
await asyncio.sleep(2)
|
|
473
|
+
|
|
474
|
+
async def _start_service_watchdog(self) -> None:
|
|
475
|
+
with suppress(Exception):
|
|
476
|
+
result = subprocess.run(
|
|
477
|
+
["pgrep", "-f", "crackerjack.*watchdog"],
|
|
478
|
+
check=False,
|
|
479
|
+
capture_output=True,
|
|
480
|
+
text=True,
|
|
481
|
+
)
|
|
482
|
+
if result.returncode == 0:
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
process = subprocess.Popen(
|
|
486
|
+
["python", " - m", "crackerjack", " -- watchdog"],
|
|
487
|
+
stdout=subprocess.PIPE,
|
|
488
|
+
stderr=subprocess.PIPE,
|
|
489
|
+
start_new_session=True,
|
|
490
|
+
)
|
|
491
|
+
self.started_services.append(("watchdog", process))
|
|
492
|
+
|
|
493
|
+
def cleanup_services(self) -> None:
|
|
494
|
+
for _service_name, process in self.started_services:
|
|
495
|
+
self._cleanup_single_service(process)
|
|
496
|
+
self.started_services.clear()
|
|
497
|
+
|
|
498
|
+
def _cleanup_single_service(self, process: subprocess.Popen) -> None:
|
|
499
|
+
with suppress(Exception):
|
|
500
|
+
if process.poll() is not None:
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
process.terminate()
|
|
504
|
+
try:
|
|
505
|
+
process.wait(timeout=5)
|
|
506
|
+
except subprocess.TimeoutExpired:
|
|
507
|
+
process.kill()
|
|
508
|
+
try:
|
|
509
|
+
process.wait(timeout=2)
|
|
510
|
+
except subprocess.TimeoutExpired:
|
|
511
|
+
import os
|
|
512
|
+
|
|
513
|
+
os.kill(process.pid, 9)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class TerminalRestorer:
|
|
517
|
+
@staticmethod
|
|
518
|
+
def restore_terminal() -> None:
|
|
519
|
+
try:
|
|
520
|
+
import subprocess
|
|
521
|
+
import sys
|
|
522
|
+
|
|
523
|
+
restoration_sequences = [
|
|
524
|
+
"\033[?1049l",
|
|
525
|
+
"\033[?1000l",
|
|
526
|
+
"\033[?1003l",
|
|
527
|
+
"\033[?1015l",
|
|
528
|
+
"\033[?1006l",
|
|
529
|
+
"\033[?25h",
|
|
530
|
+
"\033[?1004l",
|
|
531
|
+
"\033[?2004l",
|
|
532
|
+
"\033[?7h",
|
|
533
|
+
"\033[0m",
|
|
534
|
+
"\r",
|
|
535
|
+
]
|
|
536
|
+
|
|
537
|
+
for sequence in restoration_sequences:
|
|
538
|
+
sys.stdout.write(sequence)
|
|
539
|
+
sys.stdout.flush()
|
|
540
|
+
|
|
541
|
+
subprocess.run(
|
|
542
|
+
["stty", "echo", "icanon", "icrnl", "ixon"],
|
|
543
|
+
check=False,
|
|
544
|
+
capture_output=True,
|
|
545
|
+
timeout=1,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
except Exception:
|
|
549
|
+
with suppress(Exception):
|
|
550
|
+
import subprocess
|
|
551
|
+
import sys
|
|
552
|
+
|
|
553
|
+
critical_sequences = [
|
|
554
|
+
"\033[?1049l",
|
|
555
|
+
"\033[?25h",
|
|
556
|
+
"\033[0m",
|
|
557
|
+
"\r",
|
|
558
|
+
]
|
|
559
|
+
|
|
560
|
+
for sequence in critical_sequences:
|
|
561
|
+
sys.stdout.write(sequence)
|
|
562
|
+
sys.stdout.flush()
|
|
563
|
+
|
|
564
|
+
subprocess.run(
|
|
565
|
+
["stty", "sane"],
|
|
566
|
+
check=False,
|
|
567
|
+
capture_output=True,
|
|
568
|
+
timeout=1,
|
|
569
|
+
)
|