crackerjack 0.29.0__py3-none-any.whl โ 0.31.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +225 -253
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +169 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +652 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +401 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +670 -0
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +561 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +640 -0
- crackerjack/dynamic_config.py +577 -0
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +411 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +435 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +144 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +615 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +370 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +141 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +360 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +347 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +347 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +395 -0
- crackerjack/services/git.py +165 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +847 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.4.dist-info/METADATA +742 -0
- crackerjack-0.31.4.dist-info/RECORD +148 -0
- crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/.pre-commit-config-ai.yaml +0 -149
- crackerjack/.pre-commit-config-fast.yaml +0 -69
- crackerjack/.pre-commit-config.yaml +0 -114
- crackerjack/crackerjack.py +0 -4140
- crackerjack/pyproject.toml +0 -285
- crackerjack-0.29.0.dist-info/METADATA +0 -1289
- crackerjack-0.29.0.dist-info/RECORD +0 -17
- {crackerjack-0.29.0.dist-info โ crackerjack-0.31.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.29.0.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 {}
|