crackerjack 0.30.3__py3-none-any.whl → 0.31.7__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 (156) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +227 -299
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +170 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +657 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +409 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +618 -928
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +585 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +826 -0
  40. crackerjack/dynamic_config.py +94 -103
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +433 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +443 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +114 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +621 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +372 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +217 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +565 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/coverage_improvement.py +223 -0
  107. crackerjack/orchestration/execution_strategies.py +341 -0
  108. crackerjack/orchestration/test_progress_streamer.py +636 -0
  109. crackerjack/plugins/__init__.py +15 -0
  110. crackerjack/plugins/base.py +200 -0
  111. crackerjack/plugins/hooks.py +246 -0
  112. crackerjack/plugins/loader.py +335 -0
  113. crackerjack/plugins/managers.py +259 -0
  114. crackerjack/py313.py +8 -3
  115. crackerjack/services/__init__.py +22 -0
  116. crackerjack/services/cache.py +314 -0
  117. crackerjack/services/config.py +358 -0
  118. crackerjack/services/config_integrity.py +99 -0
  119. crackerjack/services/contextual_ai_assistant.py +516 -0
  120. crackerjack/services/coverage_ratchet.py +356 -0
  121. crackerjack/services/debug.py +736 -0
  122. crackerjack/services/dependency_monitor.py +617 -0
  123. crackerjack/services/enhanced_filesystem.py +439 -0
  124. crackerjack/services/file_hasher.py +151 -0
  125. crackerjack/services/filesystem.py +421 -0
  126. crackerjack/services/git.py +176 -0
  127. crackerjack/services/health_metrics.py +611 -0
  128. crackerjack/services/initialization.py +873 -0
  129. crackerjack/services/log_manager.py +286 -0
  130. crackerjack/services/logging.py +174 -0
  131. crackerjack/services/metrics.py +578 -0
  132. crackerjack/services/pattern_cache.py +362 -0
  133. crackerjack/services/pattern_detector.py +515 -0
  134. crackerjack/services/performance_benchmarks.py +653 -0
  135. crackerjack/services/security.py +163 -0
  136. crackerjack/services/server_manager.py +234 -0
  137. crackerjack/services/smart_scheduling.py +144 -0
  138. crackerjack/services/tool_version_service.py +61 -0
  139. crackerjack/services/unified_config.py +437 -0
  140. crackerjack/services/version_checker.py +248 -0
  141. crackerjack/slash_commands/__init__.py +14 -0
  142. crackerjack/slash_commands/init.md +122 -0
  143. crackerjack/slash_commands/run.md +163 -0
  144. crackerjack/slash_commands/status.md +127 -0
  145. crackerjack-0.31.7.dist-info/METADATA +742 -0
  146. crackerjack-0.31.7.dist-info/RECORD +149 -0
  147. crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
  148. crackerjack/.gitignore +0 -34
  149. crackerjack/.libcst.codemod.yaml +0 -18
  150. crackerjack/.pdm.toml +0 -1
  151. crackerjack/crackerjack.py +0 -3805
  152. crackerjack/pyproject.toml +0 -286
  153. crackerjack-0.30.3.dist-info/METADATA +0 -1290
  154. crackerjack-0.30.3.dist-info/RECORD +0 -16
  155. {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/WHEEL +0 -0
  156. {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,433 @@
1
+ import subprocess
2
+ import typing as t
3
+ from contextlib import suppress
4
+ from pathlib import Path
5
+
6
+ from rich.console import Console
7
+
8
+ from crackerjack.services.filesystem import FileSystemService
9
+ from crackerjack.services.security import SecurityService
10
+
11
+
12
+ class PublishManagerImpl:
13
+ def __init__(self, console: Console, pkg_path: Path, dry_run: bool = False) -> None:
14
+ self.console = console
15
+ self.pkg_path = pkg_path
16
+ self.dry_run = dry_run
17
+ self.filesystem = FileSystemService()
18
+ self.security = SecurityService()
19
+
20
+ def _run_command(
21
+ self,
22
+ cmd: list[str],
23
+ timeout: int = 300,
24
+ ) -> subprocess.CompletedProcess[str]:
25
+ secure_env = self.security.create_secure_command_env()
26
+
27
+ result = subprocess.run(
28
+ cmd,
29
+ check=False,
30
+ cwd=self.pkg_path,
31
+ capture_output=True,
32
+ text=True,
33
+ timeout=timeout,
34
+ env=secure_env,
35
+ )
36
+
37
+ if result.stdout:
38
+ result.stdout = self.security.mask_tokens(result.stdout)
39
+ if result.stderr:
40
+ result.stderr = self.security.mask_tokens(result.stderr)
41
+
42
+ return result
43
+
44
+ def _get_current_version(self) -> str | None:
45
+ pyproject_path = self.pkg_path / "pyproject.toml"
46
+ if not pyproject_path.exists():
47
+ return None
48
+ try:
49
+ from tomllib import loads
50
+
51
+ content = self.filesystem.read_file(pyproject_path)
52
+ data = loads(content)
53
+ return data.get("project", {}).get("version")
54
+ except Exception as e:
55
+ self.console.print(f"[yellow]⚠️[/yellow] Error reading version: {e}")
56
+ return None
57
+
58
+ def _update_version_in_file(self, new_version: str) -> bool:
59
+ pyproject_path = self.pkg_path / "pyproject.toml"
60
+ try:
61
+ content = self.filesystem.read_file(pyproject_path)
62
+ import re
63
+
64
+ # More specific pattern to only match project version, not tool versions
65
+ pattern = r'^(version\s*=\s*["\'])([^"\']+)(["\'])$'
66
+ replacement = f"\\g<1>{new_version}\\g<3>"
67
+ new_content = re.sub(pattern, replacement, content, flags=re.MULTILINE)
68
+ if content != new_content:
69
+ if not self.dry_run:
70
+ self.filesystem.write_file(pyproject_path, new_content)
71
+ self.console.print(
72
+ f"[green]✅[/green] Updated version to {new_version}",
73
+ )
74
+ return True
75
+ self.console.print(
76
+ "[yellow]⚠️[/yellow] Version pattern not found in pyproject.toml",
77
+ )
78
+ return False
79
+ except Exception as e:
80
+ self.console.print(f"[red]❌[/red] Error updating version: {e}")
81
+ return False
82
+
83
+ def _calculate_next_version(self, current: str, bump_type: str) -> str:
84
+ try:
85
+ parts = current.split(".")
86
+ if len(parts) != 3:
87
+ msg = f"Invalid version format: {current}"
88
+ raise ValueError(msg)
89
+ major, minor, patch = map(int, parts)
90
+ if bump_type == "major":
91
+ return f"{major + 1}.0.0"
92
+ if bump_type == "minor":
93
+ return f"{major}.{minor + 1}.0"
94
+ if bump_type == "patch":
95
+ return f"{major}.{minor}.{patch + 1}"
96
+ msg = f"Invalid bump type: {bump_type}"
97
+ raise ValueError(msg)
98
+ except Exception as e:
99
+ self.console.print(f"[red]❌[/red] Error calculating version: {e}")
100
+ raise
101
+
102
+ def bump_version(self, version_type: str) -> str:
103
+ current_version = self._get_current_version()
104
+ if not current_version:
105
+ self.console.print("[red]❌[/red] Could not determine current version")
106
+ msg = "Cannot determine current version"
107
+ raise ValueError(msg)
108
+ self.console.print(f"[cyan]📦[/cyan] Current version: {current_version}")
109
+
110
+ # Handle interactive version selection
111
+ if version_type == "interactive":
112
+ version_type = self._prompt_for_version_type()
113
+
114
+ try:
115
+ new_version = self._calculate_next_version(current_version, version_type)
116
+ if self.dry_run:
117
+ self.console.print(
118
+ f"[yellow]🔍[/yellow] Would bump {version_type} version: {current_version} → {new_version}",
119
+ )
120
+ elif self._update_version_in_file(new_version):
121
+ self.console.print(
122
+ f"[green]🚀[/green] Bumped {version_type} version: {current_version} → {new_version}",
123
+ )
124
+ else:
125
+ msg = "Failed to update version in file"
126
+ raise ValueError(msg)
127
+
128
+ return new_version
129
+ except Exception as e:
130
+ self.console.print(f"[red]❌[/red] Version bump failed: {e}")
131
+ raise
132
+
133
+ def _prompt_for_version_type(self) -> str:
134
+ """Prompt user to select version type interactively."""
135
+ try:
136
+ from rich.prompt import Prompt
137
+
138
+ return Prompt.ask(
139
+ "[cyan]📦[/cyan] Select version bump type",
140
+ choices=["patch", "minor", "major"],
141
+ default="patch",
142
+ )
143
+ except ImportError:
144
+ self.console.print(
145
+ "[yellow]⚠️[/yellow] Rich prompt not available, defaulting to patch"
146
+ )
147
+ return "patch"
148
+
149
+ def validate_auth(self) -> bool:
150
+ auth_methods = self._collect_auth_methods()
151
+ return self._report_auth_status(auth_methods)
152
+
153
+ def _collect_auth_methods(self) -> list[str]:
154
+ auth_methods: list[str] = []
155
+
156
+ env_auth = self._check_env_token_auth()
157
+ if env_auth:
158
+ auth_methods.append(env_auth)
159
+
160
+ keyring_auth = self._check_keyring_auth()
161
+ if keyring_auth:
162
+ auth_methods.append(keyring_auth)
163
+
164
+ return auth_methods
165
+
166
+ def _check_env_token_auth(self) -> str | None:
167
+ import os
168
+
169
+ token = os.getenv("UV_PUBLISH_TOKEN")
170
+ if not token:
171
+ return None
172
+
173
+ if self.security.validate_token_format(token, "pypi"):
174
+ masked_token = self.security.mask_tokens(token)
175
+ self.console.print(f"[dim]Token format: {masked_token}[/dim]", style="dim")
176
+ return "Environment variable (UV_PUBLISH_TOKEN)"
177
+ self.console.print(
178
+ "[yellow]⚠️[/yellow] UV_PUBLISH_TOKEN format appears invalid",
179
+ )
180
+ return None
181
+
182
+ def _check_keyring_auth(self) -> str | None:
183
+ try:
184
+ result = self._run_command(
185
+ ["keyring", "get", "https://upload.pypi.org/legacy/", "__token__"],
186
+ )
187
+ if result.returncode == 0 and result.stdout.strip():
188
+ keyring_token = result.stdout.strip()
189
+ if self.security.validate_token_format(keyring_token, "pypi"):
190
+ return "Keyring storage"
191
+ self.console.print(
192
+ "[yellow]⚠️[/yellow] Keyring token format appears invalid",
193
+ )
194
+ except (subprocess.SubprocessError, OSError, FileNotFoundError):
195
+ pass
196
+ return None
197
+
198
+ def _report_auth_status(self, auth_methods: list[str]) -> bool:
199
+ if auth_methods:
200
+ self.console.print("[green]✅[/green] PyPI authentication available: ")
201
+ for method in auth_methods:
202
+ self.console.print(f" - {method}")
203
+ return True
204
+ self._display_auth_setup_instructions()
205
+ return False
206
+
207
+ def _display_auth_setup_instructions(self) -> None:
208
+ self.console.print("[red]❌[/red] No valid PyPI authentication found")
209
+ self.console.print("\n[yellow]💡[/yellow] Setup options: ")
210
+ self.console.print(
211
+ " 1. Set environment variable: export UV_PUBLISH_TOKEN=<your-pypi-token>",
212
+ )
213
+ self.console.print(
214
+ " 2. Use keyring: keyring set https://upload.pypi.org/legacy/ __token__",
215
+ )
216
+ self.console.print(
217
+ " 3. Ensure token starts with 'pypi-' and is properly formatted",
218
+ )
219
+
220
+ def build_package(self) -> bool:
221
+ try:
222
+ self.console.print("[yellow]🔨[/yellow] Building package...")
223
+
224
+ if self.dry_run:
225
+ return self._handle_dry_run_build()
226
+
227
+ return self._execute_build()
228
+ except Exception as e:
229
+ self.console.print(f"[red]❌[/red] Build error: {e}")
230
+ return False
231
+
232
+ def _handle_dry_run_build(self) -> bool:
233
+ self.console.print("[yellow]🔍[/yellow] Would build package")
234
+ return True
235
+
236
+ def _clean_dist_directory(self) -> None:
237
+ """Clean dist directory to ensure only current version artifacts are uploaded."""
238
+ dist_dir = self.pkg_path / "dist"
239
+ if not dist_dir.exists():
240
+ return
241
+
242
+ try:
243
+ import shutil
244
+
245
+ # Remove entire dist directory and recreate it
246
+ shutil.rmtree(dist_dir)
247
+ dist_dir.mkdir(exist_ok=True)
248
+ self.console.print("[cyan]🧹[/cyan] Cleaned dist directory for fresh build")
249
+ except Exception as e:
250
+ self.console.print(
251
+ f"[yellow]⚠️[/yellow] Warning: Could not clean dist directory: {e}"
252
+ )
253
+ # Continue with build anyway - uv publish will fail with clear error
254
+
255
+ def _execute_build(self) -> bool:
256
+ # Clean dist directory before building to avoid uploading multiple versions
257
+ self._clean_dist_directory()
258
+
259
+ result = self._run_command(["uv", "build"])
260
+
261
+ if result.returncode != 0:
262
+ self.console.print(f"[red]❌[/red] Build failed: {result.stderr}")
263
+ return False
264
+
265
+ self.console.print("[green]✅[/green] Package built successfully")
266
+ self._display_build_artifacts()
267
+ return True
268
+
269
+ def _display_build_artifacts(self) -> None:
270
+ dist_dir = self.pkg_path / "dist"
271
+ if not dist_dir.exists():
272
+ return
273
+
274
+ artifacts = list(dist_dir.glob("*"))
275
+ self.console.print(f"[cyan]📦[/cyan] Build artifacts ({len(artifacts)}): ")
276
+
277
+ for artifact in artifacts[-5:]:
278
+ size_str = self._format_file_size(artifact.stat().st_size)
279
+ self.console.print(f" - {artifact.name} ({size_str})")
280
+
281
+ def _format_file_size(self, size: int) -> str:
282
+ if size < 1024 * 1024:
283
+ return f"{size / 1024:.1f}KB"
284
+ return f"{size / (1024 * 1024):.1f}MB"
285
+
286
+ def publish_package(self) -> bool:
287
+ if not self._validate_prerequisites():
288
+ return False
289
+
290
+ try:
291
+ self.console.print("[yellow]🚀[/yellow] Publishing to PyPI...")
292
+ return self._perform_publish_workflow()
293
+ except Exception as e:
294
+ self.console.print(f"[red]❌[/red] Publish error: {e}")
295
+ return False
296
+
297
+ def _validate_prerequisites(self) -> bool:
298
+ return self.validate_auth()
299
+
300
+ def _perform_publish_workflow(self) -> bool:
301
+ if self.dry_run:
302
+ return self._handle_dry_run_publish()
303
+
304
+ if not self.build_package():
305
+ return False
306
+
307
+ return self._execute_publish()
308
+
309
+ def _handle_dry_run_publish(self) -> bool:
310
+ self.console.print("[yellow]🔍[/yellow] Would publish package to PyPI")
311
+ return True
312
+
313
+ def _execute_publish(self) -> bool:
314
+ result = self._run_command(["uv", "publish"])
315
+
316
+ if result.returncode != 0:
317
+ self._handle_publish_failure(result.stderr)
318
+ return False
319
+
320
+ self._handle_publish_success()
321
+ return True
322
+
323
+ def _handle_publish_failure(self, error_msg: str) -> None:
324
+ self.console.print(f"[red]❌[/red] Publish failed: {error_msg}")
325
+
326
+ def _handle_publish_success(self) -> None:
327
+ self.console.print("[green]🎉[/green] Package published successfully!")
328
+ self._display_package_url()
329
+
330
+ def _display_package_url(self) -> None:
331
+ current_version = self._get_current_version()
332
+ package_name = self._get_package_name()
333
+
334
+ if package_name and current_version:
335
+ url = f"https://pypi.org/project/{package_name}/{current_version}/"
336
+ self.console.print(f"[cyan]🔗[/cyan] Package URL: {url}")
337
+
338
+ def _get_package_name(self) -> str | None:
339
+ pyproject_path = self.pkg_path / "pyproject.toml"
340
+
341
+ with suppress(Exception):
342
+ from tomllib import loads
343
+
344
+ content = self.filesystem.read_file(pyproject_path)
345
+ data = loads(content)
346
+ return data.get("project", {}).get("name", "")
347
+
348
+ return None
349
+
350
+ def cleanup_old_releases(self, keep_releases: int = 10) -> bool:
351
+ try:
352
+ self.console.print(
353
+ f"[yellow]🧹[/yellow] Cleaning up old releases (keeping {keep_releases})...",
354
+ )
355
+ if self.dry_run:
356
+ self.console.print(
357
+ "[yellow]🔍[/yellow] Would clean up old PyPI releases",
358
+ )
359
+ return True
360
+ pyproject_path = self.pkg_path / "pyproject.toml"
361
+ from tomllib import loads
362
+
363
+ content = self.filesystem.read_file(pyproject_path)
364
+ data = loads(content)
365
+ package_name = data.get("project", {}).get("name", "")
366
+ if not package_name:
367
+ self.console.print(
368
+ "[yellow]⚠️[/yellow] Could not determine package name",
369
+ )
370
+ return False
371
+ self.console.print(
372
+ f"[cyan]📦[/cyan] Would analyze releases for {package_name}",
373
+ )
374
+ self.console.print(
375
+ f"[cyan]🔧[/cyan] Would keep {keep_releases} most recent releases",
376
+ )
377
+
378
+ return True
379
+ except Exception as e:
380
+ self.console.print(f"[red]❌[/red] Cleanup error: {e}")
381
+ return False
382
+
383
+ def create_git_tag(self, version: str) -> bool:
384
+ try:
385
+ if self.dry_run:
386
+ self.console.print(
387
+ f"[yellow]🔍[/yellow] Would create git tag: v{version}",
388
+ )
389
+ return True
390
+ result = self._run_command(["git", "tag", f"v{version}"])
391
+ if result.returncode == 0:
392
+ self.console.print(f"[green]🏷️[/green] Created git tag: v{version}")
393
+ push_result = self._run_command(
394
+ ["git", "push", "origin", f"v{version}"],
395
+ )
396
+ if push_result.returncode == 0:
397
+ self.console.print("[green]📤[/green] Pushed tag to remote")
398
+ else:
399
+ self.console.print(
400
+ f"[yellow]⚠️[/yellow] Tag created but push failed: {push_result.stderr}",
401
+ )
402
+
403
+ return True
404
+ self.console.print(
405
+ f"[red]❌[/red] Failed to create tag: {result.stderr}",
406
+ )
407
+ return False
408
+ except Exception as e:
409
+ self.console.print(f"[red]❌[/red] Tag creation error: {e}")
410
+ return False
411
+
412
+ def get_package_info(self) -> dict[str, t.Any]:
413
+ pyproject_path = self.pkg_path / "pyproject.toml"
414
+ if not pyproject_path.exists():
415
+ return {}
416
+ try:
417
+ from tomllib import loads
418
+
419
+ content = self.filesystem.read_file(pyproject_path)
420
+ data = loads(content)
421
+ project = data.get("project", {})
422
+
423
+ return {
424
+ "name": project.get("name", ""),
425
+ "version": project.get("version", ""),
426
+ "description": project.get("description", ""),
427
+ "authors": project.get("authors", []),
428
+ "dependencies": project.get("dependencies", []),
429
+ "python_requires": project.get("requires-python", ""),
430
+ }
431
+ except Exception as e:
432
+ self.console.print(f"[yellow]⚠️[/yellow] Error reading package info: {e}")
433
+ return {}
@@ -0,0 +1,151 @@
1
+ """Test command building and configuration.
2
+
3
+ This module handles pytest command construction with various options and configurations.
4
+ Split from test_manager.py for better separation of concerns.
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ from crackerjack.models.protocols import OptionsProtocol
10
+
11
+
12
+ class TestCommandBuilder:
13
+ """Builds pytest commands with appropriate options and configurations."""
14
+
15
+ def __init__(self, pkg_path: Path) -> None:
16
+ self.pkg_path = pkg_path
17
+
18
+ def build_command(self, options: OptionsProtocol) -> list[str]:
19
+ """Build complete pytest command with all options."""
20
+ cmd = ["python", "-m", "pytest"]
21
+
22
+ self._add_coverage_options(cmd, options)
23
+ self._add_worker_options(cmd, options)
24
+ self._add_benchmark_options(cmd, options)
25
+ self._add_timeout_options(cmd, options)
26
+ self._add_verbosity_options(cmd, options)
27
+ self._add_test_path(cmd)
28
+
29
+ return cmd
30
+
31
+ def get_optimal_workers(self, options: OptionsProtocol) -> int:
32
+ """Calculate optimal number of pytest workers based on system and configuration."""
33
+ if hasattr(options, "test_workers") and options.test_workers:
34
+ return options.test_workers
35
+
36
+ # Auto-detect based on CPU count
37
+ import multiprocessing
38
+
39
+ cpu_count = multiprocessing.cpu_count()
40
+
41
+ # Conservative worker count to avoid overwhelming the system
42
+ if cpu_count <= 2:
43
+ return 1
44
+ elif cpu_count <= 4:
45
+ return 2
46
+ elif cpu_count <= 8:
47
+ return 3
48
+ return 4
49
+
50
+ def get_test_timeout(self, options: OptionsProtocol) -> int:
51
+ """Get test timeout based on options or default."""
52
+ if hasattr(options, "test_timeout") and options.test_timeout:
53
+ return options.test_timeout
54
+
55
+ # Default timeout based on test configuration
56
+ if hasattr(options, "benchmark") and options.benchmark:
57
+ return 900 # 15 minutes for benchmarks
58
+ return 300 # 5 minutes for regular tests
59
+
60
+ def _add_coverage_options(self, cmd: list[str], options: OptionsProtocol) -> None:
61
+ """Add coverage-related options to command."""
62
+ # Always include coverage for comprehensive testing
63
+ cmd.extend(
64
+ [
65
+ "--cov=crackerjack",
66
+ "--cov-report=term-missing",
67
+ "--cov-report=html",
68
+ "--cov-fail-under=0", # Don't fail on low coverage, let ratchet handle it
69
+ ]
70
+ )
71
+
72
+ def _add_worker_options(self, cmd: list[str], options: OptionsProtocol) -> None:
73
+ """Add parallel execution options to command."""
74
+ workers = self.get_optimal_workers(options)
75
+ if workers > 1:
76
+ cmd.extend(["-n", str(workers)])
77
+
78
+ def _add_benchmark_options(self, cmd: list[str], options: OptionsProtocol) -> None:
79
+ """Add benchmark-specific options to command."""
80
+ if hasattr(options, "benchmark") and options.benchmark:
81
+ cmd.extend(
82
+ [
83
+ "--benchmark-only",
84
+ "--benchmark-sort=mean",
85
+ "--benchmark-columns=min,max,mean,stddev",
86
+ ]
87
+ )
88
+
89
+ def _add_timeout_options(self, cmd: list[str], options: OptionsProtocol) -> None:
90
+ """Add timeout options to command."""
91
+ timeout = self.get_test_timeout(options)
92
+ cmd.extend(["--timeout", str(timeout)])
93
+
94
+ def _add_verbosity_options(self, cmd: list[str], options: OptionsProtocol) -> None:
95
+ """Add verbosity and output formatting options."""
96
+ # Always use verbose output for better progress tracking
97
+ cmd.append("-v")
98
+
99
+ # Add useful output options
100
+ cmd.extend(
101
+ [
102
+ "--tb=short", # Shorter traceback format
103
+ "--strict-markers", # Ensure all markers are defined
104
+ "--strict-config", # Ensure configuration is valid
105
+ ]
106
+ )
107
+
108
+ def _add_test_path(self, cmd: list[str]) -> None:
109
+ """Add test path to command."""
110
+ # Add tests directory if it exists, otherwise current directory
111
+ test_paths = ["tests", "test"]
112
+
113
+ for test_path in test_paths:
114
+ full_path = self.pkg_path / test_path
115
+ if full_path.exists() and full_path.is_dir():
116
+ cmd.append(str(full_path))
117
+ return
118
+
119
+ # Fallback to current directory
120
+ cmd.append(str(self.pkg_path))
121
+
122
+ def build_specific_test_command(self, test_pattern: str) -> list[str]:
123
+ """Build command for running specific tests matching a pattern."""
124
+ cmd = ["python", "-m", "pytest", "-v"]
125
+
126
+ # Add basic coverage
127
+ cmd.extend(
128
+ [
129
+ "--cov=crackerjack",
130
+ "--cov-report=term-missing",
131
+ ]
132
+ )
133
+
134
+ # Add the test pattern
135
+ cmd.extend(["-k", test_pattern])
136
+
137
+ # Add test path
138
+ self._add_test_path(cmd)
139
+
140
+ return cmd
141
+
142
+ def build_validation_command(self) -> list[str]:
143
+ """Build command for test environment validation."""
144
+ return [
145
+ "python",
146
+ "-m",
147
+ "pytest",
148
+ "--collect-only",
149
+ "--quiet",
150
+ "tests" if (self.pkg_path / "tests").exists() else ".",
151
+ ]