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.

Files changed (155) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +225 -299
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +169 -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 +652 -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 +401 -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 +561 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +640 -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 +411 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +435 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +144 -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 +615 -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 +370 -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 +141 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +360 -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/execution_strategies.py +341 -0
  107. crackerjack/orchestration/test_progress_streamer.py +636 -0
  108. crackerjack/plugins/__init__.py +15 -0
  109. crackerjack/plugins/base.py +200 -0
  110. crackerjack/plugins/hooks.py +246 -0
  111. crackerjack/plugins/loader.py +335 -0
  112. crackerjack/plugins/managers.py +259 -0
  113. crackerjack/py313.py +8 -3
  114. crackerjack/services/__init__.py +22 -0
  115. crackerjack/services/cache.py +314 -0
  116. crackerjack/services/config.py +347 -0
  117. crackerjack/services/config_integrity.py +99 -0
  118. crackerjack/services/contextual_ai_assistant.py +516 -0
  119. crackerjack/services/coverage_ratchet.py +347 -0
  120. crackerjack/services/debug.py +736 -0
  121. crackerjack/services/dependency_monitor.py +617 -0
  122. crackerjack/services/enhanced_filesystem.py +439 -0
  123. crackerjack/services/file_hasher.py +151 -0
  124. crackerjack/services/filesystem.py +395 -0
  125. crackerjack/services/git.py +165 -0
  126. crackerjack/services/health_metrics.py +611 -0
  127. crackerjack/services/initialization.py +847 -0
  128. crackerjack/services/log_manager.py +286 -0
  129. crackerjack/services/logging.py +174 -0
  130. crackerjack/services/metrics.py +578 -0
  131. crackerjack/services/pattern_cache.py +362 -0
  132. crackerjack/services/pattern_detector.py +515 -0
  133. crackerjack/services/performance_benchmarks.py +653 -0
  134. crackerjack/services/security.py +163 -0
  135. crackerjack/services/server_manager.py +234 -0
  136. crackerjack/services/smart_scheduling.py +144 -0
  137. crackerjack/services/tool_version_service.py +61 -0
  138. crackerjack/services/unified_config.py +437 -0
  139. crackerjack/services/version_checker.py +248 -0
  140. crackerjack/slash_commands/__init__.py +14 -0
  141. crackerjack/slash_commands/init.md +122 -0
  142. crackerjack/slash_commands/run.md +163 -0
  143. crackerjack/slash_commands/status.md +127 -0
  144. crackerjack-0.31.4.dist-info/METADATA +742 -0
  145. crackerjack-0.31.4.dist-info/RECORD +148 -0
  146. crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
  147. crackerjack/.gitignore +0 -34
  148. crackerjack/.libcst.codemod.yaml +0 -18
  149. crackerjack/.pdm.toml +0 -1
  150. crackerjack/crackerjack.py +0 -3805
  151. crackerjack/pyproject.toml +0 -286
  152. crackerjack-0.30.3.dist-info/METADATA +0 -1290
  153. crackerjack-0.30.3.dist-info/RECORD +0 -16
  154. {crackerjack-0.30.3.dist-info โ†’ crackerjack-0.31.4.dist-info}/WHEEL +0 -0
  155. {crackerjack-0.30.3.dist-info โ†’ crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,11 @@
1
+ from .async_hook_manager import AsyncHookManager
2
+ from .hook_manager import HookManagerImpl
3
+ from .publish_manager import PublishManagerImpl
4
+ from .test_manager import TestManagementImpl
5
+
6
+ __all__ = [
7
+ "AsyncHookManager",
8
+ "HookManagerImpl",
9
+ "PublishManagerImpl",
10
+ "TestManagementImpl",
11
+ ]
@@ -0,0 +1,135 @@
1
+ import asyncio
2
+ import typing as t
3
+ from pathlib import Path
4
+
5
+ from rich.console import Console
6
+
7
+ from crackerjack.config.hooks import HookConfigLoader
8
+ from crackerjack.executors.async_hook_executor import AsyncHookExecutor
9
+ from crackerjack.models.task import HookResult
10
+
11
+
12
+ class AsyncHookManager:
13
+ def __init__(
14
+ self,
15
+ console: Console,
16
+ pkg_path: Path,
17
+ max_concurrent: int = 3,
18
+ ) -> None:
19
+ self.console = console
20
+ self.pkg_path = pkg_path
21
+ self.async_executor = AsyncHookExecutor(
22
+ console,
23
+ pkg_path,
24
+ max_concurrent=max_concurrent,
25
+ quiet=True,
26
+ )
27
+ self.config_loader = HookConfigLoader()
28
+
29
+ async def run_fast_hooks_async(self) -> list[HookResult]:
30
+ strategy = self.config_loader.load_strategy("fast")
31
+
32
+ strategy.parallel = False
33
+
34
+ execution_result = await self.async_executor.execute_strategy(strategy)
35
+ return execution_result.results
36
+
37
+ async def run_comprehensive_hooks_async(self) -> list[HookResult]:
38
+ strategy = self.config_loader.load_strategy("comprehensive")
39
+
40
+ strategy.parallel = True
41
+ strategy.max_workers = 3
42
+
43
+ execution_result = await self.async_executor.execute_strategy(strategy)
44
+ return execution_result.results
45
+
46
+ def run_fast_hooks(self) -> list[HookResult]:
47
+ return asyncio.run(self.run_fast_hooks_async())
48
+
49
+ def run_comprehensive_hooks(self) -> list[HookResult]:
50
+ return asyncio.run(self.run_comprehensive_hooks_async())
51
+
52
+ async def install_hooks_async(self) -> bool:
53
+ try:
54
+ process = await asyncio.create_subprocess_exec(
55
+ "pre-commit",
56
+ "install",
57
+ cwd=self.pkg_path,
58
+ stdout=asyncio.subprocess.PIPE,
59
+ stderr=asyncio.subprocess.PIPE,
60
+ )
61
+
62
+ _, stderr = await asyncio.wait_for(process.communicate(), timeout=30)
63
+
64
+ if process.returncode == 0:
65
+ self.console.print("[green]โœ…[/green] Pre-commit hooks installed")
66
+ return True
67
+ error_msg = stderr.decode() if stderr else "Unknown error"
68
+ self.console.print(
69
+ f"[red]โŒ[/red] Failed to install hooks: {error_msg}",
70
+ )
71
+ return False
72
+
73
+ except TimeoutError:
74
+ self.console.print("[red]โŒ[/red] Hook installation timed out")
75
+ return False
76
+ except Exception as e:
77
+ self.console.print(f"[red]โŒ[/red] Error installing hooks: {e}")
78
+ return False
79
+
80
+ def install_hooks(self) -> bool:
81
+ return asyncio.run(self.install_hooks_async())
82
+
83
+ async def update_hooks_async(self) -> bool:
84
+ try:
85
+ process = await asyncio.create_subprocess_exec(
86
+ "pre-commit",
87
+ "autoupdate",
88
+ cwd=self.pkg_path,
89
+ stdout=asyncio.subprocess.PIPE,
90
+ stderr=asyncio.subprocess.PIPE,
91
+ )
92
+
93
+ _, stderr = await asyncio.wait_for(process.communicate(), timeout=60)
94
+
95
+ if process.returncode == 0:
96
+ self.console.print("[green]โœ…[/green] Pre-commit hooks updated")
97
+ return True
98
+ error_msg = stderr.decode() if stderr else "Unknown error"
99
+ self.console.print(f"[red]โŒ[/red] Failed to update hooks: {error_msg}")
100
+ return False
101
+
102
+ except TimeoutError:
103
+ self.console.print("[red]โŒ[/red] Hook update timed out")
104
+ return False
105
+ except Exception as e:
106
+ self.console.print(f"[red]โŒ[/red] Error updating hooks: {e}")
107
+ return False
108
+
109
+ def update_hooks(self) -> bool:
110
+ return asyncio.run(self.update_hooks_async())
111
+
112
+ def get_hook_summary(self, results: list[HookResult]) -> dict[str, t.Any]:
113
+ if not results:
114
+ return {
115
+ "total": 0,
116
+ "passed": 0,
117
+ "failed": 0,
118
+ "errors": 0,
119
+ "total_duration": 0,
120
+ "success_rate": 0,
121
+ }
122
+
123
+ passed = sum(1 for r in results if r.status == "passed")
124
+ failed = sum(1 for r in results if r.status == "failed")
125
+ errors = sum(1 for r in results if r.status in ("timeout", "error"))
126
+ total_duration = sum(r.duration for r in results)
127
+
128
+ return {
129
+ "total": len(results),
130
+ "passed": passed,
131
+ "failed": failed,
132
+ "errors": errors,
133
+ "total_duration": total_duration,
134
+ "success_rate": (passed / len(results)) * 100 if results else 0,
135
+ }
@@ -0,0 +1,137 @@
1
+ import subprocess
2
+ import typing as t
3
+ from pathlib import Path
4
+
5
+ from rich.console import Console
6
+
7
+ from crackerjack.config.hooks import HookConfigLoader
8
+ from crackerjack.executors.hook_executor import HookExecutor
9
+ from crackerjack.models.task import HookResult
10
+
11
+
12
+ class HookManagerImpl:
13
+ def __init__(
14
+ self,
15
+ console: Console,
16
+ pkg_path: Path,
17
+ verbose: bool = False,
18
+ quiet: bool = False,
19
+ ) -> None:
20
+ self.console = console
21
+ self.pkg_path = pkg_path
22
+ self.executor = HookExecutor(console, pkg_path, verbose, quiet)
23
+ self.config_loader = HookConfigLoader()
24
+ self._config_path: Path | None = None
25
+
26
+ def set_config_path(self, config_path: Path) -> None:
27
+ self._config_path = config_path
28
+
29
+ def run_fast_hooks(self) -> list[HookResult]:
30
+ strategy = self.config_loader.load_strategy("fast")
31
+
32
+ if self._config_path:
33
+ for hook in strategy.hooks:
34
+ hook.config_path = self._config_path
35
+ execution_result = self.executor.execute_strategy(strategy)
36
+ return execution_result.results
37
+
38
+ def run_comprehensive_hooks(self) -> list[HookResult]:
39
+ strategy = self.config_loader.load_strategy("comprehensive")
40
+
41
+ if self._config_path:
42
+ for hook in strategy.hooks:
43
+ hook.config_path = self._config_path
44
+ execution_result = self.executor.execute_strategy(strategy)
45
+ return execution_result.results
46
+
47
+ def run_hooks(self) -> list[HookResult]:
48
+ fast_results = self.run_fast_hooks()
49
+ comprehensive_results = self.run_comprehensive_hooks()
50
+ return fast_results + comprehensive_results
51
+
52
+ def validate_hooks_config(self) -> bool:
53
+ try:
54
+ result = subprocess.run(
55
+ ["pre-commit", "validate-config"],
56
+ cwd=self.pkg_path,
57
+ capture_output=True,
58
+ text=True,
59
+ check=False,
60
+ )
61
+ return result.returncode == 0
62
+ except Exception:
63
+ return False
64
+
65
+ def get_hook_ids(self) -> list[str]:
66
+ fast_strategy = self.config_loader.load_strategy("fast")
67
+ comprehensive_strategy = self.config_loader.load_strategy("comprehensive")
68
+
69
+ all_hooks = fast_strategy.hooks + comprehensive_strategy.hooks
70
+ return [hook.name for hook in all_hooks]
71
+
72
+ def install_hooks(self) -> bool:
73
+ try:
74
+ result = subprocess.run(
75
+ ["pre-commit", "install"],
76
+ check=False,
77
+ cwd=self.pkg_path,
78
+ capture_output=True,
79
+ text=True,
80
+ timeout=30,
81
+ )
82
+ if result.returncode == 0:
83
+ self.console.print("[green]โœ…[/green] Pre-commit hooks installed")
84
+ return True
85
+ self.console.print(
86
+ f"[red]โŒ[/red] Failed to install hooks: {result.stderr}",
87
+ )
88
+ return False
89
+ except Exception as e:
90
+ self.console.print(f"[red]โŒ[/red] Error installing hooks: {e}")
91
+ return False
92
+
93
+ def update_hooks(self) -> bool:
94
+ try:
95
+ result = subprocess.run(
96
+ ["pre-commit", "autoupdate"],
97
+ check=False,
98
+ cwd=self.pkg_path,
99
+ capture_output=True,
100
+ text=True,
101
+ timeout=60,
102
+ )
103
+ if result.returncode == 0:
104
+ self.console.print("[green]โœ…[/green] Pre-commit hooks updated")
105
+ return True
106
+ self.console.print(
107
+ f"[red]โŒ[/red] Failed to update hooks: {result.stderr}",
108
+ )
109
+ return False
110
+ except Exception as e:
111
+ self.console.print(f"[red]โŒ[/red] Error updating hooks: {e}")
112
+ return False
113
+
114
+ def get_hook_summary(self, results: list[HookResult]) -> dict[str, t.Any]:
115
+ if not results:
116
+ return {
117
+ "total": 0,
118
+ "passed": 0,
119
+ "failed": 0,
120
+ "errors": 0,
121
+ "total_duration": 0,
122
+ "success_rate": 0,
123
+ }
124
+
125
+ passed = sum(1 for r in results if r.status == "passed")
126
+ failed = sum(1 for r in results if r.status == "failed")
127
+ errors = sum(1 for r in results if r.status in ("timeout", "error"))
128
+ total_duration = sum(r.duration for r in results)
129
+
130
+ return {
131
+ "total": len(results),
132
+ "passed": passed,
133
+ "failed": failed,
134
+ "errors": errors,
135
+ "total_duration": total_duration,
136
+ "success_rate": (passed / len(results)) * 100 if results else 0,
137
+ }
@@ -0,0 +1,411 @@
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 _execute_build(self) -> bool:
237
+ result = self._run_command(["uv", "build"])
238
+
239
+ if result.returncode != 0:
240
+ self.console.print(f"[red]โŒ[/red] Build failed: {result.stderr}")
241
+ return False
242
+
243
+ self.console.print("[green]โœ…[/green] Package built successfully")
244
+ self._display_build_artifacts()
245
+ return True
246
+
247
+ def _display_build_artifacts(self) -> None:
248
+ dist_dir = self.pkg_path / "dist"
249
+ if not dist_dir.exists():
250
+ return
251
+
252
+ artifacts = list(dist_dir.glob("*"))
253
+ self.console.print(f"[cyan]๐Ÿ“ฆ[/cyan] Build artifacts ({len(artifacts)}): ")
254
+
255
+ for artifact in artifacts[-5:]:
256
+ size_str = self._format_file_size(artifact.stat().st_size)
257
+ self.console.print(f" - {artifact.name} ({size_str})")
258
+
259
+ def _format_file_size(self, size: int) -> str:
260
+ if size < 1024 * 1024:
261
+ return f"{size / 1024:.1f}KB"
262
+ return f"{size / (1024 * 1024):.1f}MB"
263
+
264
+ def publish_package(self) -> bool:
265
+ if not self._validate_prerequisites():
266
+ return False
267
+
268
+ try:
269
+ self.console.print("[yellow]๐Ÿš€[/yellow] Publishing to PyPI...")
270
+ return self._perform_publish_workflow()
271
+ except Exception as e:
272
+ self.console.print(f"[red]โŒ[/red] Publish error: {e}")
273
+ return False
274
+
275
+ def _validate_prerequisites(self) -> bool:
276
+ return self.validate_auth()
277
+
278
+ def _perform_publish_workflow(self) -> bool:
279
+ if self.dry_run:
280
+ return self._handle_dry_run_publish()
281
+
282
+ if not self.build_package():
283
+ return False
284
+
285
+ return self._execute_publish()
286
+
287
+ def _handle_dry_run_publish(self) -> bool:
288
+ self.console.print("[yellow]๐Ÿ”[/yellow] Would publish package to PyPI")
289
+ return True
290
+
291
+ def _execute_publish(self) -> bool:
292
+ result = self._run_command(["uv", "publish"])
293
+
294
+ if result.returncode != 0:
295
+ self._handle_publish_failure(result.stderr)
296
+ return False
297
+
298
+ self._handle_publish_success()
299
+ return True
300
+
301
+ def _handle_publish_failure(self, error_msg: str) -> None:
302
+ self.console.print(f"[red]โŒ[/red] Publish failed: {error_msg}")
303
+
304
+ def _handle_publish_success(self) -> None:
305
+ self.console.print("[green]๐ŸŽ‰[/green] Package published successfully!")
306
+ self._display_package_url()
307
+
308
+ def _display_package_url(self) -> None:
309
+ current_version = self._get_current_version()
310
+ package_name = self._get_package_name()
311
+
312
+ if package_name and current_version:
313
+ url = f"https://pypi.org/project/{package_name}/{current_version}/"
314
+ self.console.print(f"[cyan]๐Ÿ”—[/cyan] Package URL: {url}")
315
+
316
+ def _get_package_name(self) -> str | None:
317
+ pyproject_path = self.pkg_path / "pyproject.toml"
318
+
319
+ with suppress(Exception):
320
+ from tomllib import loads
321
+
322
+ content = self.filesystem.read_file(pyproject_path)
323
+ data = loads(content)
324
+ return data.get("project", {}).get("name", "")
325
+
326
+ return None
327
+
328
+ def cleanup_old_releases(self, keep_releases: int = 10) -> bool:
329
+ try:
330
+ self.console.print(
331
+ f"[yellow]๐Ÿงน[/yellow] Cleaning up old releases (keeping {keep_releases})...",
332
+ )
333
+ if self.dry_run:
334
+ self.console.print(
335
+ "[yellow]๐Ÿ”[/yellow] Would clean up old PyPI releases",
336
+ )
337
+ return True
338
+ pyproject_path = self.pkg_path / "pyproject.toml"
339
+ from tomllib import loads
340
+
341
+ content = self.filesystem.read_file(pyproject_path)
342
+ data = loads(content)
343
+ package_name = data.get("project", {}).get("name", "")
344
+ if not package_name:
345
+ self.console.print(
346
+ "[yellow]โš ๏ธ[/yellow] Could not determine package name",
347
+ )
348
+ return False
349
+ self.console.print(
350
+ f"[cyan]๐Ÿ“ฆ[/cyan] Would analyze releases for {package_name}",
351
+ )
352
+ self.console.print(
353
+ f"[cyan]๐Ÿ”ง[/cyan] Would keep {keep_releases} most recent releases",
354
+ )
355
+
356
+ return True
357
+ except Exception as e:
358
+ self.console.print(f"[red]โŒ[/red] Cleanup error: {e}")
359
+ return False
360
+
361
+ def create_git_tag(self, version: str) -> bool:
362
+ try:
363
+ if self.dry_run:
364
+ self.console.print(
365
+ f"[yellow]๐Ÿ”[/yellow] Would create git tag: v{version}",
366
+ )
367
+ return True
368
+ result = self._run_command(["git", "tag", f"v{version}"])
369
+ if result.returncode == 0:
370
+ self.console.print(f"[green]๐Ÿท๏ธ[/green] Created git tag: v{version}")
371
+ push_result = self._run_command(
372
+ ["git", "push", "origin", f"v{version}"],
373
+ )
374
+ if push_result.returncode == 0:
375
+ self.console.print("[green]๐Ÿ“ค[/green] Pushed tag to remote")
376
+ else:
377
+ self.console.print(
378
+ f"[yellow]โš ๏ธ[/yellow] Tag created but push failed: {push_result.stderr}",
379
+ )
380
+
381
+ return True
382
+ self.console.print(
383
+ f"[red]โŒ[/red] Failed to create tag: {result.stderr}",
384
+ )
385
+ return False
386
+ except Exception as e:
387
+ self.console.print(f"[red]โŒ[/red] Tag creation error: {e}")
388
+ return False
389
+
390
+ def get_package_info(self) -> dict[str, t.Any]:
391
+ pyproject_path = self.pkg_path / "pyproject.toml"
392
+ if not pyproject_path.exists():
393
+ return {}
394
+ try:
395
+ from tomllib import loads
396
+
397
+ content = self.filesystem.read_file(pyproject_path)
398
+ data = loads(content)
399
+ project = data.get("project", {})
400
+
401
+ return {
402
+ "name": project.get("name", ""),
403
+ "version": project.get("version", ""),
404
+ "description": project.get("description", ""),
405
+ "authors": project.get("authors", []),
406
+ "dependencies": project.get("dependencies", []),
407
+ "python_requires": project.get("requires-python", ""),
408
+ }
409
+ except Exception as e:
410
+ self.console.print(f"[yellow]โš ๏ธ[/yellow] Error reading package info: {e}")
411
+ return {}