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.

Files changed (155) hide show
  1. crackerjack/CLAUDE.md +288 -705
  2. crackerjack/__main__.py +22 -8
  3. crackerjack/agents/__init__.py +0 -3
  4. crackerjack/agents/architect_agent.py +0 -43
  5. crackerjack/agents/base.py +1 -9
  6. crackerjack/agents/coordinator.py +2 -148
  7. crackerjack/agents/documentation_agent.py +109 -81
  8. crackerjack/agents/dry_agent.py +122 -97
  9. crackerjack/agents/formatting_agent.py +3 -16
  10. crackerjack/agents/import_optimization_agent.py +1174 -130
  11. crackerjack/agents/performance_agent.py +956 -188
  12. crackerjack/agents/performance_helpers.py +229 -0
  13. crackerjack/agents/proactive_agent.py +1 -48
  14. crackerjack/agents/refactoring_agent.py +516 -246
  15. crackerjack/agents/refactoring_helpers.py +282 -0
  16. crackerjack/agents/security_agent.py +393 -90
  17. crackerjack/agents/test_creation_agent.py +1776 -120
  18. crackerjack/agents/test_specialist_agent.py +59 -15
  19. crackerjack/agents/tracker.py +0 -102
  20. crackerjack/api.py +145 -37
  21. crackerjack/cli/handlers.py +48 -30
  22. crackerjack/cli/interactive.py +11 -11
  23. crackerjack/cli/options.py +66 -4
  24. crackerjack/code_cleaner.py +808 -148
  25. crackerjack/config/global_lock_config.py +110 -0
  26. crackerjack/config/hooks.py +43 -64
  27. crackerjack/core/async_workflow_orchestrator.py +247 -97
  28. crackerjack/core/autofix_coordinator.py +192 -109
  29. crackerjack/core/enhanced_container.py +46 -63
  30. crackerjack/core/file_lifecycle.py +549 -0
  31. crackerjack/core/performance.py +9 -8
  32. crackerjack/core/performance_monitor.py +395 -0
  33. crackerjack/core/phase_coordinator.py +281 -94
  34. crackerjack/core/proactive_workflow.py +9 -58
  35. crackerjack/core/resource_manager.py +501 -0
  36. crackerjack/core/service_watchdog.py +490 -0
  37. crackerjack/core/session_coordinator.py +4 -8
  38. crackerjack/core/timeout_manager.py +504 -0
  39. crackerjack/core/websocket_lifecycle.py +475 -0
  40. crackerjack/core/workflow_orchestrator.py +343 -209
  41. crackerjack/dynamic_config.py +50 -9
  42. crackerjack/errors.py +3 -4
  43. crackerjack/executors/async_hook_executor.py +63 -13
  44. crackerjack/executors/cached_hook_executor.py +14 -14
  45. crackerjack/executors/hook_executor.py +100 -37
  46. crackerjack/executors/hook_lock_manager.py +856 -0
  47. crackerjack/executors/individual_hook_executor.py +120 -86
  48. crackerjack/intelligence/__init__.py +0 -7
  49. crackerjack/intelligence/adaptive_learning.py +13 -86
  50. crackerjack/intelligence/agent_orchestrator.py +15 -78
  51. crackerjack/intelligence/agent_registry.py +12 -59
  52. crackerjack/intelligence/agent_selector.py +31 -92
  53. crackerjack/intelligence/integration.py +1 -41
  54. crackerjack/interactive.py +9 -9
  55. crackerjack/managers/async_hook_manager.py +25 -8
  56. crackerjack/managers/hook_manager.py +9 -9
  57. crackerjack/managers/publish_manager.py +57 -59
  58. crackerjack/managers/test_command_builder.py +6 -36
  59. crackerjack/managers/test_executor.py +9 -61
  60. crackerjack/managers/test_manager.py +17 -63
  61. crackerjack/managers/test_manager_backup.py +77 -127
  62. crackerjack/managers/test_progress.py +4 -23
  63. crackerjack/mcp/cache.py +5 -12
  64. crackerjack/mcp/client_runner.py +10 -10
  65. crackerjack/mcp/context.py +64 -6
  66. crackerjack/mcp/dashboard.py +14 -11
  67. crackerjack/mcp/enhanced_progress_monitor.py +55 -55
  68. crackerjack/mcp/file_monitor.py +72 -42
  69. crackerjack/mcp/progress_components.py +103 -84
  70. crackerjack/mcp/progress_monitor.py +122 -49
  71. crackerjack/mcp/rate_limiter.py +12 -12
  72. crackerjack/mcp/server_core.py +16 -22
  73. crackerjack/mcp/service_watchdog.py +26 -26
  74. crackerjack/mcp/state.py +15 -0
  75. crackerjack/mcp/tools/core_tools.py +95 -39
  76. crackerjack/mcp/tools/error_analyzer.py +6 -32
  77. crackerjack/mcp/tools/execution_tools.py +1 -56
  78. crackerjack/mcp/tools/execution_tools_backup.py +35 -131
  79. crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
  80. crackerjack/mcp/tools/intelligence_tools.py +2 -55
  81. crackerjack/mcp/tools/monitoring_tools.py +308 -145
  82. crackerjack/mcp/tools/proactive_tools.py +12 -42
  83. crackerjack/mcp/tools/progress_tools.py +23 -15
  84. crackerjack/mcp/tools/utility_tools.py +3 -40
  85. crackerjack/mcp/tools/workflow_executor.py +40 -60
  86. crackerjack/mcp/websocket/app.py +0 -3
  87. crackerjack/mcp/websocket/endpoints.py +206 -268
  88. crackerjack/mcp/websocket/jobs.py +213 -66
  89. crackerjack/mcp/websocket/server.py +84 -6
  90. crackerjack/mcp/websocket/websocket_handler.py +137 -29
  91. crackerjack/models/config_adapter.py +3 -16
  92. crackerjack/models/protocols.py +162 -3
  93. crackerjack/models/resource_protocols.py +454 -0
  94. crackerjack/models/task.py +3 -3
  95. crackerjack/monitoring/__init__.py +0 -0
  96. crackerjack/monitoring/ai_agent_watchdog.py +25 -71
  97. crackerjack/monitoring/regression_prevention.py +28 -87
  98. crackerjack/orchestration/advanced_orchestrator.py +44 -78
  99. crackerjack/orchestration/coverage_improvement.py +10 -60
  100. crackerjack/orchestration/execution_strategies.py +16 -16
  101. crackerjack/orchestration/test_progress_streamer.py +61 -53
  102. crackerjack/plugins/base.py +1 -1
  103. crackerjack/plugins/managers.py +22 -20
  104. crackerjack/py313.py +65 -21
  105. crackerjack/services/backup_service.py +467 -0
  106. crackerjack/services/bounded_status_operations.py +627 -0
  107. crackerjack/services/cache.py +7 -9
  108. crackerjack/services/config.py +35 -52
  109. crackerjack/services/config_integrity.py +5 -16
  110. crackerjack/services/config_merge.py +542 -0
  111. crackerjack/services/contextual_ai_assistant.py +17 -19
  112. crackerjack/services/coverage_ratchet.py +44 -73
  113. crackerjack/services/debug.py +25 -39
  114. crackerjack/services/dependency_monitor.py +52 -50
  115. crackerjack/services/enhanced_filesystem.py +14 -11
  116. crackerjack/services/file_hasher.py +1 -1
  117. crackerjack/services/filesystem.py +1 -12
  118. crackerjack/services/git.py +71 -47
  119. crackerjack/services/health_metrics.py +31 -27
  120. crackerjack/services/initialization.py +276 -428
  121. crackerjack/services/input_validator.py +760 -0
  122. crackerjack/services/log_manager.py +16 -16
  123. crackerjack/services/logging.py +7 -6
  124. crackerjack/services/metrics.py +43 -43
  125. crackerjack/services/pattern_cache.py +2 -31
  126. crackerjack/services/pattern_detector.py +26 -63
  127. crackerjack/services/performance_benchmarks.py +20 -45
  128. crackerjack/services/regex_patterns.py +2887 -0
  129. crackerjack/services/regex_utils.py +537 -0
  130. crackerjack/services/secure_path_utils.py +683 -0
  131. crackerjack/services/secure_status_formatter.py +534 -0
  132. crackerjack/services/secure_subprocess.py +605 -0
  133. crackerjack/services/security.py +47 -10
  134. crackerjack/services/security_logger.py +492 -0
  135. crackerjack/services/server_manager.py +109 -50
  136. crackerjack/services/smart_scheduling.py +8 -25
  137. crackerjack/services/status_authentication.py +603 -0
  138. crackerjack/services/status_security_manager.py +442 -0
  139. crackerjack/services/thread_safe_status_collector.py +546 -0
  140. crackerjack/services/tool_version_service.py +1 -23
  141. crackerjack/services/unified_config.py +36 -58
  142. crackerjack/services/validation_rate_limiter.py +269 -0
  143. crackerjack/services/version_checker.py +9 -40
  144. crackerjack/services/websocket_resource_limiter.py +572 -0
  145. crackerjack/slash_commands/__init__.py +52 -2
  146. crackerjack/tools/__init__.py +0 -0
  147. crackerjack/tools/validate_input_validator_patterns.py +262 -0
  148. crackerjack/tools/validate_regex_patterns.py +198 -0
  149. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/METADATA +197 -12
  150. crackerjack-0.31.13.dist-info/RECORD +178 -0
  151. crackerjack/cli/facade.py +0 -104
  152. crackerjack-0.31.10.dist-info/RECORD +0 -149
  153. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/WHEEL +0 -0
  154. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/entry_points.txt +0 -0
  155. {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
- project_name,
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", # Special handling: example.mcp.json -> .mcp.json with merging
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(f"[yellow]⚠️[/yellow] Could not git add {file_name}: {e}")
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
- # Target: .mcp.json in target project
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
- """Generate customized CLAUDE.md content for external projects."""
370
- return f"""
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
- # Implement with modern Python best practices
400
- Task tool with subagent_type="python-pro" for code implementation
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
- # Security review before completion
406
- Task tool with subagent_type="security-auditor" for security analysis
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
- ### **Core Principles**
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
- ### **Quality Rules**
422
- - **Cognitive complexity ≤15** per function (automatically enforced)
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
- # Quality checks (fast feedback during development)
449
+
433
450
  python -m crackerjack
434
451
 
435
- # With comprehensive testing
436
- python -m crackerjack -t
437
452
 
438
- # AI agent mode with autonomous fixing
439
- python -m crackerjack --ai-agent -t
453
+ python -m crackerjack - t
454
+
455
+
456
+ python -m crackerjack - - ai - agent - t
440
457
 
441
- # Full release workflow
442
- python -m crackerjack -a patch
458
+
459
+ python -m crackerjack - a patch
443
460
  ```
444
461
 
445
- ### **Recommended Workflow**
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
- """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
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
- 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
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
- 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
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
- # 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"
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
- target_file.write_text(merged_content)
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.console.print(f"[yellow]⚠️[/yellow] Could not git add {file_name}: {e}")
534
+ self._handle_file_processing_error(file_name, e, results)
521
535
 
522
- self.console.print(f"[green]✅[/green] Appended to {file_name}")
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
- self.git_service.add_files([str(target_file)])
613
- except Exception as e:
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
- def _ensure_crackerjack_dev_dependency(
621
- self,
622
- target_config: dict[str, t.Any],
623
- source_config: dict[str, t.Any],
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
- if "dev" not in target_config["dependency-groups"]:
631
- target_config["dependency-groups"]["dev"] = []
644
+ # Write the merged configuration
645
+ self.config_merge_service.write_pyproject_config(merged_config, target_file)
632
646
 
633
- dev_deps = target_config["dependency-groups"]["dev"]
634
- if "crackerjack" not in str(dev_deps):
635
- # Add crackerjack to dev dependencies
636
- dev_deps.append("crackerjack")
647
+ t.cast("list[str]", results["files_copied"]).append(
648
+ "pyproject.toml (merged)"
649
+ )
637
650
 
638
- def _merge_tool_configurations(
639
- self,
640
- target_config: dict[str, t.Any],
641
- source_config: dict[str, t.Any],
642
- project_name: str,
643
- ) -> None:
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
- for tool_name in tools_to_merge:
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
- # Special handling for pytest.ini_options markers
684
- self._merge_pytest_markers(target_tools, source_tools)
660
+ except Exception as e:
661
+ self._handle_file_processing_error("pyproject.toml", e, results)
685
662
 
686
- def _merge_tool_settings(
663
+ def _smart_merge_pre_commit_config(
687
664
  self,
688
- target_tool: dict[str, t.Any],
689
- source_tool: dict[str, t.Any],
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
- """Merge individual tool settings."""
694
- updated_keys = []
695
-
696
- for key, value in source_tool.items():
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
- if updated_keys:
704
- self.console.print(
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
- def _merge_pytest_markers(
709
- self,
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
- if new_markers:
735
- target_markers.extend(new_markers)
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
- def _remove_fixed_coverage_requirements(
741
- self,
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
- # Remove --cov-fail-under from pytest addopts
752
- addopts = target_coverage.get("addopts", "")
753
- if isinstance(addopts, str):
754
- original_addopts = addopts
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
- # Remove fail_under from coverage.report section
767
- coverage_report = (
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
- f"[green]🔄[/green] Reset coverage.report.fail_under from {original_fail_under} to 0 (ratchet system)",
698
+ "[yellow]⚠️[/yellow] Source .pre-commit-config.yaml is not a dictionary, skipping merge"
775
699
  )
700
+ return None
776
701
 
777
- def _extract_coverage_requirement(self, addopts: str | list[str]) -> int | None:
778
- """Extract coverage requirement from pytest addopts."""
779
- import re
702
+ return source_config
780
703
 
781
- # Handle both string and list formats
782
- addopts_str = " ".join(addopts) if isinstance(addopts, list) else addopts
783
- match = re.search(r"--cov-fail-under=(\d+)", addopts_str)
784
- return int(match.group(1)) if match else None
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 _smart_merge_pre_commit_config(
712
+ def _should_skip_merge(
787
713
  self,
788
- source_file: Path,
789
714
  target_file: Path,
790
- project_name: str,
791
- force: bool,
715
+ merged_config: dict[str, t.Any],
792
716
  results: dict[str, t.Any],
793
- ) -> None:
794
- """Smart merge for .pre-commit-config.yaml."""
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
- # No existing file, copy with project-specific replacements
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
- target_config = yaml.safe_load(f)
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
- if new_repos:
843
- target_repos.extend(new_repos)
844
- target_config["repos"] = target_repos
725
+ # Ensure old_config is a dict
726
+ if not isinstance(old_config, dict):
727
+ old_config = {}
845
728
 
846
- # Write merged config with proper formatting
847
- yaml_content = yaml.dump(
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
- # Ensure content is not None before cleaning
860
- if content is None:
861
- content = ""
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
- content = FileSystemService.clean_trailing_whitespace_and_newlines(content)
736
+ return False
867
737
 
868
- with target_file.open("w") as f:
869
- f.write(content)
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
- t.cast("list[str]", results["files_copied"]).append(
872
- ".pre-commit-config.yaml (merged)",
873
- )
749
+ t.cast("list[str]", results["files_copied"]).append(
750
+ ".pre-commit-config.yaml (merged)"
751
+ )
874
752
 
875
- try:
876
- self.git_service.add_files([str(target_file)])
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"[green][/green] Merged .pre-commit-config.yaml ({len(new_repos)} new repos)",
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 _replace_project_name_in_tool_config(
889
- self, tool_config: dict[str, t.Any], project_name: str
890
- ) -> dict[str, t.Any]:
891
- """Replace project name in entire tool configuration."""
892
- if project_name == "crackerjack":
893
- return tool_config # No replacement needed
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
+ )