crackerjack 0.31.10__py3-none-any.whl → 0.31.13__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 +288 -705
- crackerjack/__main__.py +22 -8
- crackerjack/agents/__init__.py +0 -3
- crackerjack/agents/architect_agent.py +0 -43
- crackerjack/agents/base.py +1 -9
- crackerjack/agents/coordinator.py +2 -148
- crackerjack/agents/documentation_agent.py +109 -81
- crackerjack/agents/dry_agent.py +122 -97
- crackerjack/agents/formatting_agent.py +3 -16
- crackerjack/agents/import_optimization_agent.py +1174 -130
- crackerjack/agents/performance_agent.py +956 -188
- crackerjack/agents/performance_helpers.py +229 -0
- crackerjack/agents/proactive_agent.py +1 -48
- crackerjack/agents/refactoring_agent.py +516 -246
- crackerjack/agents/refactoring_helpers.py +282 -0
- crackerjack/agents/security_agent.py +393 -90
- crackerjack/agents/test_creation_agent.py +1776 -120
- crackerjack/agents/test_specialist_agent.py +59 -15
- crackerjack/agents/tracker.py +0 -102
- crackerjack/api.py +145 -37
- crackerjack/cli/handlers.py +48 -30
- crackerjack/cli/interactive.py +11 -11
- crackerjack/cli/options.py +66 -4
- crackerjack/code_cleaner.py +808 -148
- crackerjack/config/global_lock_config.py +110 -0
- crackerjack/config/hooks.py +43 -64
- crackerjack/core/async_workflow_orchestrator.py +247 -97
- crackerjack/core/autofix_coordinator.py +192 -109
- crackerjack/core/enhanced_container.py +46 -63
- crackerjack/core/file_lifecycle.py +549 -0
- crackerjack/core/performance.py +9 -8
- crackerjack/core/performance_monitor.py +395 -0
- crackerjack/core/phase_coordinator.py +281 -94
- crackerjack/core/proactive_workflow.py +9 -58
- crackerjack/core/resource_manager.py +501 -0
- crackerjack/core/service_watchdog.py +490 -0
- crackerjack/core/session_coordinator.py +4 -8
- crackerjack/core/timeout_manager.py +504 -0
- crackerjack/core/websocket_lifecycle.py +475 -0
- crackerjack/core/workflow_orchestrator.py +343 -209
- crackerjack/dynamic_config.py +50 -9
- crackerjack/errors.py +3 -4
- crackerjack/executors/async_hook_executor.py +63 -13
- crackerjack/executors/cached_hook_executor.py +14 -14
- crackerjack/executors/hook_executor.py +100 -37
- crackerjack/executors/hook_lock_manager.py +856 -0
- crackerjack/executors/individual_hook_executor.py +120 -86
- crackerjack/intelligence/__init__.py +0 -7
- crackerjack/intelligence/adaptive_learning.py +13 -86
- crackerjack/intelligence/agent_orchestrator.py +15 -78
- crackerjack/intelligence/agent_registry.py +12 -59
- crackerjack/intelligence/agent_selector.py +31 -92
- crackerjack/intelligence/integration.py +1 -41
- crackerjack/interactive.py +9 -9
- crackerjack/managers/async_hook_manager.py +25 -8
- crackerjack/managers/hook_manager.py +9 -9
- crackerjack/managers/publish_manager.py +57 -59
- crackerjack/managers/test_command_builder.py +6 -36
- crackerjack/managers/test_executor.py +9 -61
- crackerjack/managers/test_manager.py +17 -63
- crackerjack/managers/test_manager_backup.py +77 -127
- crackerjack/managers/test_progress.py +4 -23
- crackerjack/mcp/cache.py +5 -12
- crackerjack/mcp/client_runner.py +10 -10
- crackerjack/mcp/context.py +64 -6
- crackerjack/mcp/dashboard.py +14 -11
- crackerjack/mcp/enhanced_progress_monitor.py +55 -55
- crackerjack/mcp/file_monitor.py +72 -42
- crackerjack/mcp/progress_components.py +103 -84
- crackerjack/mcp/progress_monitor.py +122 -49
- crackerjack/mcp/rate_limiter.py +12 -12
- crackerjack/mcp/server_core.py +16 -22
- crackerjack/mcp/service_watchdog.py +26 -26
- crackerjack/mcp/state.py +15 -0
- crackerjack/mcp/tools/core_tools.py +95 -39
- crackerjack/mcp/tools/error_analyzer.py +6 -32
- crackerjack/mcp/tools/execution_tools.py +1 -56
- crackerjack/mcp/tools/execution_tools_backup.py +35 -131
- crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
- crackerjack/mcp/tools/intelligence_tools.py +2 -55
- crackerjack/mcp/tools/monitoring_tools.py +308 -145
- crackerjack/mcp/tools/proactive_tools.py +12 -42
- crackerjack/mcp/tools/progress_tools.py +23 -15
- crackerjack/mcp/tools/utility_tools.py +3 -40
- crackerjack/mcp/tools/workflow_executor.py +40 -60
- crackerjack/mcp/websocket/app.py +0 -3
- crackerjack/mcp/websocket/endpoints.py +206 -268
- crackerjack/mcp/websocket/jobs.py +213 -66
- crackerjack/mcp/websocket/server.py +84 -6
- crackerjack/mcp/websocket/websocket_handler.py +137 -29
- crackerjack/models/config_adapter.py +3 -16
- crackerjack/models/protocols.py +162 -3
- crackerjack/models/resource_protocols.py +454 -0
- crackerjack/models/task.py +3 -3
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +25 -71
- crackerjack/monitoring/regression_prevention.py +28 -87
- crackerjack/orchestration/advanced_orchestrator.py +44 -78
- crackerjack/orchestration/coverage_improvement.py +10 -60
- crackerjack/orchestration/execution_strategies.py +16 -16
- crackerjack/orchestration/test_progress_streamer.py +61 -53
- crackerjack/plugins/base.py +1 -1
- crackerjack/plugins/managers.py +22 -20
- crackerjack/py313.py +65 -21
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +627 -0
- crackerjack/services/cache.py +7 -9
- crackerjack/services/config.py +35 -52
- crackerjack/services/config_integrity.py +5 -16
- crackerjack/services/config_merge.py +542 -0
- crackerjack/services/contextual_ai_assistant.py +17 -19
- crackerjack/services/coverage_ratchet.py +44 -73
- crackerjack/services/debug.py +25 -39
- crackerjack/services/dependency_monitor.py +52 -50
- crackerjack/services/enhanced_filesystem.py +14 -11
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/filesystem.py +1 -12
- crackerjack/services/git.py +71 -47
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +276 -428
- crackerjack/services/input_validator.py +760 -0
- crackerjack/services/log_manager.py +16 -16
- crackerjack/services/logging.py +7 -6
- crackerjack/services/metrics.py +43 -43
- crackerjack/services/pattern_cache.py +2 -31
- crackerjack/services/pattern_detector.py +26 -63
- crackerjack/services/performance_benchmarks.py +20 -45
- crackerjack/services/regex_patterns.py +2887 -0
- crackerjack/services/regex_utils.py +537 -0
- crackerjack/services/secure_path_utils.py +683 -0
- crackerjack/services/secure_status_formatter.py +534 -0
- crackerjack/services/secure_subprocess.py +605 -0
- crackerjack/services/security.py +47 -10
- crackerjack/services/security_logger.py +492 -0
- crackerjack/services/server_manager.py +109 -50
- crackerjack/services/smart_scheduling.py +8 -25
- crackerjack/services/status_authentication.py +603 -0
- crackerjack/services/status_security_manager.py +442 -0
- crackerjack/services/thread_safe_status_collector.py +546 -0
- crackerjack/services/tool_version_service.py +1 -23
- crackerjack/services/unified_config.py +36 -58
- crackerjack/services/validation_rate_limiter.py +269 -0
- crackerjack/services/version_checker.py +9 -40
- crackerjack/services/websocket_resource_limiter.py +572 -0
- crackerjack/slash_commands/__init__.py +52 -2
- crackerjack/tools/__init__.py +0 -0
- crackerjack/tools/validate_input_validator_patterns.py +262 -0
- crackerjack/tools/validate_regex_patterns.py +198 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/METADATA +197 -12
- crackerjack-0.31.13.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.10.dist-info/RECORD +0 -149
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,12 +4,15 @@ import typing as t
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
import tomli
|
|
7
|
-
import tomli_w
|
|
8
7
|
import yaml
|
|
9
8
|
from rich.console import Console
|
|
10
9
|
|
|
10
|
+
from crackerjack.models.protocols import ConfigMergeServiceProtocol
|
|
11
|
+
|
|
12
|
+
from .config_merge import ConfigMergeService
|
|
11
13
|
from .filesystem import FileSystemService
|
|
12
14
|
from .git import GitService
|
|
15
|
+
from .input_validator import get_input_validator, validate_and_sanitize_path
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
class InitializationService:
|
|
@@ -19,11 +22,16 @@ class InitializationService:
|
|
|
19
22
|
filesystem: FileSystemService,
|
|
20
23
|
git_service: GitService,
|
|
21
24
|
pkg_path: Path,
|
|
25
|
+
config_merge_service: ConfigMergeServiceProtocol | None = None,
|
|
22
26
|
) -> None:
|
|
23
27
|
self.console = console
|
|
24
28
|
self.filesystem = filesystem
|
|
25
29
|
self.git_service = git_service
|
|
26
30
|
self.pkg_path = pkg_path
|
|
31
|
+
# Use dependency injection with default implementation
|
|
32
|
+
self.config_merge_service = config_merge_service or ConfigMergeService(
|
|
33
|
+
console, filesystem, git_service
|
|
34
|
+
)
|
|
27
35
|
|
|
28
36
|
def initialize_project(
|
|
29
37
|
self,
|
|
@@ -33,17 +41,42 @@ class InitializationService:
|
|
|
33
41
|
if target_path is None:
|
|
34
42
|
target_path = Path.cwd()
|
|
35
43
|
|
|
44
|
+
# Validate target path for security
|
|
45
|
+
try:
|
|
46
|
+
target_path = validate_and_sanitize_path(target_path, allow_absolute=True)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
return {
|
|
49
|
+
"target_path": str(target_path),
|
|
50
|
+
"files_copied": [],
|
|
51
|
+
"files_skipped": [],
|
|
52
|
+
"errors": [f"Invalid target path: {e}"],
|
|
53
|
+
"success": False,
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
results = self._create_results_dict(target_path)
|
|
37
57
|
|
|
38
58
|
try:
|
|
39
59
|
config_files = self._get_config_files()
|
|
40
60
|
project_name = target_path.name
|
|
41
61
|
|
|
62
|
+
# Validate project name
|
|
63
|
+
validator = get_input_validator()
|
|
64
|
+
name_result = validator.validate_project_name(project_name)
|
|
65
|
+
if not name_result.valid:
|
|
66
|
+
results["errors"].append(
|
|
67
|
+
f"Invalid project name: {name_result.error_message}"
|
|
68
|
+
)
|
|
69
|
+
results["success"] = False
|
|
70
|
+
return results
|
|
71
|
+
|
|
72
|
+
# Use sanitized project name
|
|
73
|
+
sanitized_project_name = name_result.sanitized_value
|
|
74
|
+
|
|
42
75
|
for file_name, merge_strategy in config_files.items():
|
|
43
76
|
self._process_config_file(
|
|
44
77
|
file_name,
|
|
45
78
|
merge_strategy,
|
|
46
|
-
|
|
79
|
+
sanitized_project_name,
|
|
47
80
|
target_path,
|
|
48
81
|
force,
|
|
49
82
|
results,
|
|
@@ -66,13 +99,13 @@ class InitializationService:
|
|
|
66
99
|
}
|
|
67
100
|
|
|
68
101
|
def _get_config_files(self) -> dict[str, str]:
|
|
69
|
-
"""Get config files with their merge strategies."""
|
|
70
102
|
return {
|
|
71
103
|
".pre-commit-config.yaml": "smart_merge",
|
|
72
104
|
"pyproject.toml": "smart_merge",
|
|
105
|
+
".gitignore": "smart_merge_gitignore",
|
|
73
106
|
"CLAUDE.md": "smart_append",
|
|
74
107
|
"RULES.md": "replace_if_missing",
|
|
75
|
-
"example.mcp.json": "special",
|
|
108
|
+
"example.mcp.json": "special",
|
|
76
109
|
}
|
|
77
110
|
|
|
78
111
|
def _process_config_file(
|
|
@@ -84,7 +117,6 @@ class InitializationService:
|
|
|
84
117
|
force: bool,
|
|
85
118
|
results: dict[str, t.Any],
|
|
86
119
|
) -> None:
|
|
87
|
-
# Special handling for example.mcp.json -> .mcp.json
|
|
88
120
|
if file_name == "example.mcp.json":
|
|
89
121
|
self._process_mcp_config(target_path, force, results)
|
|
90
122
|
return
|
|
@@ -97,7 +129,6 @@ class InitializationService:
|
|
|
97
129
|
return
|
|
98
130
|
|
|
99
131
|
try:
|
|
100
|
-
# Handle different merge strategies
|
|
101
132
|
if merge_strategy == "smart_merge":
|
|
102
133
|
self._smart_merge_config(
|
|
103
134
|
source_file,
|
|
@@ -107,6 +138,13 @@ class InitializationService:
|
|
|
107
138
|
force,
|
|
108
139
|
results,
|
|
109
140
|
)
|
|
141
|
+
elif merge_strategy == "smart_merge_gitignore":
|
|
142
|
+
self._smart_merge_gitignore(
|
|
143
|
+
target_file,
|
|
144
|
+
project_name,
|
|
145
|
+
force,
|
|
146
|
+
results,
|
|
147
|
+
)
|
|
110
148
|
elif merge_strategy == "smart_append":
|
|
111
149
|
self._smart_append_config(
|
|
112
150
|
source_file,
|
|
@@ -127,7 +165,6 @@ class InitializationService:
|
|
|
127
165
|
else:
|
|
128
166
|
self._skip_existing_file(file_name, results)
|
|
129
167
|
else:
|
|
130
|
-
# Fallback to old behavior
|
|
131
168
|
if not self._should_copy_file(target_file, force, file_name, results):
|
|
132
169
|
return
|
|
133
170
|
content = self._read_and_process_content(
|
|
@@ -150,7 +187,7 @@ class InitializationService:
|
|
|
150
187
|
if target_file.exists() and not force:
|
|
151
188
|
t.cast("list[str]", results["files_skipped"]).append(file_name)
|
|
152
189
|
self.console.print(
|
|
153
|
-
f"[yellow]⚠️[/yellow] Skipped {file_name} (already exists)",
|
|
190
|
+
f"[yellow]⚠️[/ yellow] Skipped {file_name} (already exists)",
|
|
154
191
|
)
|
|
155
192
|
return False
|
|
156
193
|
return True
|
|
@@ -181,13 +218,15 @@ class InitializationService:
|
|
|
181
218
|
try:
|
|
182
219
|
self.git_service.add_files([str(target_file)])
|
|
183
220
|
except Exception as e:
|
|
184
|
-
self.console.print(
|
|
221
|
+
self.console.print(
|
|
222
|
+
f"[yellow]⚠️[/ yellow] Could not git add {file_name}: {e}"
|
|
223
|
+
)
|
|
185
224
|
|
|
186
|
-
self.console.print(f"[green]✅[/green] Copied {file_name}")
|
|
225
|
+
self.console.print(f"[green]✅[/ green] Copied {file_name}")
|
|
187
226
|
|
|
188
227
|
def _skip_existing_file(self, file_name: str, results: dict[str, t.Any]) -> None:
|
|
189
228
|
t.cast("list[str]", results["files_skipped"]).append(file_name)
|
|
190
|
-
self.console.print(f"[yellow]⚠️[/yellow] Skipped {file_name} (already exists)")
|
|
229
|
+
self.console.print(f"[yellow]⚠️[/ yellow] Skipped {file_name} (already exists)")
|
|
191
230
|
|
|
192
231
|
def _handle_missing_source_file(
|
|
193
232
|
self,
|
|
@@ -196,7 +235,7 @@ class InitializationService:
|
|
|
196
235
|
) -> None:
|
|
197
236
|
error_msg = f"Source file not found: {file_name}"
|
|
198
237
|
t.cast("list[str]", results["errors"]).append(error_msg)
|
|
199
|
-
self.console.print(f"[yellow]⚠️[/yellow] {error_msg}")
|
|
238
|
+
self.console.print(f"[yellow]⚠️[/ yellow] {error_msg}")
|
|
200
239
|
|
|
201
240
|
def _handle_file_processing_error(
|
|
202
241
|
self,
|
|
@@ -207,17 +246,17 @@ class InitializationService:
|
|
|
207
246
|
error_msg = f"Failed to copy {file_name}: {error}"
|
|
208
247
|
t.cast("list[str]", results["errors"]).append(error_msg)
|
|
209
248
|
results["success"] = False
|
|
210
|
-
self.console.print(f"[red]❌[/red] {error_msg}")
|
|
249
|
+
self.console.print(f"[red]❌[/ red] {error_msg}")
|
|
211
250
|
|
|
212
251
|
def _print_summary(self, results: dict[str, t.Any]) -> None:
|
|
213
252
|
if results["success"]:
|
|
214
253
|
self.console.print(
|
|
215
|
-
f"[green]🎉 Project initialized successfully ! [/green] "
|
|
254
|
+
f"[green]🎉 Project initialized successfully ! [/ green] "
|
|
216
255
|
f"Copied {len(t.cast('list[str]', results['files_copied']))} files",
|
|
217
256
|
)
|
|
218
257
|
else:
|
|
219
258
|
self.console.print(
|
|
220
|
-
"[red]❌ Project initialization completed with errors[/red]",
|
|
259
|
+
"[red]❌ Project initialization completed with errors[/ red]",
|
|
221
260
|
)
|
|
222
261
|
|
|
223
262
|
def _handle_initialization_error(
|
|
@@ -227,7 +266,7 @@ class InitializationService:
|
|
|
227
266
|
) -> None:
|
|
228
267
|
results["success"] = False
|
|
229
268
|
t.cast("list[str]", results["errors"]).append(f"Initialization failed: {error}")
|
|
230
|
-
self.console.print(f"[red]❌[/red] Initialization failed: {error}")
|
|
269
|
+
self.console.print(f"[red]❌[/ red] Initialization failed: {error}")
|
|
231
270
|
|
|
232
271
|
def check_uv_installed(self) -> bool:
|
|
233
272
|
try:
|
|
@@ -248,10 +287,8 @@ class InitializationService:
|
|
|
248
287
|
force: bool,
|
|
249
288
|
results: dict[str, t.Any],
|
|
250
289
|
) -> None:
|
|
251
|
-
"""Handle special processing for example.mcp.json -> .mcp.json with merging."""
|
|
252
|
-
# Source: example.mcp.json in crackerjack package (contains servers to add to projects)
|
|
253
290
|
source_file = self.pkg_path / "example.mcp.json"
|
|
254
|
-
|
|
291
|
+
|
|
255
292
|
target_file = target_path / ".mcp.json"
|
|
256
293
|
|
|
257
294
|
if not source_file.exists():
|
|
@@ -259,7 +296,6 @@ class InitializationService:
|
|
|
259
296
|
return
|
|
260
297
|
|
|
261
298
|
try:
|
|
262
|
-
# Load the crackerjack MCP servers to add
|
|
263
299
|
with source_file.open() as f:
|
|
264
300
|
source_config = json.load(f)
|
|
265
301
|
|
|
@@ -273,26 +309,22 @@ class InitializationService:
|
|
|
273
309
|
|
|
274
310
|
crackerjack_servers = source_config["mcpServers"]
|
|
275
311
|
|
|
276
|
-
# If target .mcp.json doesn't exist, create it with crackerjack servers
|
|
277
312
|
if not target_file.exists():
|
|
278
313
|
target_config = {"mcpServers": crackerjack_servers}
|
|
279
314
|
self._write_mcp_config_and_track(target_file, target_config, results)
|
|
280
315
|
self.console.print(
|
|
281
|
-
"[green]✅[/green] Created .mcp.json with crackerjack MCP servers",
|
|
316
|
+
"[green]✅[/ green] Created .mcp.json with crackerjack MCP servers",
|
|
282
317
|
)
|
|
283
318
|
return
|
|
284
319
|
|
|
285
|
-
# If target exists and force=False, skip unless we're merging
|
|
286
320
|
if target_file.exists() and not force:
|
|
287
|
-
# Always merge crackerjack servers into existing config
|
|
288
321
|
self._merge_mcp_config(target_file, crackerjack_servers, results)
|
|
289
322
|
return
|
|
290
323
|
|
|
291
|
-
# If force=True, replace entirely with crackerjack servers
|
|
292
324
|
target_config = {"mcpServers": crackerjack_servers}
|
|
293
325
|
self._write_mcp_config_and_track(target_file, target_config, results)
|
|
294
326
|
self.console.print(
|
|
295
|
-
"[green]✅[/green] Updated .mcp.json with crackerjack MCP servers",
|
|
327
|
+
"[green]✅[/ green] Updated .mcp.json with crackerjack MCP servers",
|
|
296
328
|
)
|
|
297
329
|
|
|
298
330
|
except Exception as e:
|
|
@@ -304,34 +336,29 @@ class InitializationService:
|
|
|
304
336
|
crackerjack_servers: dict[str, t.Any],
|
|
305
337
|
results: dict[str, t.Any],
|
|
306
338
|
) -> None:
|
|
307
|
-
"""Merge crackerjack servers into existing .mcp.json."""
|
|
308
339
|
try:
|
|
309
|
-
# Load existing config
|
|
310
340
|
with target_file.open() as f:
|
|
311
341
|
existing_config = json.load(f)
|
|
312
342
|
|
|
313
343
|
if not isinstance(existing_config.get("mcpServers"), dict):
|
|
314
344
|
existing_config["mcpServers"] = {}
|
|
315
345
|
|
|
316
|
-
# Merge crackerjack servers (they override existing ones with same name)
|
|
317
346
|
existing_servers = existing_config["mcpServers"]
|
|
318
347
|
updated_servers = {}
|
|
319
348
|
|
|
320
349
|
for name, config in crackerjack_servers.items():
|
|
321
350
|
if name in existing_servers:
|
|
322
351
|
self.console.print(
|
|
323
|
-
f"[yellow]🔄[/yellow] Updating existing MCP server: {name}",
|
|
352
|
+
f"[yellow]🔄[/ yellow] Updating existing MCP server: {name}",
|
|
324
353
|
)
|
|
325
354
|
else:
|
|
326
355
|
self.console.print(
|
|
327
|
-
f"[green]➕[/green] Adding new MCP server: {name}",
|
|
356
|
+
f"[green]➕[/ green] Adding new MCP server: {name}",
|
|
328
357
|
)
|
|
329
358
|
updated_servers[name] = config
|
|
330
359
|
|
|
331
|
-
# Merge into existing config
|
|
332
360
|
existing_servers.update(updated_servers)
|
|
333
361
|
|
|
334
|
-
# Write the merged config
|
|
335
362
|
self._write_mcp_config_and_track(target_file, existing_config, results)
|
|
336
363
|
|
|
337
364
|
t.cast("list[str]", results["files_copied"]).append(".mcp.json (merged)")
|
|
@@ -345,17 +372,15 @@ class InitializationService:
|
|
|
345
372
|
config: dict[str, t.Any],
|
|
346
373
|
results: dict[str, t.Any],
|
|
347
374
|
) -> None:
|
|
348
|
-
"""Write MCP config file and track in results."""
|
|
349
375
|
with target_file.open("w") as f:
|
|
350
376
|
json.dump(config, f, indent=2)
|
|
351
377
|
|
|
352
378
|
t.cast("list[str]", results["files_copied"]).append(".mcp.json")
|
|
353
379
|
|
|
354
|
-
# Try to git add the file
|
|
355
380
|
try:
|
|
356
381
|
self.git_service.add_files([str(target_file)])
|
|
357
382
|
except Exception as e:
|
|
358
|
-
self.console.print(f"[yellow]⚠️[/yellow] Could not git add .mcp.json: {e}")
|
|
383
|
+
self.console.print(f"[yellow]⚠️[/ yellow] Could not git add .mcp.json: {e}")
|
|
359
384
|
|
|
360
385
|
def validate_project_structure(self) -> bool:
|
|
361
386
|
required_indicators = [
|
|
@@ -366,17 +391,14 @@ class InitializationService:
|
|
|
366
391
|
return any(path.exists() for path in required_indicators)
|
|
367
392
|
|
|
368
393
|
def _generate_project_claude_content(self, project_name: str) -> str:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
# Crackerjack Integration for {project_name}
|
|
394
|
+
return """
|
|
395
|
+
|
|
372
396
|
|
|
373
397
|
This project uses crackerjack for Python project management and quality assurance.
|
|
374
398
|
|
|
375
|
-
## Recommended Claude Code Agents
|
|
376
399
|
|
|
377
|
-
For optimal development experience with this crackerjack-enabled project, use these specialized agents:
|
|
400
|
+
For optimal development experience with this crackerjack - enabled project, use these specialized agents:
|
|
378
401
|
|
|
379
|
-
### **Primary Agents (Use for all Python development)**
|
|
380
402
|
|
|
381
403
|
- **🏗️ crackerjack-architect**: Expert in crackerjack's modular architecture and Python project management patterns. **Use PROACTIVELY** for all feature development, architectural decisions, and ensuring code follows crackerjack standards from the start.
|
|
382
404
|
|
|
@@ -384,81 +406,74 @@ For optimal development experience with this crackerjack-enabled project, use th
|
|
|
384
406
|
|
|
385
407
|
- **🧪 pytest-hypothesis-specialist**: Advanced testing patterns, property-based testing, and test optimization
|
|
386
408
|
|
|
387
|
-
### **Task-Specific Agents**
|
|
388
409
|
|
|
389
410
|
- **🧪 crackerjack-test-specialist**: Advanced testing specialist for complex testing scenarios and coverage optimization
|
|
390
411
|
- **🏗️ backend-architect**: System design, API architecture, and service integration patterns
|
|
391
412
|
- **🔒 security-auditor**: Security analysis, vulnerability detection, and secure coding practices
|
|
392
413
|
|
|
393
|
-
### **Agent Usage Patterns**
|
|
394
414
|
|
|
395
415
|
```bash
|
|
396
|
-
# Start development with crackerjack-compliant architecture
|
|
397
|
-
Task tool with subagent_type="crackerjack-architect" for feature planning
|
|
398
416
|
|
|
399
|
-
|
|
400
|
-
|
|
417
|
+
Task tool with subagent_type ="crackerjack-architect" for feature planning
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
Task tool with subagent_type ="python-pro" for code implementation
|
|
401
421
|
|
|
402
|
-
# Add comprehensive testing
|
|
403
|
-
Task tool with subagent_type="pytest-hypothesis-specialist" for test development
|
|
404
422
|
|
|
405
|
-
|
|
406
|
-
|
|
423
|
+
Task tool with subagent_type ="pytest-hypothesis-specialist" for test development
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
Task tool with subagent_type ="security-auditor" for security analysis
|
|
407
427
|
```
|
|
408
428
|
|
|
409
429
|
**💡 Pro Tip**: The crackerjack-architect agent automatically ensures code follows crackerjack patterns from the start, eliminating the need for retrofitting and quality fixes.
|
|
410
430
|
|
|
411
|
-
## Crackerjack Quality Standards
|
|
412
431
|
|
|
413
432
|
This project follows crackerjack's clean code philosophy:
|
|
414
433
|
|
|
415
|
-
|
|
434
|
+
|
|
416
435
|
- **EVERY LINE OF CODE IS A LIABILITY**: The best code is no code
|
|
417
436
|
- **DRY (Don't Repeat Yourself)**: If you write it twice, you're doing it wrong
|
|
418
437
|
- **YAGNI (You Ain't Gonna Need It)**: Build only what's needed NOW
|
|
419
438
|
- **KISS (Keep It Simple, Stupid)**: Complexity is the enemy of maintainability
|
|
420
439
|
|
|
421
|
-
|
|
422
|
-
- **Cognitive complexity ≤15**
|
|
440
|
+
|
|
441
|
+
- **Cognitive complexity ≤15 **per function (automatically enforced)
|
|
423
442
|
- **Coverage ratchet system**: Never decrease coverage, always improve toward 100%
|
|
424
443
|
- **Type annotations required**: All functions must have return type hints
|
|
425
444
|
- **Security patterns**: No hardcoded paths, proper temp file handling
|
|
426
445
|
- **Python 3.13+ modern patterns**: Use `|` unions, pathlib over os.path
|
|
427
446
|
|
|
428
|
-
## Development Workflow
|
|
429
447
|
|
|
430
|
-
### **Quality Commands**
|
|
431
448
|
```bash
|
|
432
|
-
|
|
449
|
+
|
|
433
450
|
python -m crackerjack
|
|
434
451
|
|
|
435
|
-
# With comprehensive testing
|
|
436
|
-
python -m crackerjack -t
|
|
437
452
|
|
|
438
|
-
|
|
439
|
-
|
|
453
|
+
python -m crackerjack - t
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
python -m crackerjack - - ai - agent - t
|
|
440
457
|
|
|
441
|
-
|
|
442
|
-
python -m crackerjack -a patch
|
|
458
|
+
|
|
459
|
+
python -m crackerjack - a patch
|
|
443
460
|
```
|
|
444
461
|
|
|
445
|
-
|
|
462
|
+
|
|
446
463
|
1. **Plan with crackerjack-architect**: Ensure proper architecture from the start
|
|
447
464
|
2. **Implement with python-pro**: Follow modern Python patterns
|
|
448
465
|
3. **Test comprehensively**: Use pytest-hypothesis-specialist for robust testing
|
|
449
466
|
4. **Run quality checks**: `python -m crackerjack -t` before committing
|
|
450
467
|
5. **Security review**: Use security-auditor for final validation
|
|
451
468
|
|
|
452
|
-
## Important Instructions
|
|
453
469
|
|
|
454
470
|
- **Use crackerjack-architect agent proactively** for all significant code changes
|
|
455
471
|
- **Never reduce test coverage** - the ratchet system only allows improvements
|
|
456
472
|
- **Follow crackerjack patterns** - the tools will enforce quality automatically
|
|
457
473
|
- **Leverage AI agent auto-fixing** - `python -m crackerjack --ai-agent -t` for autonomous quality fixes
|
|
458
474
|
|
|
459
|
-
|
|
460
|
-
*This project is enhanced by crackerjack's intelligent Python project management.*
|
|
461
|
-
""".strip()
|
|
475
|
+
- --
|
|
476
|
+
* This project is enhanced by crackerjack's intelligent Python project management.*"""
|
|
462
477
|
|
|
463
478
|
def _smart_append_config(
|
|
464
479
|
self,
|
|
@@ -469,57 +484,110 @@ python -m crackerjack -a patch
|
|
|
469
484
|
force: bool,
|
|
470
485
|
results: dict[str, t.Any],
|
|
471
486
|
) -> None:
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
487
|
+
try:
|
|
488
|
+
# Generate appropriate source content
|
|
489
|
+
if file_name == "CLAUDE.md" and project_name != "crackerjack":
|
|
490
|
+
source_content = self._generate_project_claude_content(project_name)
|
|
491
|
+
else:
|
|
492
|
+
source_content = self._read_and_process_content(
|
|
493
|
+
source_file, True, project_name
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Define markers for this file type
|
|
497
|
+
crackerjack_start_marker = "<!-- CRACKERJACK INTEGRATION START -->"
|
|
498
|
+
crackerjack_end_marker = "<!-- CRACKERJACK INTEGRATION END -->"
|
|
499
|
+
|
|
500
|
+
# Delegate to ConfigMergeService for smart append logic
|
|
501
|
+
merged_content = self.config_merge_service.smart_append_file(
|
|
502
|
+
source_content,
|
|
503
|
+
target_file,
|
|
504
|
+
crackerjack_start_marker,
|
|
505
|
+
crackerjack_end_marker,
|
|
506
|
+
force,
|
|
479
507
|
)
|
|
480
508
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
509
|
+
# Check if content was actually changed
|
|
510
|
+
if target_file.exists():
|
|
511
|
+
existing_content = target_file.read_text()
|
|
512
|
+
if crackerjack_start_marker in existing_content and not force:
|
|
513
|
+
self._skip_existing_file(
|
|
514
|
+
f"{file_name} (crackerjack section)", results
|
|
515
|
+
)
|
|
516
|
+
return
|
|
485
517
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if crackerjack_start_marker in existing_content:
|
|
493
|
-
if force:
|
|
494
|
-
# Replace existing crackerjack section
|
|
495
|
-
start_idx = existing_content.find(crackerjack_start_marker)
|
|
496
|
-
end_idx = existing_content.find(crackerjack_end_marker)
|
|
497
|
-
if end_idx != -1:
|
|
498
|
-
end_idx += len(crackerjack_end_marker)
|
|
499
|
-
# Remove old crackerjack section
|
|
500
|
-
existing_content = (
|
|
501
|
-
existing_content[:start_idx] + existing_content[end_idx:]
|
|
502
|
-
).strip()
|
|
503
|
-
else:
|
|
504
|
-
self._skip_existing_file(f"{file_name} (crackerjack section)", results)
|
|
505
|
-
return
|
|
518
|
+
# Write the merged content
|
|
519
|
+
target_file.write_text(merged_content)
|
|
520
|
+
t.cast("list[str]", results["files_copied"]).append(
|
|
521
|
+
f"{file_name} (appended)"
|
|
522
|
+
)
|
|
506
523
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
524
|
+
try:
|
|
525
|
+
self.git_service.add_files([str(target_file)])
|
|
526
|
+
except Exception as e:
|
|
527
|
+
self.console.print(
|
|
528
|
+
f"[yellow]⚠️[/ yellow] Could not git add {file_name}: {e}"
|
|
529
|
+
)
|
|
513
530
|
|
|
514
|
-
|
|
515
|
-
t.cast("list[str]", results["files_copied"]).append(f"{file_name} (appended)")
|
|
531
|
+
self.console.print(f"[green]✅[/ green] Appended to {file_name}")
|
|
516
532
|
|
|
517
|
-
try:
|
|
518
|
-
self.git_service.add_files([str(target_file)])
|
|
519
533
|
except Exception as e:
|
|
520
|
-
self.
|
|
534
|
+
self._handle_file_processing_error(file_name, e, results)
|
|
521
535
|
|
|
522
|
-
|
|
536
|
+
def _smart_merge_gitignore(
|
|
537
|
+
self,
|
|
538
|
+
target_file: Path,
|
|
539
|
+
project_name: str,
|
|
540
|
+
force: bool,
|
|
541
|
+
results: dict[str, t.Any],
|
|
542
|
+
) -> None:
|
|
543
|
+
"""Smart merge .gitignore patterns using ConfigMergeService."""
|
|
544
|
+
# Define crackerjack .gitignore patterns
|
|
545
|
+
gitignore_patterns = [
|
|
546
|
+
"# Build/Distribution",
|
|
547
|
+
"/build/",
|
|
548
|
+
"/dist/",
|
|
549
|
+
"*.egg-info/",
|
|
550
|
+
"",
|
|
551
|
+
"# Caches",
|
|
552
|
+
"__pycache__/",
|
|
553
|
+
".mypy_cache/",
|
|
554
|
+
".ruff_cache/",
|
|
555
|
+
".pytest_cache/",
|
|
556
|
+
"",
|
|
557
|
+
"# Coverage",
|
|
558
|
+
".coverage*",
|
|
559
|
+
"htmlcov/",
|
|
560
|
+
"",
|
|
561
|
+
"# Development",
|
|
562
|
+
".venv/",
|
|
563
|
+
".DS_STORE",
|
|
564
|
+
"*.pyc",
|
|
565
|
+
"",
|
|
566
|
+
"# Crackerjack specific",
|
|
567
|
+
"crackerjack-debug-*.log",
|
|
568
|
+
"crackerjack-ai-debug-*.log",
|
|
569
|
+
".crackerjack-*",
|
|
570
|
+
]
|
|
571
|
+
|
|
572
|
+
try:
|
|
573
|
+
merged_content = self.config_merge_service.smart_merge_gitignore(
|
|
574
|
+
gitignore_patterns, target_file
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
target_file.write_text(merged_content)
|
|
578
|
+
t.cast("list[str]", results["files_copied"]).append(".gitignore (merged)")
|
|
579
|
+
|
|
580
|
+
try:
|
|
581
|
+
self.git_service.add_files([str(target_file)])
|
|
582
|
+
except Exception as e:
|
|
583
|
+
self.console.print(
|
|
584
|
+
f"[yellow]⚠️[/ yellow] Could not git add .gitignore: {e}"
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
self.console.print("[green]✅[/ green] Smart merged .gitignore")
|
|
588
|
+
|
|
589
|
+
except Exception as e:
|
|
590
|
+
self._handle_file_processing_error(".gitignore", e, results)
|
|
523
591
|
|
|
524
592
|
def _smart_merge_config(
|
|
525
593
|
self,
|
|
@@ -530,7 +598,6 @@ python -m crackerjack -a patch
|
|
|
530
598
|
force: bool,
|
|
531
599
|
results: dict[str, t.Any],
|
|
532
600
|
) -> None:
|
|
533
|
-
"""Smart merge for configuration files."""
|
|
534
601
|
if file_name == "pyproject.toml":
|
|
535
602
|
self._smart_merge_pyproject(
|
|
536
603
|
source_file,
|
|
@@ -547,7 +614,6 @@ python -m crackerjack -a patch
|
|
|
547
614
|
force,
|
|
548
615
|
results,
|
|
549
616
|
)
|
|
550
|
-
# Fallback to regular copy
|
|
551
617
|
elif not target_file.exists() or force:
|
|
552
618
|
content = self._read_and_process_content(
|
|
553
619
|
source_file,
|
|
@@ -566,357 +632,139 @@ python -m crackerjack -a patch
|
|
|
566
632
|
force: bool,
|
|
567
633
|
results: dict[str, t.Any],
|
|
568
634
|
) -> None:
|
|
569
|
-
"""Intelligently merge pyproject.toml configurations."""
|
|
570
|
-
# Load source (crackerjack) config
|
|
571
|
-
with source_file.open("rb") as f:
|
|
572
|
-
source_config = tomli.load(f)
|
|
573
|
-
|
|
574
|
-
if not target_file.exists():
|
|
575
|
-
# No existing file, just copy and replace project name
|
|
576
|
-
content = self._read_and_process_content(source_file, True, project_name)
|
|
577
|
-
self._write_file_and_track(target_file, content, "pyproject.toml", results)
|
|
578
|
-
return
|
|
579
|
-
|
|
580
|
-
# Load existing config
|
|
581
|
-
with target_file.open("rb") as f:
|
|
582
|
-
target_config = tomli.load(f)
|
|
583
|
-
|
|
584
|
-
# 1. Ensure crackerjack is in dev dependencies
|
|
585
|
-
self._ensure_crackerjack_dev_dependency(target_config, source_config)
|
|
586
|
-
|
|
587
|
-
# 2. Merge tool configurations
|
|
588
|
-
self._merge_tool_configurations(target_config, source_config, project_name)
|
|
589
|
-
|
|
590
|
-
# 3. Remove any fixed coverage requirements (use ratchet system instead)
|
|
591
|
-
self._remove_fixed_coverage_requirements(target_config)
|
|
592
|
-
|
|
593
|
-
# Write merged config with proper formatting
|
|
594
|
-
import io
|
|
595
|
-
|
|
596
|
-
# Use in-memory buffer to clean content before writing
|
|
597
|
-
buffer = io.BytesIO()
|
|
598
|
-
tomli_w.dump(target_config, buffer)
|
|
599
|
-
content = buffer.getvalue().decode("utf-8")
|
|
600
|
-
|
|
601
|
-
# Clean trailing whitespace and ensure single trailing newline
|
|
602
|
-
from crackerjack.services.filesystem import FileSystemService
|
|
603
|
-
|
|
604
|
-
content = FileSystemService.clean_trailing_whitespace_and_newlines(content)
|
|
605
|
-
|
|
606
|
-
with target_file.open("w", encoding="utf-8") as f:
|
|
607
|
-
f.write(content)
|
|
608
|
-
|
|
609
|
-
t.cast("list[str]", results["files_copied"]).append("pyproject.toml (merged)")
|
|
610
|
-
|
|
611
635
|
try:
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
self.console.print(
|
|
615
|
-
f"[yellow]⚠️[/yellow] Could not git add pyproject.toml: {e}",
|
|
616
|
-
)
|
|
617
|
-
|
|
618
|
-
self.console.print("[green]✅[/green] Smart merged pyproject.toml")
|
|
636
|
+
with source_file.open("rb") as f:
|
|
637
|
+
source_config = tomli.load(f)
|
|
619
638
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
) -> None:
|
|
625
|
-
"""Ensure crackerjack is in dev dependencies."""
|
|
626
|
-
# Check different dependency group structures
|
|
627
|
-
if "dependency-groups" not in target_config:
|
|
628
|
-
target_config["dependency-groups"] = {}
|
|
639
|
+
# Delegate to ConfigMergeService for smart merge logic
|
|
640
|
+
merged_config = self.config_merge_service.smart_merge_pyproject(
|
|
641
|
+
source_config, target_file, project_name
|
|
642
|
+
)
|
|
629
643
|
|
|
630
|
-
|
|
631
|
-
|
|
644
|
+
# Write the merged configuration
|
|
645
|
+
self.config_merge_service.write_pyproject_config(merged_config, target_file)
|
|
632
646
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
dev_deps.append("crackerjack")
|
|
647
|
+
t.cast("list[str]", results["files_copied"]).append(
|
|
648
|
+
"pyproject.toml (merged)"
|
|
649
|
+
)
|
|
637
650
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
"""Merge tool configurations, preserving existing settings."""
|
|
645
|
-
source_tools = source_config.get("tool", {})
|
|
646
|
-
|
|
647
|
-
if "tool" not in target_config:
|
|
648
|
-
target_config["tool"] = {}
|
|
649
|
-
|
|
650
|
-
target_tools = target_config["tool"]
|
|
651
|
-
|
|
652
|
-
# Tools to merge (add if missing, preserve if existing)
|
|
653
|
-
tools_to_merge = [
|
|
654
|
-
"ruff",
|
|
655
|
-
"pyright",
|
|
656
|
-
"bandit",
|
|
657
|
-
"vulture",
|
|
658
|
-
"refurb",
|
|
659
|
-
"complexipy",
|
|
660
|
-
"codespell",
|
|
661
|
-
"creosote",
|
|
662
|
-
]
|
|
651
|
+
try:
|
|
652
|
+
self.git_service.add_files([str(target_file)])
|
|
653
|
+
except Exception as e:
|
|
654
|
+
self.console.print(
|
|
655
|
+
f"[yellow]⚠️[/ yellow] Could not git add pyproject.toml: {e}",
|
|
656
|
+
)
|
|
663
657
|
|
|
664
|
-
|
|
665
|
-
if tool_name in source_tools:
|
|
666
|
-
if tool_name not in target_tools:
|
|
667
|
-
# Tool missing, add it with project-name replacement
|
|
668
|
-
target_tools[tool_name] = self._replace_project_name_in_tool_config(
|
|
669
|
-
source_tools[tool_name], project_name
|
|
670
|
-
)
|
|
671
|
-
self.console.print(
|
|
672
|
-
f"[green]➕[/green] Added [tool.{tool_name}] configuration",
|
|
673
|
-
)
|
|
674
|
-
else:
|
|
675
|
-
# Tool exists, merge settings
|
|
676
|
-
self._merge_tool_settings(
|
|
677
|
-
target_tools[tool_name],
|
|
678
|
-
source_tools[tool_name],
|
|
679
|
-
tool_name,
|
|
680
|
-
project_name,
|
|
681
|
-
)
|
|
658
|
+
self.console.print("[green]✅[/ green] Smart merged pyproject.toml")
|
|
682
659
|
|
|
683
|
-
|
|
684
|
-
|
|
660
|
+
except Exception as e:
|
|
661
|
+
self._handle_file_processing_error("pyproject.toml", e, results)
|
|
685
662
|
|
|
686
|
-
def
|
|
663
|
+
def _smart_merge_pre_commit_config(
|
|
687
664
|
self,
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
tool_name: str,
|
|
665
|
+
source_file: Path,
|
|
666
|
+
target_file: Path,
|
|
691
667
|
project_name: str,
|
|
668
|
+
force: bool,
|
|
669
|
+
results: dict[str, t.Any],
|
|
692
670
|
) -> None:
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
if key not in target_tool:
|
|
698
|
-
target_tool[key] = self._replace_project_name_in_config_value(
|
|
699
|
-
value, project_name
|
|
700
|
-
)
|
|
701
|
-
updated_keys.append(key)
|
|
671
|
+
try:
|
|
672
|
+
source_config = self._load_source_config(source_file)
|
|
673
|
+
if source_config is None:
|
|
674
|
+
return
|
|
702
675
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
f"[yellow]🔄[/yellow] Updated [tool.{tool_name}] with: {', '.join(updated_keys)}",
|
|
676
|
+
merged_config = self._perform_config_merge(
|
|
677
|
+
source_config, target_file, project_name
|
|
706
678
|
)
|
|
707
679
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
target_tools: dict[str, t.Any],
|
|
711
|
-
source_tools: dict[str, t.Any],
|
|
712
|
-
) -> None:
|
|
713
|
-
"""Merge pytest markers without duplication."""
|
|
714
|
-
if "pytest" not in source_tools or "pytest" not in target_tools:
|
|
715
|
-
return
|
|
716
|
-
|
|
717
|
-
source_pytest = source_tools["pytest"]
|
|
718
|
-
target_pytest = target_tools["pytest"]
|
|
719
|
-
|
|
720
|
-
if "ini_options" not in source_pytest or "ini_options" not in target_pytest:
|
|
721
|
-
return
|
|
722
|
-
|
|
723
|
-
source_markers = source_pytest["ini_options"].get("markers", [])
|
|
724
|
-
target_markers = target_pytest["ini_options"].get("markers", [])
|
|
725
|
-
|
|
726
|
-
# Extract marker names to avoid duplication
|
|
727
|
-
existing_marker_names = {marker.split(":")[0] for marker in target_markers}
|
|
728
|
-
new_markers = [
|
|
729
|
-
marker
|
|
730
|
-
for marker in source_markers
|
|
731
|
-
if marker.split(":")[0] not in existing_marker_names
|
|
732
|
-
]
|
|
680
|
+
if self._should_skip_merge(target_file, merged_config, results):
|
|
681
|
+
return
|
|
733
682
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
self.console.print(
|
|
737
|
-
f"[green]➕[/green] Added pytest markers: {len(new_markers)}",
|
|
683
|
+
self._write_and_finalize_config(
|
|
684
|
+
merged_config, target_file, source_config, results
|
|
738
685
|
)
|
|
739
686
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
target_config: dict[str, t.Any],
|
|
743
|
-
) -> None:
|
|
744
|
-
"""Remove fixed coverage requirements in favor of ratchet system."""
|
|
745
|
-
import re
|
|
746
|
-
|
|
747
|
-
target_coverage = (
|
|
748
|
-
target_config.get("tool", {}).get("pytest", {}).get("ini_options", {})
|
|
749
|
-
)
|
|
687
|
+
except Exception as e:
|
|
688
|
+
self._handle_file_processing_error(".pre-commit-config.yaml", e, results)
|
|
750
689
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
# Remove --cov-fail-under=N pattern
|
|
756
|
-
addopts = re.sub(r"--cov-fail-under=\d+\.?\d*\s*", "", addopts).strip()
|
|
757
|
-
# Clean up extra spaces
|
|
758
|
-
addopts = " ".join(addopts.split())
|
|
759
|
-
|
|
760
|
-
if original_addopts != addopts:
|
|
761
|
-
target_coverage["addopts"] = addopts
|
|
762
|
-
self.console.print(
|
|
763
|
-
"[green]🔄[/green] Removed fixed coverage requirement (using ratchet system)",
|
|
764
|
-
)
|
|
690
|
+
def _load_source_config(self, source_file: Path) -> dict[str, t.Any] | None:
|
|
691
|
+
"""Load and validate source configuration file."""
|
|
692
|
+
with source_file.open() as f:
|
|
693
|
+
source_config = yaml.safe_load(f) or {}
|
|
765
694
|
|
|
766
|
-
#
|
|
767
|
-
|
|
768
|
-
target_config.get("tool", {}).get("coverage", {}).get("report", {})
|
|
769
|
-
)
|
|
770
|
-
if "fail_under" in coverage_report:
|
|
771
|
-
original_fail_under = coverage_report["fail_under"]
|
|
772
|
-
coverage_report["fail_under"] = 0
|
|
695
|
+
# Ensure source_config is a dict
|
|
696
|
+
if not isinstance(source_config, dict):
|
|
773
697
|
self.console.print(
|
|
774
|
-
|
|
698
|
+
"[yellow]⚠️[/yellow] Source .pre-commit-config.yaml is not a dictionary, skipping merge"
|
|
775
699
|
)
|
|
700
|
+
return None
|
|
776
701
|
|
|
777
|
-
|
|
778
|
-
"""Extract coverage requirement from pytest addopts."""
|
|
779
|
-
import re
|
|
702
|
+
return source_config
|
|
780
703
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
704
|
+
def _perform_config_merge(
|
|
705
|
+
self, source_config: dict[str, t.Any], target_file: Path, project_name: str
|
|
706
|
+
) -> dict[str, t.Any]:
|
|
707
|
+
"""Perform the configuration merge using ConfigMergeService."""
|
|
708
|
+
return self.config_merge_service.smart_merge_pre_commit_config(
|
|
709
|
+
source_config, target_file, project_name
|
|
710
|
+
)
|
|
785
711
|
|
|
786
|
-
def
|
|
712
|
+
def _should_skip_merge(
|
|
787
713
|
self,
|
|
788
|
-
source_file: Path,
|
|
789
714
|
target_file: Path,
|
|
790
|
-
|
|
791
|
-
force: bool,
|
|
715
|
+
merged_config: dict[str, t.Any],
|
|
792
716
|
results: dict[str, t.Any],
|
|
793
|
-
) ->
|
|
794
|
-
"""
|
|
795
|
-
# Load source config
|
|
796
|
-
with source_file.open() as f:
|
|
797
|
-
source_config = yaml.safe_load(f)
|
|
798
|
-
|
|
717
|
+
) -> bool:
|
|
718
|
+
"""Check if merge should be skipped due to no changes."""
|
|
799
719
|
if not target_file.exists():
|
|
800
|
-
|
|
801
|
-
content = self._read_and_process_content(
|
|
802
|
-
source_file,
|
|
803
|
-
True, # should_replace
|
|
804
|
-
project_name,
|
|
805
|
-
)
|
|
806
|
-
# Clean trailing whitespace and ensure single trailing newline
|
|
807
|
-
from crackerjack.services.filesystem import FileSystemService
|
|
808
|
-
|
|
809
|
-
content = FileSystemService.clean_trailing_whitespace_and_newlines(content)
|
|
810
|
-
self._write_file_and_track(
|
|
811
|
-
target_file,
|
|
812
|
-
content,
|
|
813
|
-
".pre-commit-config.yaml",
|
|
814
|
-
results,
|
|
815
|
-
)
|
|
816
|
-
return
|
|
720
|
+
return False
|
|
817
721
|
|
|
818
|
-
# Load existing config
|
|
819
722
|
with target_file.open() as f:
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
# Ensure configs are dictionaries
|
|
823
|
-
if not isinstance(source_config, dict):
|
|
824
|
-
source_config = {}
|
|
825
|
-
if not isinstance(target_config, dict):
|
|
826
|
-
target_config = {}
|
|
827
|
-
|
|
828
|
-
# Merge hooks without duplication
|
|
829
|
-
source_repos = source_config.get("repos", [])
|
|
830
|
-
target_repos = target_config.get("repos", [])
|
|
831
|
-
|
|
832
|
-
# Track existing repo URLs
|
|
833
|
-
existing_repo_urls = {repo.get("repo", "") for repo in target_repos}
|
|
834
|
-
|
|
835
|
-
# Add new repos that don't already exist
|
|
836
|
-
new_repos = [
|
|
837
|
-
repo
|
|
838
|
-
for repo in source_repos
|
|
839
|
-
if repo.get("repo", "") not in existing_repo_urls
|
|
840
|
-
]
|
|
723
|
+
old_config = yaml.safe_load(f) or {}
|
|
841
724
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
725
|
+
# Ensure old_config is a dict
|
|
726
|
+
if not isinstance(old_config, dict):
|
|
727
|
+
old_config = {}
|
|
845
728
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
target_config,
|
|
849
|
-
default_flow_style=False,
|
|
850
|
-
sort_keys=False,
|
|
851
|
-
width=float("inf"),
|
|
852
|
-
)
|
|
853
|
-
content = (
|
|
854
|
-
yaml_content.decode()
|
|
855
|
-
if isinstance(yaml_content, bytes)
|
|
856
|
-
else yaml_content
|
|
857
|
-
)
|
|
729
|
+
old_repo_count = len(old_config.get("repos", []))
|
|
730
|
+
new_repo_count = len(merged_config.get("repos", []))
|
|
858
731
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
# Clean trailing whitespace and ensure single trailing newline
|
|
864
|
-
from crackerjack.services.filesystem import FileSystemService
|
|
732
|
+
if new_repo_count == old_repo_count:
|
|
733
|
+
self._skip_existing_file(".pre-commit-config.yaml (no new repos)", results)
|
|
734
|
+
return True
|
|
865
735
|
|
|
866
|
-
|
|
736
|
+
return False
|
|
867
737
|
|
|
868
|
-
|
|
869
|
-
|
|
738
|
+
def _write_and_finalize_config(
|
|
739
|
+
self,
|
|
740
|
+
merged_config: dict[str, t.Any],
|
|
741
|
+
target_file: Path,
|
|
742
|
+
source_config: dict[str, t.Any],
|
|
743
|
+
results: dict[str, t.Any],
|
|
744
|
+
) -> None:
|
|
745
|
+
"""Write merged config and finalize the process."""
|
|
746
|
+
# Write the merged configuration
|
|
747
|
+
self.config_merge_service.write_pre_commit_config(merged_config, target_file)
|
|
870
748
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
749
|
+
t.cast("list[str]", results["files_copied"]).append(
|
|
750
|
+
".pre-commit-config.yaml (merged)"
|
|
751
|
+
)
|
|
874
752
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
except Exception as e:
|
|
878
|
-
self.console.print(
|
|
879
|
-
f"[yellow]⚠️[/yellow] Could not git add .pre-commit-config.yaml: {e}",
|
|
880
|
-
)
|
|
753
|
+
self._git_add_config_file(target_file)
|
|
754
|
+
self._display_merge_success(source_config)
|
|
881
755
|
|
|
756
|
+
def _git_add_config_file(self, target_file: Path) -> None:
|
|
757
|
+
"""Add config file to git with error handling."""
|
|
758
|
+
try:
|
|
759
|
+
self.git_service.add_files([str(target_file)])
|
|
760
|
+
except Exception as e:
|
|
882
761
|
self.console.print(
|
|
883
|
-
f"[
|
|
762
|
+
f"[yellow]⚠️[/ yellow] Could not git add .pre-commit-config.yaml: {e}"
|
|
884
763
|
)
|
|
885
|
-
else:
|
|
886
|
-
self._skip_existing_file(".pre-commit-config.yaml (no new repos)", results)
|
|
887
764
|
|
|
888
|
-
def
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
# Deep copy to avoid modifying original
|
|
896
|
-
import copy
|
|
897
|
-
|
|
898
|
-
result = copy.deepcopy(tool_config)
|
|
899
|
-
|
|
900
|
-
# Recursively replace in the configuration
|
|
901
|
-
return self._replace_project_name_in_config_value(result, project_name)
|
|
902
|
-
|
|
903
|
-
def _replace_project_name_in_config_value(
|
|
904
|
-
self, value: t.Any, project_name: str
|
|
905
|
-
) -> t.Any:
|
|
906
|
-
"""Replace project name in a configuration value (recursive)."""
|
|
907
|
-
if project_name == "crackerjack":
|
|
908
|
-
return value # No replacement needed
|
|
909
|
-
|
|
910
|
-
if isinstance(value, str):
|
|
911
|
-
return value.replace("crackerjack", project_name)
|
|
912
|
-
elif isinstance(value, list):
|
|
913
|
-
return [
|
|
914
|
-
self._replace_project_name_in_config_value(item, project_name)
|
|
915
|
-
for item in value
|
|
916
|
-
]
|
|
917
|
-
elif isinstance(value, dict):
|
|
918
|
-
return {
|
|
919
|
-
key: self._replace_project_name_in_config_value(val, project_name)
|
|
920
|
-
for key, val in value.items()
|
|
921
|
-
}
|
|
922
|
-
return value # Numbers, booleans, etc. - no replacement needed
|
|
765
|
+
def _display_merge_success(self, source_config: dict[str, t.Any]) -> None:
|
|
766
|
+
"""Display success message with repo count."""
|
|
767
|
+
source_repo_count = len(source_config.get("repos", []))
|
|
768
|
+
self.console.print(
|
|
769
|
+
f"[green]✅[/ green] Merged .pre-commit-config.yaml ({source_repo_count} repos processed)"
|
|
770
|
+
)
|