crackerjack 0.31.18__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.

Files changed (43) hide show
  1. crackerjack/CLAUDE.md +71 -452
  2. crackerjack/__main__.py +1 -1
  3. crackerjack/agents/refactoring_agent.py +67 -46
  4. crackerjack/cli/handlers.py +7 -7
  5. crackerjack/config/hooks.py +36 -6
  6. crackerjack/core/async_workflow_orchestrator.py +2 -2
  7. crackerjack/core/enhanced_container.py +67 -0
  8. crackerjack/core/phase_coordinator.py +211 -44
  9. crackerjack/core/workflow_orchestrator.py +723 -72
  10. crackerjack/dynamic_config.py +1 -25
  11. crackerjack/managers/publish_manager.py +22 -5
  12. crackerjack/managers/test_command_builder.py +19 -13
  13. crackerjack/managers/test_manager.py +15 -4
  14. crackerjack/mcp/server_core.py +162 -34
  15. crackerjack/mcp/tools/core_tools.py +1 -1
  16. crackerjack/mcp/tools/execution_tools.py +16 -3
  17. crackerjack/mcp/tools/workflow_executor.py +130 -40
  18. crackerjack/mixins/__init__.py +5 -0
  19. crackerjack/mixins/error_handling.py +214 -0
  20. crackerjack/models/config.py +9 -0
  21. crackerjack/models/protocols.py +114 -0
  22. crackerjack/models/task.py +3 -0
  23. crackerjack/security/__init__.py +1 -0
  24. crackerjack/security/audit.py +226 -0
  25. crackerjack/services/config.py +3 -2
  26. crackerjack/services/config_merge.py +11 -5
  27. crackerjack/services/coverage_ratchet.py +22 -0
  28. crackerjack/services/git.py +121 -22
  29. crackerjack/services/initialization.py +25 -9
  30. crackerjack/services/memory_optimizer.py +477 -0
  31. crackerjack/services/parallel_executor.py +474 -0
  32. crackerjack/services/performance_benchmarks.py +292 -577
  33. crackerjack/services/performance_cache.py +443 -0
  34. crackerjack/services/performance_monitor.py +633 -0
  35. crackerjack/services/security.py +63 -0
  36. crackerjack/services/security_logger.py +9 -1
  37. crackerjack/services/terminal_utils.py +0 -0
  38. crackerjack/tools/validate_regex_patterns.py +14 -0
  39. {crackerjack-0.31.18.dist-info → crackerjack-0.33.0.dist-info}/METADATA +2 -2
  40. {crackerjack-0.31.18.dist-info → crackerjack-0.33.0.dist-info}/RECORD +43 -34
  41. {crackerjack-0.31.18.dist-info → crackerjack-0.33.0.dist-info}/WHEEL +0 -0
  42. {crackerjack-0.31.18.dist-info → crackerjack-0.33.0.dist-info}/entry_points.txt +0 -0
  43. {crackerjack-0.31.18.dist-info → crackerjack-0.33.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,30 +5,6 @@ from pathlib import Path
5
5
  import jinja2
6
6
 
7
7
 
8
- def _get_project_root() -> Path:
9
- """Get the absolute path to the crackerjack project root."""
10
- # Start from this file and go up to find the real project root
11
- current_path = Path(__file__).resolve()
12
- for parent in current_path.parents:
13
- if (parent / "pyproject.toml").exists() and (parent / "tools").exists():
14
- # Verify it's the crackerjack project by checking for unique markers and tools dir
15
- try:
16
- pyproject_content = (parent / "pyproject.toml").read_text()
17
- if (
18
- 'name = "crackerjack"' in pyproject_content
19
- and (
20
- parent / "tools" / "validate_regex_patterns_standalone.py"
21
- ).exists()
22
- ):
23
- return parent
24
- except Exception:
25
- # If we can't read the file, continue searching
26
- continue
27
-
28
- # Fallback to the parent of the crackerjack package directory
29
- return Path(__file__).resolve().parent.parent
30
-
31
-
32
8
  class HookMetadata(t.TypedDict):
33
9
  id: str
34
10
  name: str | None
@@ -70,7 +46,7 @@ HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
70
46
  "additional_dependencies": None,
71
47
  "types_or": None,
72
48
  "language": "system",
73
- "entry": f"uv run python {_get_project_root() / 'tools' / 'validate_regex_patterns_standalone.py'}",
49
+ "entry": "uv run python -m crackerjack.tools.validate_regex_patterns",
74
50
  "experimental": False,
75
51
  },
76
52
  {
@@ -5,17 +5,34 @@ from pathlib import Path
5
5
 
6
6
  from rich.console import Console
7
7
 
8
- from crackerjack.services.filesystem import FileSystemService
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__(self, console: Console, pkg_path: Path, dry_run: bool = False) -> None:
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
- self.filesystem = FileSystemService()
18
- self.security = SecurityService()
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)
@@ -23,17 +23,21 @@ class TestCommandBuilder:
23
23
  if hasattr(options, "test_workers") and options.test_workers:
24
24
  return options.test_workers
25
25
 
26
- import multiprocessing
27
-
28
- cpu_count = multiprocessing.cpu_count()
29
-
30
- if cpu_count <= 2:
31
- return 1
32
- elif cpu_count <= 4:
33
- return 2
34
- elif cpu_count <= 8:
35
- return 3
36
- return 4
26
+ # Temporarily disable multi-worker execution due to pytest-xdist
27
+ # hanging issues with async tests. See GitHub issue for details.
28
+ # TODO: Re-enable after fixing async test timeout issues
29
+ return 1
30
+
31
+ # Original multi-worker logic (commented out):
32
+ # import multiprocessing
33
+ # cpu_count = multiprocessing.cpu_count()
34
+ # if cpu_count <= 2:
35
+ # return 1
36
+ # elif cpu_count <= 4:
37
+ # return 2
38
+ # elif cpu_count <= 8:
39
+ # return 3
40
+ # return 4
37
41
 
38
42
  def get_test_timeout(self, options: OptionsProtocol) -> int:
39
43
  if hasattr(options, "test_timeout") and options.test_timeout:
@@ -95,7 +99,7 @@ class TestCommandBuilder:
95
99
  cmd.append(str(self.pkg_path))
96
100
 
97
101
  def build_specific_test_command(self, test_pattern: str) -> list[str]:
98
- cmd = ["python", "-m", "pytest", "-v"]
102
+ cmd = ["uv", "run", "python", "-m", "pytest", "-v"]
99
103
 
100
104
  cmd.extend(
101
105
  [
@@ -112,6 +116,8 @@ class TestCommandBuilder:
112
116
 
113
117
  def build_validation_command(self) -> list[str]:
114
118
  return [
119
+ "uv",
120
+ "run",
115
121
  "python",
116
122
  "-m",
117
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__(self, console: Console, pkg_path: Path) -> None:
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
- self.coverage_ratchet = CoverageRatchetService(pkg_path, console)
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
@@ -7,7 +7,12 @@ from typing import Final
7
7
  from rich.console import Console
8
8
 
9
9
  try:
10
- from mcp.server.fastmcp import FastMCP
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
- mcp_app = FastMCP("crackerjack - mcp-server")
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 main(project_path_arg: str = ".", websocket_port: int | None = None) -> None:
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
- config = MCPServerConfig(
188
- project_path=project_path,
189
- rate_limit_config=RateLimitConfig(),
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
- if websocket_port:
196
- context.websocket_server_port = websocket_port
317
+ # Set up server context
318
+ _setup_server_context(project_path, websocket_port)
197
319
 
198
- _initialize_context(context)
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
- console.print("[green]Starting Crackerjack MCP Server...[/ green]")
206
- console.print(f"Project path: {project_path}")
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
- console.print("[yellow]MCP app created, about to run...[/ yellow]")
211
- try:
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
- project_path = sys.argv[1] if len(sys.argv) > 1 else "."
236
- websocket_port = int(sys.argv[2]) if len(sys.argv) > 2 else None
237
-
238
- main(project_path, websocket_port)
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.initialize_project(target_path=Path.cwd())
189
+ results = init_service.initialize_project_full(target_path=Path.cwd())
190
190
 
191
191
  return results.get("success", False)
192
192
 
@@ -29,6 +29,14 @@ def _register_execute_crackerjack_tool(mcp_app: t.Any) -> None:
29
29
 
30
30
  extra_kwargs = kwargs_result["kwargs"]
31
31
 
32
+ # Add extended timeout for long-running operations
33
+ if "execution_timeout" not in extra_kwargs:
34
+ # Default to 15 minutes, extend to 20 minutes for test operations
35
+ if extra_kwargs.get("test", False) or extra_kwargs.get("testing", False):
36
+ extra_kwargs["execution_timeout"] = 1200 # 20 minutes for tests
37
+ else:
38
+ extra_kwargs["execution_timeout"] = 900 # 15 minutes default
39
+
32
40
  try:
33
41
  result = await execute_crackerjack_workflow(args, extra_kwargs)
34
42
  return json.dumps(result, indent=2)
@@ -153,9 +161,14 @@ def _execute_initialization(target_path: t.Any, force: bool) -> dict[str, t.Any]
153
161
 
154
162
  console = Console()
155
163
 
156
- return InitializationService(console, target_path).initialize_project(
157
- force_update=force
158
- )
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)
159
172
 
160
173
 
161
174
  def _create_init_error_response(message: str) -> str: