crackerjack 0.30.3__py3-none-any.whl → 0.31.4__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 +225 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +169 -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 +652 -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 +401 -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 +561 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +640 -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 +411 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +435 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +144 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +615 -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 +370 -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 +141 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +360 -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/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 +347 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +347 -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 +395 -0
- crackerjack/services/git.py +165 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +847 -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.4.dist-info/METADATA +742 -0
- crackerjack-0.31.4.dist-info/RECORD +148 -0
- crackerjack-0.31.4.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.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import typing as t
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import tomli
|
|
7
|
+
import tomli_w
|
|
8
|
+
import yaml
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from .filesystem import FileSystemService
|
|
12
|
+
from .git import GitService
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InitializationService:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
console: Console,
|
|
19
|
+
filesystem: FileSystemService,
|
|
20
|
+
git_service: GitService,
|
|
21
|
+
pkg_path: Path,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.console = console
|
|
24
|
+
self.filesystem = filesystem
|
|
25
|
+
self.git_service = git_service
|
|
26
|
+
self.pkg_path = pkg_path
|
|
27
|
+
|
|
28
|
+
def initialize_project(
|
|
29
|
+
self,
|
|
30
|
+
target_path: Path | None = None,
|
|
31
|
+
force: bool = False,
|
|
32
|
+
) -> dict[str, t.Any]:
|
|
33
|
+
if target_path is None:
|
|
34
|
+
target_path = Path.cwd()
|
|
35
|
+
|
|
36
|
+
results = self._create_results_dict(target_path)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
config_files = self._get_config_files()
|
|
40
|
+
project_name = target_path.name
|
|
41
|
+
|
|
42
|
+
for file_name, merge_strategy in config_files.items():
|
|
43
|
+
self._process_config_file(
|
|
44
|
+
file_name,
|
|
45
|
+
merge_strategy,
|
|
46
|
+
project_name,
|
|
47
|
+
target_path,
|
|
48
|
+
force,
|
|
49
|
+
results,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self._print_summary(results)
|
|
53
|
+
|
|
54
|
+
except Exception as e:
|
|
55
|
+
self._handle_initialization_error(results, e)
|
|
56
|
+
|
|
57
|
+
return results
|
|
58
|
+
|
|
59
|
+
def _create_results_dict(self, target_path: Path) -> dict[str, t.Any]:
|
|
60
|
+
return {
|
|
61
|
+
"target_path": str(target_path),
|
|
62
|
+
"files_copied": [],
|
|
63
|
+
"files_skipped": [],
|
|
64
|
+
"errors": [],
|
|
65
|
+
"success": True,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def _get_config_files(self) -> dict[str, str]:
|
|
69
|
+
"""Get config files with their merge strategies."""
|
|
70
|
+
return {
|
|
71
|
+
".pre-commit-config.yaml": "smart_merge",
|
|
72
|
+
"pyproject.toml": "smart_merge",
|
|
73
|
+
"CLAUDE.md": "smart_append",
|
|
74
|
+
"RULES.md": "replace_if_missing",
|
|
75
|
+
"mcp.json": "special", # Special handling: mcp.json -> .mcp.json with merging
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def _process_config_file(
|
|
79
|
+
self,
|
|
80
|
+
file_name: str,
|
|
81
|
+
merge_strategy: str,
|
|
82
|
+
project_name: str,
|
|
83
|
+
target_path: Path,
|
|
84
|
+
force: bool,
|
|
85
|
+
results: dict[str, t.Any],
|
|
86
|
+
) -> None:
|
|
87
|
+
# Special handling for mcp.json -> .mcp.json
|
|
88
|
+
if file_name == "mcp.json":
|
|
89
|
+
self._process_mcp_config(target_path, force, results)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
source_file = self.pkg_path.parent / file_name
|
|
93
|
+
target_file = target_path / file_name
|
|
94
|
+
|
|
95
|
+
if not source_file.exists():
|
|
96
|
+
self._handle_missing_source_file(file_name, results)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# Handle different merge strategies
|
|
101
|
+
if merge_strategy == "smart_merge":
|
|
102
|
+
self._smart_merge_config(
|
|
103
|
+
source_file,
|
|
104
|
+
target_file,
|
|
105
|
+
file_name,
|
|
106
|
+
project_name,
|
|
107
|
+
force,
|
|
108
|
+
results,
|
|
109
|
+
)
|
|
110
|
+
elif merge_strategy == "smart_append":
|
|
111
|
+
self._smart_append_config(
|
|
112
|
+
source_file,
|
|
113
|
+
target_file,
|
|
114
|
+
file_name,
|
|
115
|
+
project_name,
|
|
116
|
+
force,
|
|
117
|
+
results,
|
|
118
|
+
)
|
|
119
|
+
elif merge_strategy == "replace_if_missing":
|
|
120
|
+
if not target_file.exists() or force:
|
|
121
|
+
content = self._read_and_process_content(
|
|
122
|
+
source_file,
|
|
123
|
+
True,
|
|
124
|
+
project_name,
|
|
125
|
+
)
|
|
126
|
+
self._write_file_and_track(target_file, content, file_name, results)
|
|
127
|
+
else:
|
|
128
|
+
self._skip_existing_file(file_name, results)
|
|
129
|
+
else:
|
|
130
|
+
# Fallback to old behavior
|
|
131
|
+
if not self._should_copy_file(target_file, force, file_name, results):
|
|
132
|
+
return
|
|
133
|
+
content = self._read_and_process_content(
|
|
134
|
+
source_file,
|
|
135
|
+
True,
|
|
136
|
+
project_name,
|
|
137
|
+
)
|
|
138
|
+
self._write_file_and_track(target_file, content, file_name, results)
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
self._handle_file_processing_error(file_name, e, results)
|
|
142
|
+
|
|
143
|
+
def _should_copy_file(
|
|
144
|
+
self,
|
|
145
|
+
target_file: Path,
|
|
146
|
+
force: bool,
|
|
147
|
+
file_name: str,
|
|
148
|
+
results: dict[str, t.Any],
|
|
149
|
+
) -> bool:
|
|
150
|
+
if target_file.exists() and not force:
|
|
151
|
+
t.cast("list[str]", results["files_skipped"]).append(file_name)
|
|
152
|
+
self.console.print(
|
|
153
|
+
f"[yellow]⚠️[/yellow] Skipped {file_name} (already exists)",
|
|
154
|
+
)
|
|
155
|
+
return False
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
def _read_and_process_content(
|
|
159
|
+
self,
|
|
160
|
+
source_file: Path,
|
|
161
|
+
should_replace: bool,
|
|
162
|
+
project_name: str,
|
|
163
|
+
) -> str:
|
|
164
|
+
content = source_file.read_text()
|
|
165
|
+
|
|
166
|
+
if should_replace and project_name != "crackerjack":
|
|
167
|
+
content = content.replace("crackerjack", project_name)
|
|
168
|
+
|
|
169
|
+
return content
|
|
170
|
+
|
|
171
|
+
def _write_file_and_track(
|
|
172
|
+
self,
|
|
173
|
+
target_file: Path,
|
|
174
|
+
content: str,
|
|
175
|
+
file_name: str,
|
|
176
|
+
results: dict[str, t.Any],
|
|
177
|
+
) -> None:
|
|
178
|
+
target_file.write_text(content)
|
|
179
|
+
t.cast("list[str]", results["files_copied"]).append(file_name)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
self.git_service.add_files([str(target_file)])
|
|
183
|
+
except Exception as e:
|
|
184
|
+
self.console.print(f"[yellow]⚠️[/yellow] Could not git add {file_name}: {e}")
|
|
185
|
+
|
|
186
|
+
self.console.print(f"[green]✅[/green] Copied {file_name}")
|
|
187
|
+
|
|
188
|
+
def _skip_existing_file(self, file_name: str, results: dict[str, t.Any]) -> None:
|
|
189
|
+
t.cast("list[str]", results["files_skipped"]).append(file_name)
|
|
190
|
+
self.console.print(f"[yellow]⚠️[/yellow] Skipped {file_name} (already exists)")
|
|
191
|
+
|
|
192
|
+
def _handle_missing_source_file(
|
|
193
|
+
self,
|
|
194
|
+
file_name: str,
|
|
195
|
+
results: dict[str, t.Any],
|
|
196
|
+
) -> None:
|
|
197
|
+
error_msg = f"Source file not found: {file_name}"
|
|
198
|
+
t.cast("list[str]", results["errors"]).append(error_msg)
|
|
199
|
+
self.console.print(f"[yellow]⚠️[/yellow] {error_msg}")
|
|
200
|
+
|
|
201
|
+
def _handle_file_processing_error(
|
|
202
|
+
self,
|
|
203
|
+
file_name: str,
|
|
204
|
+
error: Exception,
|
|
205
|
+
results: dict[str, t.Any],
|
|
206
|
+
) -> None:
|
|
207
|
+
error_msg = f"Failed to copy {file_name}: {error}"
|
|
208
|
+
t.cast("list[str]", results["errors"]).append(error_msg)
|
|
209
|
+
results["success"] = False
|
|
210
|
+
self.console.print(f"[red]❌[/red] {error_msg}")
|
|
211
|
+
|
|
212
|
+
def _print_summary(self, results: dict[str, t.Any]) -> None:
|
|
213
|
+
if results["success"]:
|
|
214
|
+
self.console.print(
|
|
215
|
+
f"[green]🎉 Project initialized successfully ! [/green] "
|
|
216
|
+
f"Copied {len(t.cast('list[str]', results['files_copied']))} files",
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
self.console.print(
|
|
220
|
+
"[red]❌ Project initialization completed with errors[/red]",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def _handle_initialization_error(
|
|
224
|
+
self,
|
|
225
|
+
results: dict[str, t.Any],
|
|
226
|
+
error: Exception,
|
|
227
|
+
) -> None:
|
|
228
|
+
results["success"] = False
|
|
229
|
+
t.cast("list[str]", results["errors"]).append(f"Initialization failed: {error}")
|
|
230
|
+
self.console.print(f"[red]❌[/red] Initialization failed: {error}")
|
|
231
|
+
|
|
232
|
+
def check_uv_installed(self) -> bool:
|
|
233
|
+
try:
|
|
234
|
+
result = subprocess.run(
|
|
235
|
+
["uv", "--version"],
|
|
236
|
+
capture_output=True,
|
|
237
|
+
text=True,
|
|
238
|
+
timeout=10,
|
|
239
|
+
check=False,
|
|
240
|
+
)
|
|
241
|
+
return result.returncode == 0
|
|
242
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
def _process_mcp_config(
|
|
246
|
+
self,
|
|
247
|
+
target_path: Path,
|
|
248
|
+
force: bool,
|
|
249
|
+
results: dict[str, t.Any],
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Handle special processing for mcp.json -> .mcp.json with merging."""
|
|
252
|
+
# Source: mcp.json in crackerjack package (contains servers to add to projects)
|
|
253
|
+
source_file = self.pkg_path / "mcp.json"
|
|
254
|
+
# Target: .mcp.json in target project
|
|
255
|
+
target_file = target_path / ".mcp.json"
|
|
256
|
+
|
|
257
|
+
if not source_file.exists():
|
|
258
|
+
self._handle_missing_source_file("mcp.json", results)
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
# Load the crackerjack MCP servers to add
|
|
263
|
+
with source_file.open() as f:
|
|
264
|
+
source_config = json.load(f)
|
|
265
|
+
|
|
266
|
+
if not isinstance(source_config.get("mcpServers"), dict):
|
|
267
|
+
self._handle_file_processing_error(
|
|
268
|
+
"mcp.json",
|
|
269
|
+
ValueError("Invalid mcp.json format: missing mcpServers"),
|
|
270
|
+
results,
|
|
271
|
+
)
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
crackerjack_servers = source_config["mcpServers"]
|
|
275
|
+
|
|
276
|
+
# If target .mcp.json doesn't exist, create it with crackerjack servers
|
|
277
|
+
if not target_file.exists():
|
|
278
|
+
target_config = {"mcpServers": crackerjack_servers}
|
|
279
|
+
self._write_mcp_config_and_track(target_file, target_config, results)
|
|
280
|
+
self.console.print(
|
|
281
|
+
"[green]✅[/green] Created .mcp.json with crackerjack MCP servers",
|
|
282
|
+
)
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
# If target exists and force=False, skip unless we're merging
|
|
286
|
+
if target_file.exists() and not force:
|
|
287
|
+
# Always merge crackerjack servers into existing config
|
|
288
|
+
self._merge_mcp_config(target_file, crackerjack_servers, results)
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
# If force=True, replace entirely with crackerjack servers
|
|
292
|
+
target_config = {"mcpServers": crackerjack_servers}
|
|
293
|
+
self._write_mcp_config_and_track(target_file, target_config, results)
|
|
294
|
+
self.console.print(
|
|
295
|
+
"[green]✅[/green] Updated .mcp.json with crackerjack MCP servers",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
except Exception as e:
|
|
299
|
+
self._handle_file_processing_error(".mcp.json", e, results)
|
|
300
|
+
|
|
301
|
+
def _merge_mcp_config(
|
|
302
|
+
self,
|
|
303
|
+
target_file: Path,
|
|
304
|
+
crackerjack_servers: dict[str, t.Any],
|
|
305
|
+
results: dict[str, t.Any],
|
|
306
|
+
) -> None:
|
|
307
|
+
"""Merge crackerjack servers into existing .mcp.json."""
|
|
308
|
+
try:
|
|
309
|
+
# Load existing config
|
|
310
|
+
with target_file.open() as f:
|
|
311
|
+
existing_config = json.load(f)
|
|
312
|
+
|
|
313
|
+
if not isinstance(existing_config.get("mcpServers"), dict):
|
|
314
|
+
existing_config["mcpServers"] = {}
|
|
315
|
+
|
|
316
|
+
# Merge crackerjack servers (they override existing ones with same name)
|
|
317
|
+
existing_servers = existing_config["mcpServers"]
|
|
318
|
+
updated_servers = {}
|
|
319
|
+
|
|
320
|
+
for name, config in crackerjack_servers.items():
|
|
321
|
+
if name in existing_servers:
|
|
322
|
+
self.console.print(
|
|
323
|
+
f"[yellow]🔄[/yellow] Updating existing MCP server: {name}",
|
|
324
|
+
)
|
|
325
|
+
else:
|
|
326
|
+
self.console.print(
|
|
327
|
+
f"[green]➕[/green] Adding new MCP server: {name}",
|
|
328
|
+
)
|
|
329
|
+
updated_servers[name] = config
|
|
330
|
+
|
|
331
|
+
# Merge into existing config
|
|
332
|
+
existing_servers.update(updated_servers)
|
|
333
|
+
|
|
334
|
+
# Write the merged config
|
|
335
|
+
self._write_mcp_config_and_track(target_file, existing_config, results)
|
|
336
|
+
|
|
337
|
+
t.cast("list[str]", results["files_copied"]).append(".mcp.json (merged)")
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
self._handle_file_processing_error(".mcp.json (merge)", e, results)
|
|
341
|
+
|
|
342
|
+
def _write_mcp_config_and_track(
|
|
343
|
+
self,
|
|
344
|
+
target_file: Path,
|
|
345
|
+
config: dict[str, t.Any],
|
|
346
|
+
results: dict[str, t.Any],
|
|
347
|
+
) -> None:
|
|
348
|
+
"""Write MCP config file and track in results."""
|
|
349
|
+
with target_file.open("w") as f:
|
|
350
|
+
json.dump(config, f, indent=2)
|
|
351
|
+
|
|
352
|
+
t.cast("list[str]", results["files_copied"]).append(".mcp.json")
|
|
353
|
+
|
|
354
|
+
# Try to git add the file
|
|
355
|
+
try:
|
|
356
|
+
self.git_service.add_files([str(target_file)])
|
|
357
|
+
except Exception as e:
|
|
358
|
+
self.console.print(f"[yellow]⚠️[/yellow] Could not git add .mcp.json: {e}")
|
|
359
|
+
|
|
360
|
+
def validate_project_structure(self) -> bool:
|
|
361
|
+
required_indicators = [
|
|
362
|
+
self.pkg_path / "pyproject.toml",
|
|
363
|
+
self.pkg_path / "setup.py",
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
return any(path.exists() for path in required_indicators)
|
|
367
|
+
|
|
368
|
+
def _generate_project_claude_content(self, project_name: str) -> str:
|
|
369
|
+
"""Generate customized CLAUDE.md content for external projects."""
|
|
370
|
+
return f"""
|
|
371
|
+
# Crackerjack Integration for {project_name}
|
|
372
|
+
|
|
373
|
+
This project uses crackerjack for Python project management and quality assurance.
|
|
374
|
+
|
|
375
|
+
## Recommended Claude Code Agents
|
|
376
|
+
|
|
377
|
+
For optimal development experience with this crackerjack-enabled project, use these specialized agents:
|
|
378
|
+
|
|
379
|
+
### **Primary Agents (Use for all Python development)**
|
|
380
|
+
|
|
381
|
+
- **🏗️ 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
|
+
|
|
383
|
+
- **🐍 python-pro**: Modern Python development with type hints, async/await patterns, and clean architecture
|
|
384
|
+
|
|
385
|
+
- **🧪 pytest-hypothesis-specialist**: Advanced testing patterns, property-based testing, and test optimization
|
|
386
|
+
|
|
387
|
+
### **Task-Specific Agents**
|
|
388
|
+
|
|
389
|
+
- **🧪 crackerjack-test-specialist**: Advanced testing specialist for complex testing scenarios and coverage optimization
|
|
390
|
+
- **🏗️ backend-architect**: System design, API architecture, and service integration patterns
|
|
391
|
+
- **🔒 security-auditor**: Security analysis, vulnerability detection, and secure coding practices
|
|
392
|
+
|
|
393
|
+
### **Agent Usage Patterns**
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
# Start development with crackerjack-compliant architecture
|
|
397
|
+
Task tool with subagent_type="crackerjack-architect" for feature planning
|
|
398
|
+
|
|
399
|
+
# Implement with modern Python best practices
|
|
400
|
+
Task tool with subagent_type="python-pro" for code implementation
|
|
401
|
+
|
|
402
|
+
# Add comprehensive testing
|
|
403
|
+
Task tool with subagent_type="pytest-hypothesis-specialist" for test development
|
|
404
|
+
|
|
405
|
+
# Security review before completion
|
|
406
|
+
Task tool with subagent_type="security-auditor" for security analysis
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**💡 Pro Tip**: The crackerjack-architect agent automatically ensures code follows crackerjack patterns from the start, eliminating the need for retrofitting and quality fixes.
|
|
410
|
+
|
|
411
|
+
## Crackerjack Quality Standards
|
|
412
|
+
|
|
413
|
+
This project follows crackerjack's clean code philosophy:
|
|
414
|
+
|
|
415
|
+
### **Core Principles**
|
|
416
|
+
- **EVERY LINE OF CODE IS A LIABILITY**: The best code is no code
|
|
417
|
+
- **DRY (Don't Repeat Yourself)**: If you write it twice, you're doing it wrong
|
|
418
|
+
- **YAGNI (You Ain't Gonna Need It)**: Build only what's needed NOW
|
|
419
|
+
- **KISS (Keep It Simple, Stupid)**: Complexity is the enemy of maintainability
|
|
420
|
+
|
|
421
|
+
### **Quality Rules**
|
|
422
|
+
- **Cognitive complexity ≤15** per function (automatically enforced)
|
|
423
|
+
- **Coverage ratchet system**: Never decrease coverage, always improve toward 100%
|
|
424
|
+
- **Type annotations required**: All functions must have return type hints
|
|
425
|
+
- **Security patterns**: No hardcoded paths, proper temp file handling
|
|
426
|
+
- **Python 3.13+ modern patterns**: Use `|` unions, pathlib over os.path
|
|
427
|
+
|
|
428
|
+
## Development Workflow
|
|
429
|
+
|
|
430
|
+
### **Quality Commands**
|
|
431
|
+
```bash
|
|
432
|
+
# Quality checks (fast feedback during development)
|
|
433
|
+
python -m crackerjack
|
|
434
|
+
|
|
435
|
+
# With comprehensive testing
|
|
436
|
+
python -m crackerjack -t
|
|
437
|
+
|
|
438
|
+
# AI agent mode with autonomous fixing
|
|
439
|
+
python -m crackerjack --ai-agent -t
|
|
440
|
+
|
|
441
|
+
# Full release workflow
|
|
442
|
+
python -m crackerjack -a patch
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### **Recommended Workflow**
|
|
446
|
+
1. **Plan with crackerjack-architect**: Ensure proper architecture from the start
|
|
447
|
+
2. **Implement with python-pro**: Follow modern Python patterns
|
|
448
|
+
3. **Test comprehensively**: Use pytest-hypothesis-specialist for robust testing
|
|
449
|
+
4. **Run quality checks**: `python -m crackerjack -t` before committing
|
|
450
|
+
5. **Security review**: Use security-auditor for final validation
|
|
451
|
+
|
|
452
|
+
## Important Instructions
|
|
453
|
+
|
|
454
|
+
- **Use crackerjack-architect agent proactively** for all significant code changes
|
|
455
|
+
- **Never reduce test coverage** - the ratchet system only allows improvements
|
|
456
|
+
- **Follow crackerjack patterns** - the tools will enforce quality automatically
|
|
457
|
+
- **Leverage AI agent auto-fixing** - `python -m crackerjack --ai-agent -t` for autonomous quality fixes
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
*This project is enhanced by crackerjack's intelligent Python project management.*
|
|
461
|
+
""".strip()
|
|
462
|
+
|
|
463
|
+
def _smart_append_config(
|
|
464
|
+
self,
|
|
465
|
+
source_file: Path,
|
|
466
|
+
target_file: Path,
|
|
467
|
+
file_name: str,
|
|
468
|
+
project_name: str,
|
|
469
|
+
force: bool,
|
|
470
|
+
results: dict[str, t.Any],
|
|
471
|
+
) -> None:
|
|
472
|
+
"""Smart append for CLAUDE.md - append crackerjack content without overwriting."""
|
|
473
|
+
if file_name == "CLAUDE.md" and project_name != "crackerjack":
|
|
474
|
+
# For external projects, generate customized crackerjack guidance
|
|
475
|
+
source_content = self._generate_project_claude_content(project_name)
|
|
476
|
+
else:
|
|
477
|
+
source_content = self._read_and_process_content(
|
|
478
|
+
source_file, True, project_name
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
if not target_file.exists():
|
|
482
|
+
# No existing file, just copy
|
|
483
|
+
self._write_file_and_track(target_file, source_content, file_name, results)
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
existing_content = target_file.read_text()
|
|
487
|
+
|
|
488
|
+
# Check if crackerjack content already exists
|
|
489
|
+
crackerjack_start_marker = "<!-- CRACKERJACK INTEGRATION START -->"
|
|
490
|
+
crackerjack_end_marker = "<!-- CRACKERJACK INTEGRATION END -->"
|
|
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
|
|
506
|
+
|
|
507
|
+
# Append crackerjack content with clear markers
|
|
508
|
+
merged_content = (
|
|
509
|
+
existing_content.strip() + "\n\n" + crackerjack_start_marker + "\n"
|
|
510
|
+
)
|
|
511
|
+
merged_content += source_content.strip() + "\n"
|
|
512
|
+
merged_content += crackerjack_end_marker + "\n"
|
|
513
|
+
|
|
514
|
+
target_file.write_text(merged_content)
|
|
515
|
+
t.cast("list[str]", results["files_copied"]).append(f"{file_name} (appended)")
|
|
516
|
+
|
|
517
|
+
try:
|
|
518
|
+
self.git_service.add_files([str(target_file)])
|
|
519
|
+
except Exception as e:
|
|
520
|
+
self.console.print(f"[yellow]⚠️[/yellow] Could not git add {file_name}: {e}")
|
|
521
|
+
|
|
522
|
+
self.console.print(f"[green]✅[/green] Appended to {file_name}")
|
|
523
|
+
|
|
524
|
+
def _smart_merge_config(
|
|
525
|
+
self,
|
|
526
|
+
source_file: Path,
|
|
527
|
+
target_file: Path,
|
|
528
|
+
file_name: str,
|
|
529
|
+
project_name: str,
|
|
530
|
+
force: bool,
|
|
531
|
+
results: dict[str, t.Any],
|
|
532
|
+
) -> None:
|
|
533
|
+
"""Smart merge for configuration files."""
|
|
534
|
+
if file_name == "pyproject.toml":
|
|
535
|
+
self._smart_merge_pyproject(
|
|
536
|
+
source_file,
|
|
537
|
+
target_file,
|
|
538
|
+
project_name,
|
|
539
|
+
force,
|
|
540
|
+
results,
|
|
541
|
+
)
|
|
542
|
+
elif file_name == ".pre-commit-config.yaml":
|
|
543
|
+
self._smart_merge_pre_commit_config(
|
|
544
|
+
source_file,
|
|
545
|
+
target_file,
|
|
546
|
+
force,
|
|
547
|
+
results,
|
|
548
|
+
)
|
|
549
|
+
# Fallback to regular copy
|
|
550
|
+
elif not target_file.exists() or force:
|
|
551
|
+
content = self._read_and_process_content(
|
|
552
|
+
source_file,
|
|
553
|
+
True,
|
|
554
|
+
project_name,
|
|
555
|
+
)
|
|
556
|
+
self._write_file_and_track(target_file, content, file_name, results)
|
|
557
|
+
else:
|
|
558
|
+
self._skip_existing_file(file_name, results)
|
|
559
|
+
|
|
560
|
+
def _smart_merge_pyproject(
|
|
561
|
+
self,
|
|
562
|
+
source_file: Path,
|
|
563
|
+
target_file: Path,
|
|
564
|
+
project_name: str,
|
|
565
|
+
force: bool,
|
|
566
|
+
results: dict[str, t.Any],
|
|
567
|
+
) -> None:
|
|
568
|
+
"""Intelligently merge pyproject.toml configurations."""
|
|
569
|
+
# Load source (crackerjack) config
|
|
570
|
+
with source_file.open("rb") as f:
|
|
571
|
+
source_config = tomli.load(f)
|
|
572
|
+
|
|
573
|
+
if not target_file.exists():
|
|
574
|
+
# No existing file, just copy and replace project name
|
|
575
|
+
content = self._read_and_process_content(source_file, True, project_name)
|
|
576
|
+
self._write_file_and_track(target_file, content, "pyproject.toml", results)
|
|
577
|
+
return
|
|
578
|
+
|
|
579
|
+
# Load existing config
|
|
580
|
+
with target_file.open("rb") as f:
|
|
581
|
+
target_config = tomli.load(f)
|
|
582
|
+
|
|
583
|
+
# 1. Ensure crackerjack is in dev dependencies
|
|
584
|
+
self._ensure_crackerjack_dev_dependency(target_config, source_config)
|
|
585
|
+
|
|
586
|
+
# 2. Merge tool configurations
|
|
587
|
+
self._merge_tool_configurations(target_config, source_config)
|
|
588
|
+
|
|
589
|
+
# 3. Remove any fixed coverage requirements (use ratchet system instead)
|
|
590
|
+
self._remove_fixed_coverage_requirements(target_config)
|
|
591
|
+
|
|
592
|
+
# Write merged config
|
|
593
|
+
with target_file.open("wb") as f:
|
|
594
|
+
tomli_w.dump(target_config, f)
|
|
595
|
+
|
|
596
|
+
t.cast("list[str]", results["files_copied"]).append("pyproject.toml (merged)")
|
|
597
|
+
|
|
598
|
+
try:
|
|
599
|
+
self.git_service.add_files([str(target_file)])
|
|
600
|
+
except Exception as e:
|
|
601
|
+
self.console.print(
|
|
602
|
+
f"[yellow]⚠️[/yellow] Could not git add pyproject.toml: {e}",
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
self.console.print("[green]✅[/green] Smart merged pyproject.toml")
|
|
606
|
+
|
|
607
|
+
def _ensure_crackerjack_dev_dependency(
|
|
608
|
+
self,
|
|
609
|
+
target_config: dict[str, t.Any],
|
|
610
|
+
source_config: dict[str, t.Any],
|
|
611
|
+
) -> None:
|
|
612
|
+
"""Ensure crackerjack is in dev dependencies."""
|
|
613
|
+
# Check different dependency group structures
|
|
614
|
+
if "dependency-groups" not in target_config:
|
|
615
|
+
target_config["dependency-groups"] = {}
|
|
616
|
+
|
|
617
|
+
if "dev" not in target_config["dependency-groups"]:
|
|
618
|
+
target_config["dependency-groups"]["dev"] = []
|
|
619
|
+
|
|
620
|
+
dev_deps = target_config["dependency-groups"]["dev"]
|
|
621
|
+
if "crackerjack" not in str(dev_deps):
|
|
622
|
+
# Add crackerjack to dev dependencies
|
|
623
|
+
dev_deps.append("crackerjack")
|
|
624
|
+
|
|
625
|
+
def _merge_tool_configurations(
|
|
626
|
+
self,
|
|
627
|
+
target_config: dict[str, t.Any],
|
|
628
|
+
source_config: dict[str, t.Any],
|
|
629
|
+
) -> None:
|
|
630
|
+
"""Merge tool configurations, preserving existing settings."""
|
|
631
|
+
source_tools = source_config.get("tool", {})
|
|
632
|
+
|
|
633
|
+
if "tool" not in target_config:
|
|
634
|
+
target_config["tool"] = {}
|
|
635
|
+
|
|
636
|
+
target_tools = target_config["tool"]
|
|
637
|
+
|
|
638
|
+
# Tools to merge (add if missing, preserve if existing)
|
|
639
|
+
tools_to_merge = [
|
|
640
|
+
"ruff",
|
|
641
|
+
"pyright",
|
|
642
|
+
"bandit",
|
|
643
|
+
"vulture",
|
|
644
|
+
"refurb",
|
|
645
|
+
"complexipy",
|
|
646
|
+
"codespell",
|
|
647
|
+
"creosote",
|
|
648
|
+
]
|
|
649
|
+
|
|
650
|
+
for tool_name in tools_to_merge:
|
|
651
|
+
if tool_name in source_tools:
|
|
652
|
+
if tool_name not in target_tools:
|
|
653
|
+
# Tool missing, add it
|
|
654
|
+
target_tools[tool_name] = source_tools[tool_name]
|
|
655
|
+
self.console.print(
|
|
656
|
+
f"[green]➕[/green] Added [tool.{tool_name}] configuration",
|
|
657
|
+
)
|
|
658
|
+
else:
|
|
659
|
+
# Tool exists, merge settings
|
|
660
|
+
self._merge_tool_settings(
|
|
661
|
+
target_tools[tool_name],
|
|
662
|
+
source_tools[tool_name],
|
|
663
|
+
tool_name,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# Special handling for pytest.ini_options markers
|
|
667
|
+
self._merge_pytest_markers(target_tools, source_tools)
|
|
668
|
+
|
|
669
|
+
def _merge_tool_settings(
|
|
670
|
+
self,
|
|
671
|
+
target_tool: dict[str, t.Any],
|
|
672
|
+
source_tool: dict[str, t.Any],
|
|
673
|
+
tool_name: str,
|
|
674
|
+
) -> None:
|
|
675
|
+
"""Merge individual tool settings."""
|
|
676
|
+
updated_keys = []
|
|
677
|
+
|
|
678
|
+
for key, value in source_tool.items():
|
|
679
|
+
if key not in target_tool:
|
|
680
|
+
target_tool[key] = value
|
|
681
|
+
updated_keys.append(key)
|
|
682
|
+
|
|
683
|
+
if updated_keys:
|
|
684
|
+
self.console.print(
|
|
685
|
+
f"[yellow]🔄[/yellow] Updated [tool.{tool_name}] with: {', '.join(updated_keys)}",
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
def _merge_pytest_markers(
|
|
689
|
+
self,
|
|
690
|
+
target_tools: dict[str, t.Any],
|
|
691
|
+
source_tools: dict[str, t.Any],
|
|
692
|
+
) -> None:
|
|
693
|
+
"""Merge pytest markers without duplication."""
|
|
694
|
+
if "pytest" not in source_tools or "pytest" not in target_tools:
|
|
695
|
+
return
|
|
696
|
+
|
|
697
|
+
source_pytest = source_tools["pytest"]
|
|
698
|
+
target_pytest = target_tools["pytest"]
|
|
699
|
+
|
|
700
|
+
if "ini_options" not in source_pytest or "ini_options" not in target_pytest:
|
|
701
|
+
return
|
|
702
|
+
|
|
703
|
+
source_markers = source_pytest["ini_options"].get("markers", [])
|
|
704
|
+
target_markers = target_pytest["ini_options"].get("markers", [])
|
|
705
|
+
|
|
706
|
+
# Extract marker names to avoid duplication
|
|
707
|
+
existing_marker_names = {marker.split(":")[0] for marker in target_markers}
|
|
708
|
+
new_markers = [
|
|
709
|
+
marker
|
|
710
|
+
for marker in source_markers
|
|
711
|
+
if marker.split(":")[0] not in existing_marker_names
|
|
712
|
+
]
|
|
713
|
+
|
|
714
|
+
if new_markers:
|
|
715
|
+
target_markers.extend(new_markers)
|
|
716
|
+
self.console.print(
|
|
717
|
+
f"[green]➕[/green] Added pytest markers: {len(new_markers)}",
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
def _remove_fixed_coverage_requirements(
|
|
721
|
+
self,
|
|
722
|
+
target_config: dict[str, t.Any],
|
|
723
|
+
) -> None:
|
|
724
|
+
"""Remove fixed coverage requirements in favor of ratchet system."""
|
|
725
|
+
import re
|
|
726
|
+
|
|
727
|
+
target_coverage = (
|
|
728
|
+
target_config.get("tool", {}).get("pytest", {}).get("ini_options", {})
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
# Remove --cov-fail-under from pytest addopts
|
|
732
|
+
addopts = target_coverage.get("addopts", "")
|
|
733
|
+
if isinstance(addopts, str):
|
|
734
|
+
original_addopts = addopts
|
|
735
|
+
# Remove --cov-fail-under=N pattern
|
|
736
|
+
addopts = re.sub(r"--cov-fail-under=\d+\.?\d*\s*", "", addopts).strip()
|
|
737
|
+
# Clean up extra spaces
|
|
738
|
+
addopts = " ".join(addopts.split())
|
|
739
|
+
|
|
740
|
+
if original_addopts != addopts:
|
|
741
|
+
target_coverage["addopts"] = addopts
|
|
742
|
+
self.console.print(
|
|
743
|
+
"[green]🔄[/green] Removed fixed coverage requirement (using ratchet system)",
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# Remove fail_under from coverage.report section
|
|
747
|
+
coverage_report = (
|
|
748
|
+
target_config.get("tool", {}).get("coverage", {}).get("report", {})
|
|
749
|
+
)
|
|
750
|
+
if "fail_under" in coverage_report:
|
|
751
|
+
original_fail_under = coverage_report["fail_under"]
|
|
752
|
+
coverage_report["fail_under"] = 0
|
|
753
|
+
self.console.print(
|
|
754
|
+
f"[green]🔄[/green] Reset coverage.report.fail_under from {original_fail_under} to 0 (ratchet system)",
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
def _extract_coverage_requirement(self, addopts: str | list[str]) -> int | None:
|
|
758
|
+
"""Extract coverage requirement from pytest addopts."""
|
|
759
|
+
import re
|
|
760
|
+
|
|
761
|
+
# Handle both string and list formats
|
|
762
|
+
addopts_str = " ".join(addopts) if isinstance(addopts, list) else addopts
|
|
763
|
+
match = re.search(r"--cov-fail-under=(\d+)", addopts_str)
|
|
764
|
+
return int(match.group(1)) if match else None
|
|
765
|
+
|
|
766
|
+
def _smart_merge_pre_commit_config(
|
|
767
|
+
self,
|
|
768
|
+
source_file: Path,
|
|
769
|
+
target_file: Path,
|
|
770
|
+
force: bool,
|
|
771
|
+
results: dict[str, t.Any],
|
|
772
|
+
) -> None:
|
|
773
|
+
"""Smart merge for .pre-commit-config.yaml."""
|
|
774
|
+
# Load source config
|
|
775
|
+
with source_file.open() as f:
|
|
776
|
+
source_config = yaml.safe_load(f)
|
|
777
|
+
|
|
778
|
+
if not target_file.exists():
|
|
779
|
+
# No existing file, just copy without trailing newline
|
|
780
|
+
content = source_file.read_text().rstrip("\n")
|
|
781
|
+
self._write_file_and_track(
|
|
782
|
+
target_file,
|
|
783
|
+
content,
|
|
784
|
+
".pre-commit-config.yaml",
|
|
785
|
+
results,
|
|
786
|
+
)
|
|
787
|
+
return
|
|
788
|
+
|
|
789
|
+
# Load existing config
|
|
790
|
+
with target_file.open() as f:
|
|
791
|
+
target_config = yaml.safe_load(f)
|
|
792
|
+
|
|
793
|
+
# Ensure configs are dictionaries
|
|
794
|
+
if not isinstance(source_config, dict):
|
|
795
|
+
source_config = {}
|
|
796
|
+
if not isinstance(target_config, dict):
|
|
797
|
+
target_config = {}
|
|
798
|
+
|
|
799
|
+
# Merge hooks without duplication
|
|
800
|
+
source_repos = source_config.get("repos", [])
|
|
801
|
+
target_repos = target_config.get("repos", [])
|
|
802
|
+
|
|
803
|
+
# Track existing repo URLs
|
|
804
|
+
existing_repo_urls = {repo.get("repo", "") for repo in target_repos}
|
|
805
|
+
|
|
806
|
+
# Add new repos that don't already exist
|
|
807
|
+
new_repos = [
|
|
808
|
+
repo
|
|
809
|
+
for repo in source_repos
|
|
810
|
+
if repo.get("repo", "") not in existing_repo_urls
|
|
811
|
+
]
|
|
812
|
+
|
|
813
|
+
if new_repos:
|
|
814
|
+
target_repos.extend(new_repos)
|
|
815
|
+
target_config["repos"] = target_repos
|
|
816
|
+
|
|
817
|
+
# Write merged config without trailing newline
|
|
818
|
+
yaml_content = yaml.dump(
|
|
819
|
+
target_config,
|
|
820
|
+
default_flow_style=False,
|
|
821
|
+
sort_keys=False,
|
|
822
|
+
width=float("inf"),
|
|
823
|
+
)
|
|
824
|
+
with target_file.open("w") as f:
|
|
825
|
+
content = (
|
|
826
|
+
yaml_content.decode()
|
|
827
|
+
if isinstance(yaml_content, bytes)
|
|
828
|
+
else yaml_content
|
|
829
|
+
)
|
|
830
|
+
f.write(content.rstrip("\n"))
|
|
831
|
+
|
|
832
|
+
t.cast("list[str]", results["files_copied"]).append(
|
|
833
|
+
".pre-commit-config.yaml (merged)",
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
try:
|
|
837
|
+
self.git_service.add_files([str(target_file)])
|
|
838
|
+
except Exception as e:
|
|
839
|
+
self.console.print(
|
|
840
|
+
f"[yellow]⚠️[/yellow] Could not git add .pre-commit-config.yaml: {e}",
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
self.console.print(
|
|
844
|
+
f"[green]✅[/green] Merged .pre-commit-config.yaml ({len(new_repos)} new repos)",
|
|
845
|
+
)
|
|
846
|
+
else:
|
|
847
|
+
self._skip_existing_file(".pre-commit-config.yaml (no new repos)", results)
|