crackerjack 0.32.0__py3-none-any.whl → 0.33.0__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/core/enhanced_container.py +67 -0
- crackerjack/core/phase_coordinator.py +183 -44
- crackerjack/core/workflow_orchestrator.py +459 -138
- crackerjack/managers/publish_manager.py +22 -5
- crackerjack/managers/test_command_builder.py +4 -2
- crackerjack/managers/test_manager.py +15 -4
- crackerjack/mcp/server_core.py +162 -34
- crackerjack/mcp/tools/core_tools.py +1 -1
- crackerjack/mcp/tools/execution_tools.py +8 -3
- crackerjack/mixins/__init__.py +5 -0
- crackerjack/mixins/error_handling.py +214 -0
- crackerjack/models/config.py +9 -0
- crackerjack/models/protocols.py +69 -0
- crackerjack/models/task.py +3 -0
- crackerjack/security/__init__.py +1 -1
- crackerjack/security/audit.py +92 -78
- crackerjack/services/config.py +3 -2
- crackerjack/services/config_merge.py +11 -5
- crackerjack/services/coverage_ratchet.py +22 -0
- crackerjack/services/git.py +37 -24
- crackerjack/services/initialization.py +25 -9
- crackerjack/services/memory_optimizer.py +477 -0
- crackerjack/services/parallel_executor.py +474 -0
- crackerjack/services/performance_benchmarks.py +292 -577
- crackerjack/services/performance_cache.py +443 -0
- crackerjack/services/performance_monitor.py +633 -0
- crackerjack/services/security.py +63 -0
- crackerjack/services/security_logger.py +9 -1
- crackerjack/services/terminal_utils.py +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/METADATA +2 -2
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/RECORD +34 -27
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/WHEEL +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,17 +5,34 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
|
|
8
|
-
from crackerjack.
|
|
9
|
-
from crackerjack.services.security import SecurityService
|
|
8
|
+
from crackerjack.models.protocols import FileSystemInterface, SecurityServiceProtocol
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
class PublishManagerImpl:
|
|
13
|
-
def __init__(
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
console: Console,
|
|
15
|
+
pkg_path: Path,
|
|
16
|
+
dry_run: bool = False,
|
|
17
|
+
filesystem: FileSystemInterface | None = None,
|
|
18
|
+
security: SecurityServiceProtocol | None = None,
|
|
19
|
+
) -> None:
|
|
14
20
|
self.console = console
|
|
15
21
|
self.pkg_path = pkg_path
|
|
16
22
|
self.dry_run = dry_run
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
|
|
24
|
+
if filesystem is None:
|
|
25
|
+
from crackerjack.services.filesystem import FileSystemService
|
|
26
|
+
|
|
27
|
+
filesystem = FileSystemService()
|
|
28
|
+
|
|
29
|
+
if security is None:
|
|
30
|
+
from crackerjack.services.security import SecurityService
|
|
31
|
+
|
|
32
|
+
security = SecurityService()
|
|
33
|
+
|
|
34
|
+
self.filesystem = filesystem
|
|
35
|
+
self.security = security
|
|
19
36
|
|
|
20
37
|
def _run_command(
|
|
21
38
|
self,
|
|
@@ -8,7 +8,7 @@ class TestCommandBuilder:
|
|
|
8
8
|
self.pkg_path = pkg_path
|
|
9
9
|
|
|
10
10
|
def build_command(self, options: OptionsProtocol) -> list[str]:
|
|
11
|
-
cmd = ["python", "-m", "pytest"]
|
|
11
|
+
cmd = ["uv", "run", "python", "-m", "pytest"]
|
|
12
12
|
|
|
13
13
|
self._add_coverage_options(cmd, options)
|
|
14
14
|
self._add_worker_options(cmd, options)
|
|
@@ -99,7 +99,7 @@ class TestCommandBuilder:
|
|
|
99
99
|
cmd.append(str(self.pkg_path))
|
|
100
100
|
|
|
101
101
|
def build_specific_test_command(self, test_pattern: str) -> list[str]:
|
|
102
|
-
cmd = ["python", "-m", "pytest", "-v"]
|
|
102
|
+
cmd = ["uv", "run", "python", "-m", "pytest", "-v"]
|
|
103
103
|
|
|
104
104
|
cmd.extend(
|
|
105
105
|
[
|
|
@@ -116,6 +116,8 @@ class TestCommandBuilder:
|
|
|
116
116
|
|
|
117
117
|
def build_validation_command(self) -> list[str]:
|
|
118
118
|
return [
|
|
119
|
+
"uv",
|
|
120
|
+
"run",
|
|
119
121
|
"python",
|
|
120
122
|
"-m",
|
|
121
123
|
"pytest",
|
|
@@ -5,21 +5,32 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
|
|
8
|
-
from crackerjack.models.protocols import OptionsProtocol
|
|
9
|
-
from crackerjack.services.coverage_ratchet import CoverageRatchetService
|
|
8
|
+
from crackerjack.models.protocols import CoverageRatchetProtocol, OptionsProtocol
|
|
10
9
|
|
|
11
10
|
from .test_command_builder import TestCommandBuilder
|
|
12
11
|
from .test_executor import TestExecutor
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
class TestManager:
|
|
16
|
-
def __init__(
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
console: Console,
|
|
18
|
+
pkg_path: Path,
|
|
19
|
+
coverage_ratchet: CoverageRatchetProtocol | None = None,
|
|
20
|
+
) -> None:
|
|
17
21
|
self.console = console
|
|
18
22
|
self.pkg_path = pkg_path
|
|
19
23
|
|
|
20
24
|
self.executor = TestExecutor(console, pkg_path)
|
|
21
25
|
self.command_builder = TestCommandBuilder(pkg_path)
|
|
22
|
-
|
|
26
|
+
|
|
27
|
+
if coverage_ratchet is None:
|
|
28
|
+
# Import here to avoid circular imports
|
|
29
|
+
from crackerjack.services.coverage_ratchet import CoverageRatchetService
|
|
30
|
+
|
|
31
|
+
coverage_ratchet = CoverageRatchetService(pkg_path, console)
|
|
32
|
+
|
|
33
|
+
self.coverage_ratchet = coverage_ratchet
|
|
23
34
|
|
|
24
35
|
self._last_test_failures: list[str] = []
|
|
25
36
|
self._progress_callback: t.Callable[[dict[str, t.Any]], None] | None = None
|
crackerjack/mcp/server_core.py
CHANGED
|
@@ -7,7 +7,12 @@ from typing import Final
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
|
|
9
9
|
try:
|
|
10
|
-
|
|
10
|
+
import tomli
|
|
11
|
+
except ImportError:
|
|
12
|
+
tomli = None
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from fastmcp import FastMCP
|
|
11
16
|
|
|
12
17
|
_mcp_available = True
|
|
13
18
|
except ImportError:
|
|
@@ -37,6 +42,42 @@ from .tools import (
|
|
|
37
42
|
console = Console()
|
|
38
43
|
|
|
39
44
|
|
|
45
|
+
def _load_mcp_config(project_path: Path) -> dict[str, t.Any]:
|
|
46
|
+
"""Load MCP server configuration from pyproject.toml."""
|
|
47
|
+
pyproject_path = project_path / "pyproject.toml"
|
|
48
|
+
|
|
49
|
+
if not pyproject_path.exists() or not tomli:
|
|
50
|
+
return {
|
|
51
|
+
"http_port": 8676,
|
|
52
|
+
"http_host": "127.0.0.1",
|
|
53
|
+
"websocket_port": 8675,
|
|
54
|
+
"http_enabled": False,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
with pyproject_path.open("rb") as f:
|
|
59
|
+
pyproject_data = tomli.load(f)
|
|
60
|
+
|
|
61
|
+
crackerjack_config = pyproject_data.get("tool", {}).get("crackerjack", {})
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
"http_port": crackerjack_config.get("mcp_http_port", 8676),
|
|
65
|
+
"http_host": crackerjack_config.get("mcp_http_host", "127.0.0.1"),
|
|
66
|
+
"websocket_port": crackerjack_config.get("mcp_websocket_port", 8675),
|
|
67
|
+
"http_enabled": crackerjack_config.get("mcp_http_enabled", False),
|
|
68
|
+
}
|
|
69
|
+
except Exception as e:
|
|
70
|
+
console.print(
|
|
71
|
+
f"[yellow]Warning: Failed to load MCP config from pyproject.toml: {e}[/yellow]"
|
|
72
|
+
)
|
|
73
|
+
return {
|
|
74
|
+
"http_port": 8676,
|
|
75
|
+
"http_host": "127.0.0.1",
|
|
76
|
+
"websocket_port": 8675,
|
|
77
|
+
"http_enabled": False,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
40
81
|
class MCPOptions:
|
|
41
82
|
def __init__(self, **kwargs: t.Any) -> None:
|
|
42
83
|
self.commit: bool = False
|
|
@@ -77,11 +118,14 @@ async def _start_websocket_server() -> bool:
|
|
|
77
118
|
return False
|
|
78
119
|
|
|
79
120
|
|
|
80
|
-
def create_mcp_server() -> t.Any | None:
|
|
121
|
+
def create_mcp_server(config: dict[str, t.Any] | None = None) -> t.Any | None:
|
|
81
122
|
if not MCP_AVAILABLE or FastMCP is None:
|
|
82
123
|
return None
|
|
83
124
|
|
|
84
|
-
|
|
125
|
+
if config is None:
|
|
126
|
+
config = {"http_port": 8676, "http_host": "127.0.0.1"}
|
|
127
|
+
|
|
128
|
+
mcp_app = FastMCP("crackerjack-mcp-server", streamable_http_path="/mcp")
|
|
85
129
|
|
|
86
130
|
from crackerjack.slash_commands import get_slash_command_path
|
|
87
131
|
|
|
@@ -128,6 +172,8 @@ def handle_mcp_server_command(
|
|
|
128
172
|
stop: bool = False,
|
|
129
173
|
restart: bool = False,
|
|
130
174
|
websocket_port: int | None = None,
|
|
175
|
+
http_mode: bool = False,
|
|
176
|
+
http_port: int | None = None,
|
|
131
177
|
) -> None:
|
|
132
178
|
if stop or restart:
|
|
133
179
|
console.print("[yellow]Stopping MCP servers...[/ yellow]")
|
|
@@ -157,7 +203,7 @@ def handle_mcp_server_command(
|
|
|
157
203
|
if start or restart:
|
|
158
204
|
console.print("[green]Starting MCP server...[/ green]")
|
|
159
205
|
try:
|
|
160
|
-
main(".", websocket_port)
|
|
206
|
+
main(".", websocket_port, http_mode, http_port)
|
|
161
207
|
except Exception as e:
|
|
162
208
|
console.print(f"[red]Failed to start MCP server: {e}[/ red]")
|
|
163
209
|
|
|
@@ -177,45 +223,111 @@ def _stop_websocket_server() -> None:
|
|
|
177
223
|
pass
|
|
178
224
|
|
|
179
225
|
|
|
180
|
-
def
|
|
226
|
+
def _merge_config_with_args(
|
|
227
|
+
mcp_config: dict[str, t.Any],
|
|
228
|
+
http_port: int | None,
|
|
229
|
+
http_mode: bool,
|
|
230
|
+
) -> dict[str, t.Any]:
|
|
231
|
+
"""Merge MCP configuration with command line arguments."""
|
|
232
|
+
if http_port:
|
|
233
|
+
mcp_config["http_port"] = http_port
|
|
234
|
+
if http_mode:
|
|
235
|
+
mcp_config["http_enabled"] = True
|
|
236
|
+
return mcp_config
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _setup_server_context(
|
|
240
|
+
project_path: Path,
|
|
241
|
+
websocket_port: int | None,
|
|
242
|
+
) -> MCPServerContext:
|
|
243
|
+
"""Set up and initialize the MCP server context."""
|
|
244
|
+
config = MCPServerConfig(
|
|
245
|
+
project_path=project_path,
|
|
246
|
+
rate_limit_config=RateLimitConfig(),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
context = MCPServerContext(config)
|
|
250
|
+
context.console = console
|
|
251
|
+
|
|
252
|
+
if websocket_port:
|
|
253
|
+
context.websocket_server_port = websocket_port
|
|
254
|
+
|
|
255
|
+
_initialize_context(context)
|
|
256
|
+
return context
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _print_server_info(
|
|
260
|
+
project_path: Path,
|
|
261
|
+
mcp_config: dict[str, t.Any],
|
|
262
|
+
websocket_port: int | None,
|
|
263
|
+
http_mode: bool,
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Print server startup information."""
|
|
266
|
+
console.print("[green]Starting Crackerjack MCP Server...[/ green]")
|
|
267
|
+
console.print(f"Project path: {project_path}")
|
|
268
|
+
|
|
269
|
+
if mcp_config.get("http_enabled", False) or http_mode:
|
|
270
|
+
console.print(
|
|
271
|
+
f"[cyan]HTTP Mode: http://{mcp_config['http_host']}:{mcp_config['http_port']}/mcp[/ cyan]"
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
console.print("[cyan]STDIO Mode[/ cyan]")
|
|
275
|
+
|
|
276
|
+
if websocket_port:
|
|
277
|
+
console.print(f"WebSocket port: {websocket_port}")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _run_mcp_server(
|
|
281
|
+
mcp_app: t.Any, mcp_config: dict[str, t.Any], http_mode: bool
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Execute the MCP server with appropriate transport mode."""
|
|
284
|
+
console.print("[yellow]MCP app created, about to run...[/ yellow]")
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
if mcp_config.get("http_enabled", False) or http_mode:
|
|
288
|
+
host = mcp_config.get("http_host", "127.0.0.1")
|
|
289
|
+
port = mcp_config.get("http_port", 8676)
|
|
290
|
+
mcp_app.run(transport="streamable-http", host=host, port=port)
|
|
291
|
+
else:
|
|
292
|
+
mcp_app.run()
|
|
293
|
+
except Exception as e:
|
|
294
|
+
console.print(f"[red]MCP run failed: {e}[/ red]")
|
|
295
|
+
import traceback
|
|
296
|
+
|
|
297
|
+
traceback.print_exc()
|
|
298
|
+
raise
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def main(
|
|
302
|
+
project_path_arg: str = ".",
|
|
303
|
+
websocket_port: int | None = None,
|
|
304
|
+
http_mode: bool = False,
|
|
305
|
+
http_port: int | None = None,
|
|
306
|
+
) -> None:
|
|
181
307
|
if not MCP_AVAILABLE:
|
|
182
308
|
return
|
|
183
309
|
|
|
184
310
|
try:
|
|
185
311
|
project_path = Path(project_path_arg).resolve()
|
|
186
312
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
context = MCPServerContext(config)
|
|
193
|
-
context.console = console
|
|
313
|
+
# Load and merge configuration
|
|
314
|
+
mcp_config = _load_mcp_config(project_path)
|
|
315
|
+
mcp_config = _merge_config_with_args(mcp_config, http_port, http_mode)
|
|
194
316
|
|
|
195
|
-
|
|
196
|
-
|
|
317
|
+
# Set up server context
|
|
318
|
+
_setup_server_context(project_path, websocket_port)
|
|
197
319
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
mcp_app = create_mcp_server()
|
|
320
|
+
# Create MCP server
|
|
321
|
+
mcp_app = create_mcp_server(mcp_config)
|
|
201
322
|
if not mcp_app:
|
|
202
323
|
console.print("[red]Failed to create MCP server[/ red]")
|
|
203
324
|
return
|
|
204
325
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if websocket_port:
|
|
208
|
-
console.print(f"WebSocket port: {websocket_port}")
|
|
326
|
+
# Print server information
|
|
327
|
+
_print_server_info(project_path, mcp_config, websocket_port, http_mode)
|
|
209
328
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
mcp_app.run()
|
|
213
|
-
except Exception as e:
|
|
214
|
-
console.print(f"[red]MCP run failed: {e}[/ red]")
|
|
215
|
-
import traceback
|
|
216
|
-
|
|
217
|
-
traceback.print_exc()
|
|
218
|
-
raise
|
|
329
|
+
# Run the server
|
|
330
|
+
_run_mcp_server(mcp_app, mcp_config, http_mode)
|
|
219
331
|
|
|
220
332
|
except KeyboardInterrupt:
|
|
221
333
|
console.print("Server stopped by user")
|
|
@@ -232,7 +344,23 @@ def main(project_path_arg: str = ".", websocket_port: int | None = None) -> None
|
|
|
232
344
|
if __name__ == "__main__":
|
|
233
345
|
import sys
|
|
234
346
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
347
|
+
# Initialize defaults
|
|
348
|
+
project_path = "."
|
|
349
|
+
websocket_port = None
|
|
350
|
+
http_mode = "--http" in sys.argv
|
|
351
|
+
http_port = None
|
|
352
|
+
|
|
353
|
+
# Parse project path from non-flag arguments
|
|
354
|
+
non_flag_args = [arg for arg in sys.argv[1:] if not arg.startswith("--")]
|
|
355
|
+
if non_flag_args:
|
|
356
|
+
project_path = non_flag_args[0]
|
|
357
|
+
if len(non_flag_args) > 1 and non_flag_args[1].isdigit():
|
|
358
|
+
websocket_port = int(non_flag_args[1])
|
|
359
|
+
|
|
360
|
+
# Parse HTTP port flag
|
|
361
|
+
if "--http-port" in sys.argv:
|
|
362
|
+
port_idx = sys.argv.index("--http-port")
|
|
363
|
+
if port_idx + 1 < len(sys.argv):
|
|
364
|
+
http_port = int(sys.argv[port_idx + 1])
|
|
365
|
+
|
|
366
|
+
main(project_path, websocket_port, http_mode, http_port)
|
|
@@ -186,7 +186,7 @@ def _execute_init_stage(orchestrator) -> bool:
|
|
|
186
186
|
|
|
187
187
|
init_service = InitializationService(console, filesystem, git_service, pkg_path)
|
|
188
188
|
|
|
189
|
-
results = init_service.
|
|
189
|
+
results = init_service.initialize_project_full(target_path=Path.cwd())
|
|
190
190
|
|
|
191
191
|
return results.get("success", False)
|
|
192
192
|
|
|
@@ -161,9 +161,14 @@ def _execute_initialization(target_path: t.Any, force: bool) -> dict[str, t.Any]
|
|
|
161
161
|
|
|
162
162
|
console = Console()
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
from crackerjack.services.filesystem import FileSystemService
|
|
165
|
+
from crackerjack.services.git import GitService
|
|
166
|
+
|
|
167
|
+
filesystem = FileSystemService()
|
|
168
|
+
git_service = GitService()
|
|
169
|
+
return InitializationService(
|
|
170
|
+
console, filesystem, git_service, target_path
|
|
171
|
+
).initialize_project_full(force=force)
|
|
167
172
|
|
|
168
173
|
|
|
169
174
|
def _create_init_error_response(message: str) -> str:
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Common error handling patterns for crackerjack components."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import typing as t
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ErrorHandlingMixin:
|
|
11
|
+
"""Mixin providing common error handling patterns for crackerjack components."""
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
# These attributes should be provided by the class using the mixin
|
|
15
|
+
self.console: Console
|
|
16
|
+
self.logger: t.Any # Logger instance
|
|
17
|
+
|
|
18
|
+
def handle_subprocess_error(
|
|
19
|
+
self,
|
|
20
|
+
error: Exception,
|
|
21
|
+
command: list[str],
|
|
22
|
+
operation_name: str,
|
|
23
|
+
critical: bool = False,
|
|
24
|
+
) -> bool:
|
|
25
|
+
"""Handle subprocess errors with consistent logging and user feedback.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
error: The exception that occurred
|
|
29
|
+
command: The command that failed
|
|
30
|
+
operation_name: Human-readable name of the operation
|
|
31
|
+
critical: Whether this is a critical error that should stop execution
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
False to indicate failure
|
|
35
|
+
"""
|
|
36
|
+
error_msg = f"{operation_name} failed: {error}"
|
|
37
|
+
|
|
38
|
+
# Log the error
|
|
39
|
+
if hasattr(self, "logger") and self.logger:
|
|
40
|
+
self.logger.error(
|
|
41
|
+
error_msg,
|
|
42
|
+
command=" ".join(command),
|
|
43
|
+
error_type=type(error).__name__,
|
|
44
|
+
critical=critical,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Display user-friendly error message
|
|
48
|
+
if critical:
|
|
49
|
+
self.console.print(f"[red]🚨 CRITICAL: {error_msg}[/red]")
|
|
50
|
+
else:
|
|
51
|
+
self.console.print(f"[red]❌ {error_msg}[/red]")
|
|
52
|
+
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
def handle_file_operation_error(
|
|
56
|
+
self,
|
|
57
|
+
error: Exception,
|
|
58
|
+
file_path: Path,
|
|
59
|
+
operation: str,
|
|
60
|
+
critical: bool = False,
|
|
61
|
+
) -> bool:
|
|
62
|
+
"""Handle file operation errors with consistent logging and user feedback.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
error: The exception that occurred
|
|
66
|
+
file_path: The file that caused the error
|
|
67
|
+
operation: The operation that failed (e.g., "read", "write", "delete")
|
|
68
|
+
critical: Whether this is a critical error that should stop execution
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
False to indicate failure
|
|
72
|
+
"""
|
|
73
|
+
error_msg = f"Failed to {operation} {file_path}: {error}"
|
|
74
|
+
|
|
75
|
+
# Log the error
|
|
76
|
+
if hasattr(self, "logger") and self.logger:
|
|
77
|
+
self.logger.error(
|
|
78
|
+
error_msg,
|
|
79
|
+
file_path=str(file_path),
|
|
80
|
+
operation=operation,
|
|
81
|
+
error_type=type(error).__name__,
|
|
82
|
+
critical=critical,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Display user-friendly error message
|
|
86
|
+
if critical:
|
|
87
|
+
self.console.print(f"[red]🚨 CRITICAL: {error_msg}[/red]")
|
|
88
|
+
else:
|
|
89
|
+
self.console.print(f"[red]❌ {error_msg}[/red]")
|
|
90
|
+
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def handle_timeout_error(
|
|
94
|
+
self,
|
|
95
|
+
operation_name: str,
|
|
96
|
+
timeout_seconds: float,
|
|
97
|
+
command: list[str] | None = None,
|
|
98
|
+
) -> bool:
|
|
99
|
+
"""Handle timeout errors with consistent logging and user feedback.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
operation_name: Human-readable name of the operation
|
|
103
|
+
timeout_seconds: The timeout that was exceeded
|
|
104
|
+
command: Optional command that timed out
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
False to indicate failure
|
|
108
|
+
"""
|
|
109
|
+
error_msg = f"{operation_name} timed out after {timeout_seconds}s"
|
|
110
|
+
|
|
111
|
+
# Log the error
|
|
112
|
+
if hasattr(self, "logger") and self.logger:
|
|
113
|
+
self.logger.warning(
|
|
114
|
+
error_msg,
|
|
115
|
+
timeout=timeout_seconds,
|
|
116
|
+
command=" ".join(command) if command else None,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Display user-friendly error message
|
|
120
|
+
self.console.print(f"[yellow]⏰ {error_msg}[/yellow]")
|
|
121
|
+
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def log_operation_success(
|
|
125
|
+
self,
|
|
126
|
+
operation_name: str,
|
|
127
|
+
details: dict[str, t.Any] | None = None,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Log successful operations with consistent formatting.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
operation_name: Human-readable name of the operation
|
|
133
|
+
details: Optional additional details to log
|
|
134
|
+
"""
|
|
135
|
+
if hasattr(self, "logger") and self.logger:
|
|
136
|
+
self.logger.info(
|
|
137
|
+
f"{operation_name} completed successfully", **(details or {})
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def validate_required_tools(
|
|
141
|
+
self,
|
|
142
|
+
tools: dict[str, str],
|
|
143
|
+
operation_name: str,
|
|
144
|
+
) -> bool:
|
|
145
|
+
"""Validate that required external tools are available.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
tools: Dict mapping tool names to their expected commands
|
|
149
|
+
operation_name: Name of operation requiring the tools
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True if all tools are available, False otherwise
|
|
153
|
+
"""
|
|
154
|
+
missing_tools = []
|
|
155
|
+
|
|
156
|
+
for tool_name, command in tools.items():
|
|
157
|
+
try:
|
|
158
|
+
subprocess.run(
|
|
159
|
+
[command, "--version"],
|
|
160
|
+
capture_output=True,
|
|
161
|
+
check=True,
|
|
162
|
+
timeout=5,
|
|
163
|
+
)
|
|
164
|
+
except (
|
|
165
|
+
subprocess.CalledProcessError,
|
|
166
|
+
subprocess.TimeoutExpired,
|
|
167
|
+
FileNotFoundError,
|
|
168
|
+
):
|
|
169
|
+
missing_tools.append(tool_name)
|
|
170
|
+
|
|
171
|
+
if missing_tools:
|
|
172
|
+
error_msg = f"Missing required tools for {operation_name}: {', '.join(missing_tools)}"
|
|
173
|
+
|
|
174
|
+
if hasattr(self, "logger") and self.logger:
|
|
175
|
+
self.logger.error(
|
|
176
|
+
error_msg,
|
|
177
|
+
missing_tools=missing_tools,
|
|
178
|
+
operation=operation_name,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
self.console.print(f"[red]❌ {error_msg}[/red]")
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
def safe_get_attribute(
|
|
187
|
+
self,
|
|
188
|
+
obj: t.Any,
|
|
189
|
+
attribute: str,
|
|
190
|
+
default: t.Any = None,
|
|
191
|
+
operation_name: str = "attribute access",
|
|
192
|
+
) -> t.Any:
|
|
193
|
+
"""Safely get an attribute with error handling.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
obj: Object to get attribute from
|
|
197
|
+
attribute: Name of attribute to get
|
|
198
|
+
default: Default value if attribute doesn't exist
|
|
199
|
+
operation_name: Name of operation for error logging
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
The attribute value or default
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
return getattr(obj, attribute, default)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
if hasattr(self, "logger") and self.logger:
|
|
208
|
+
self.logger.warning(
|
|
209
|
+
f"Error accessing {attribute} during {operation_name}: {e}",
|
|
210
|
+
attribute=attribute,
|
|
211
|
+
operation=operation_name,
|
|
212
|
+
error_type=type(e).__name__,
|
|
213
|
+
)
|
|
214
|
+
return default
|
crackerjack/models/config.py
CHANGED
|
@@ -79,6 +79,14 @@ class EnterpriseConfig:
|
|
|
79
79
|
organization: str | None = None
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
@dataclass
|
|
83
|
+
class MCPServerConfig:
|
|
84
|
+
http_port: int = 8676
|
|
85
|
+
http_host: str = "127.0.0.1"
|
|
86
|
+
websocket_port: int = 8675
|
|
87
|
+
http_enabled: bool = False
|
|
88
|
+
|
|
89
|
+
|
|
82
90
|
@dataclass
|
|
83
91
|
class WorkflowOptions:
|
|
84
92
|
cleaning: CleaningConfig = field(default_factory=CleaningConfig)
|
|
@@ -91,3 +99,4 @@ class WorkflowOptions:
|
|
|
91
99
|
progress: ProgressConfig = field(default_factory=ProgressConfig)
|
|
92
100
|
cleanup: CleanupConfig = field(default_factory=CleanupConfig)
|
|
93
101
|
enterprise: EnterpriseConfig = field(default_factory=EnterpriseConfig)
|
|
102
|
+
mcp_server: MCPServerConfig = field(default_factory=MCPServerConfig)
|