crackerjack 0.18.2__py3-none-any.whl → 0.45.2__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.
- crackerjack/README.md +19 -0
- crackerjack/__init__.py +96 -2
- crackerjack/__main__.py +637 -138
- crackerjack/adapters/README.md +18 -0
- crackerjack/adapters/__init__.py +39 -0
- crackerjack/adapters/_output_paths.py +167 -0
- crackerjack/adapters/_qa_adapter_base.py +309 -0
- crackerjack/adapters/_tool_adapter_base.py +706 -0
- crackerjack/adapters/ai/README.md +65 -0
- crackerjack/adapters/ai/__init__.py +5 -0
- crackerjack/adapters/ai/claude.py +853 -0
- crackerjack/adapters/complexity/README.md +53 -0
- crackerjack/adapters/complexity/__init__.py +10 -0
- crackerjack/adapters/complexity/complexipy.py +641 -0
- crackerjack/adapters/dependency/__init__.py +22 -0
- crackerjack/adapters/dependency/pip_audit.py +418 -0
- crackerjack/adapters/format/README.md +72 -0
- crackerjack/adapters/format/__init__.py +11 -0
- crackerjack/adapters/format/mdformat.py +313 -0
- crackerjack/adapters/format/ruff.py +516 -0
- crackerjack/adapters/lint/README.md +47 -0
- crackerjack/adapters/lint/__init__.py +11 -0
- crackerjack/adapters/lint/codespell.py +273 -0
- crackerjack/adapters/lsp/README.md +49 -0
- crackerjack/adapters/lsp/__init__.py +27 -0
- crackerjack/adapters/lsp/_base.py +194 -0
- crackerjack/adapters/lsp/_client.py +358 -0
- crackerjack/adapters/lsp/_manager.py +193 -0
- crackerjack/adapters/lsp/skylos.py +283 -0
- crackerjack/adapters/lsp/zuban.py +557 -0
- crackerjack/adapters/refactor/README.md +59 -0
- crackerjack/adapters/refactor/__init__.py +12 -0
- crackerjack/adapters/refactor/creosote.py +318 -0
- crackerjack/adapters/refactor/refurb.py +406 -0
- crackerjack/adapters/refactor/skylos.py +494 -0
- crackerjack/adapters/sast/README.md +132 -0
- crackerjack/adapters/sast/__init__.py +32 -0
- crackerjack/adapters/sast/_base.py +201 -0
- crackerjack/adapters/sast/bandit.py +423 -0
- crackerjack/adapters/sast/pyscn.py +405 -0
- crackerjack/adapters/sast/semgrep.py +241 -0
- crackerjack/adapters/security/README.md +111 -0
- crackerjack/adapters/security/__init__.py +17 -0
- crackerjack/adapters/security/gitleaks.py +339 -0
- crackerjack/adapters/type/README.md +52 -0
- crackerjack/adapters/type/__init__.py +12 -0
- crackerjack/adapters/type/pyrefly.py +402 -0
- crackerjack/adapters/type/ty.py +402 -0
- crackerjack/adapters/type/zuban.py +522 -0
- crackerjack/adapters/utility/README.md +51 -0
- crackerjack/adapters/utility/__init__.py +10 -0
- crackerjack/adapters/utility/checks.py +884 -0
- crackerjack/agents/README.md +264 -0
- crackerjack/agents/__init__.py +66 -0
- crackerjack/agents/architect_agent.py +238 -0
- crackerjack/agents/base.py +167 -0
- crackerjack/agents/claude_code_bridge.py +641 -0
- crackerjack/agents/coordinator.py +600 -0
- crackerjack/agents/documentation_agent.py +520 -0
- crackerjack/agents/dry_agent.py +585 -0
- crackerjack/agents/enhanced_coordinator.py +279 -0
- crackerjack/agents/enhanced_proactive_agent.py +185 -0
- crackerjack/agents/error_middleware.py +53 -0
- crackerjack/agents/formatting_agent.py +230 -0
- crackerjack/agents/helpers/__init__.py +9 -0
- crackerjack/agents/helpers/performance/__init__.py +22 -0
- crackerjack/agents/helpers/performance/performance_ast_analyzer.py +357 -0
- crackerjack/agents/helpers/performance/performance_pattern_detector.py +909 -0
- crackerjack/agents/helpers/performance/performance_recommender.py +572 -0
- crackerjack/agents/helpers/refactoring/__init__.py +22 -0
- crackerjack/agents/helpers/refactoring/code_transformer.py +536 -0
- crackerjack/agents/helpers/refactoring/complexity_analyzer.py +344 -0
- crackerjack/agents/helpers/refactoring/dead_code_detector.py +437 -0
- crackerjack/agents/helpers/test_creation/__init__.py +19 -0
- crackerjack/agents/helpers/test_creation/test_ast_analyzer.py +216 -0
- crackerjack/agents/helpers/test_creation/test_coverage_analyzer.py +643 -0
- crackerjack/agents/helpers/test_creation/test_template_generator.py +1031 -0
- crackerjack/agents/import_optimization_agent.py +1181 -0
- crackerjack/agents/performance_agent.py +325 -0
- crackerjack/agents/performance_helpers.py +205 -0
- crackerjack/agents/proactive_agent.py +55 -0
- crackerjack/agents/refactoring_agent.py +511 -0
- crackerjack/agents/refactoring_helpers.py +247 -0
- crackerjack/agents/security_agent.py +793 -0
- crackerjack/agents/semantic_agent.py +479 -0
- crackerjack/agents/semantic_helpers.py +356 -0
- crackerjack/agents/test_creation_agent.py +570 -0
- crackerjack/agents/test_specialist_agent.py +526 -0
- crackerjack/agents/tracker.py +110 -0
- crackerjack/api.py +647 -0
- crackerjack/cli/README.md +394 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/cache_handlers.py +209 -0
- crackerjack/cli/cache_handlers_enhanced.py +680 -0
- crackerjack/cli/facade.py +162 -0
- crackerjack/cli/formatting.py +13 -0
- crackerjack/cli/handlers/__init__.py +85 -0
- crackerjack/cli/handlers/advanced.py +103 -0
- crackerjack/cli/handlers/ai_features.py +62 -0
- crackerjack/cli/handlers/analytics.py +479 -0
- crackerjack/cli/handlers/changelog.py +271 -0
- crackerjack/cli/handlers/config_handlers.py +16 -0
- crackerjack/cli/handlers/coverage.py +84 -0
- crackerjack/cli/handlers/documentation.py +280 -0
- crackerjack/cli/handlers/main_handlers.py +497 -0
- crackerjack/cli/handlers/monitoring.py +371 -0
- crackerjack/cli/handlers.py +700 -0
- crackerjack/cli/interactive.py +488 -0
- crackerjack/cli/options.py +1216 -0
- crackerjack/cli/semantic_handlers.py +292 -0
- crackerjack/cli/utils.py +19 -0
- crackerjack/cli/version.py +19 -0
- crackerjack/code_cleaner.py +1307 -0
- crackerjack/config/README.md +472 -0
- crackerjack/config/__init__.py +275 -0
- crackerjack/config/global_lock_config.py +207 -0
- crackerjack/config/hooks.py +390 -0
- crackerjack/config/loader.py +239 -0
- crackerjack/config/settings.py +141 -0
- crackerjack/config/tool_commands.py +331 -0
- crackerjack/core/README.md +393 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +738 -0
- crackerjack/core/autofix_coordinator.py +282 -0
- crackerjack/core/container.py +105 -0
- crackerjack/core/enhanced_container.py +583 -0
- crackerjack/core/file_lifecycle.py +472 -0
- crackerjack/core/performance.py +244 -0
- crackerjack/core/performance_monitor.py +357 -0
- crackerjack/core/phase_coordinator.py +1227 -0
- crackerjack/core/proactive_workflow.py +267 -0
- crackerjack/core/resource_manager.py +425 -0
- crackerjack/core/retry.py +275 -0
- crackerjack/core/service_watchdog.py +601 -0
- crackerjack/core/session_coordinator.py +239 -0
- crackerjack/core/timeout_manager.py +563 -0
- crackerjack/core/websocket_lifecycle.py +410 -0
- crackerjack/core/workflow/__init__.py +21 -0
- crackerjack/core/workflow/workflow_ai_coordinator.py +863 -0
- crackerjack/core/workflow/workflow_event_orchestrator.py +1107 -0
- crackerjack/core/workflow/workflow_issue_parser.py +714 -0
- crackerjack/core/workflow/workflow_phase_executor.py +1158 -0
- crackerjack/core/workflow/workflow_security_gates.py +400 -0
- crackerjack/core/workflow_orchestrator.py +2243 -0
- crackerjack/data/README.md +11 -0
- crackerjack/data/__init__.py +8 -0
- crackerjack/data/models.py +79 -0
- crackerjack/data/repository.py +210 -0
- crackerjack/decorators/README.md +180 -0
- crackerjack/decorators/__init__.py +35 -0
- crackerjack/decorators/error_handling.py +649 -0
- crackerjack/decorators/error_handling_decorators.py +334 -0
- crackerjack/decorators/helpers.py +58 -0
- crackerjack/decorators/patterns.py +281 -0
- crackerjack/decorators/utils.py +58 -0
- crackerjack/docs/INDEX.md +11 -0
- crackerjack/docs/README.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/README.md +11 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +767 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +1065 -0
- crackerjack/dynamic_config.py +678 -0
- crackerjack/errors.py +378 -0
- crackerjack/events/README.md +11 -0
- crackerjack/events/__init__.py +16 -0
- crackerjack/events/telemetry.py +175 -0
- crackerjack/events/workflow_bus.py +346 -0
- crackerjack/exceptions/README.md +301 -0
- crackerjack/exceptions/__init__.py +5 -0
- crackerjack/exceptions/config.py +4 -0
- crackerjack/exceptions/tool_execution_error.py +245 -0
- crackerjack/executors/README.md +591 -0
- crackerjack/executors/__init__.py +13 -0
- crackerjack/executors/async_hook_executor.py +938 -0
- crackerjack/executors/cached_hook_executor.py +316 -0
- crackerjack/executors/hook_executor.py +1295 -0
- crackerjack/executors/hook_lock_manager.py +708 -0
- crackerjack/executors/individual_hook_executor.py +739 -0
- crackerjack/executors/lsp_aware_hook_executor.py +349 -0
- crackerjack/executors/progress_hook_executor.py +282 -0
- crackerjack/executors/tool_proxy.py +433 -0
- crackerjack/hooks/README.md +485 -0
- crackerjack/hooks/lsp_hook.py +93 -0
- crackerjack/intelligence/README.md +557 -0
- crackerjack/intelligence/__init__.py +37 -0
- crackerjack/intelligence/adaptive_learning.py +693 -0
- crackerjack/intelligence/agent_orchestrator.py +485 -0
- crackerjack/intelligence/agent_registry.py +377 -0
- crackerjack/intelligence/agent_selector.py +439 -0
- crackerjack/intelligence/integration.py +250 -0
- crackerjack/interactive.py +719 -0
- crackerjack/managers/README.md +369 -0
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +585 -0
- crackerjack/managers/publish_manager.py +631 -0
- crackerjack/managers/test_command_builder.py +391 -0
- crackerjack/managers/test_executor.py +474 -0
- crackerjack/managers/test_manager.py +1357 -0
- crackerjack/managers/test_progress.py +187 -0
- crackerjack/mcp/README.md +374 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +352 -0
- crackerjack/mcp/client_runner.py +121 -0
- crackerjack/mcp/context.py +802 -0
- crackerjack/mcp/dashboard.py +657 -0
- crackerjack/mcp/enhanced_progress_monitor.py +493 -0
- crackerjack/mcp/file_monitor.py +394 -0
- crackerjack/mcp/progress_components.py +607 -0
- crackerjack/mcp/progress_monitor.py +1016 -0
- crackerjack/mcp/rate_limiter.py +336 -0
- crackerjack/mcp/server.py +24 -0
- crackerjack/mcp/server_core.py +526 -0
- crackerjack/mcp/service_watchdog.py +505 -0
- crackerjack/mcp/state.py +407 -0
- crackerjack/mcp/task_manager.py +259 -0
- crackerjack/mcp/tools/README.md +27 -0
- crackerjack/mcp/tools/__init__.py +19 -0
- crackerjack/mcp/tools/core_tools.py +469 -0
- crackerjack/mcp/tools/error_analyzer.py +283 -0
- crackerjack/mcp/tools/execution_tools.py +384 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +46 -0
- crackerjack/mcp/tools/intelligence_tools.py +264 -0
- crackerjack/mcp/tools/monitoring_tools.py +628 -0
- crackerjack/mcp/tools/proactive_tools.py +367 -0
- crackerjack/mcp/tools/progress_tools.py +222 -0
- crackerjack/mcp/tools/semantic_tools.py +584 -0
- crackerjack/mcp/tools/utility_tools.py +358 -0
- crackerjack/mcp/tools/workflow_executor.py +699 -0
- crackerjack/mcp/websocket/README.md +31 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +54 -0
- crackerjack/mcp/websocket/endpoints.py +492 -0
- crackerjack/mcp/websocket/event_bridge.py +188 -0
- crackerjack/mcp/websocket/jobs.py +406 -0
- crackerjack/mcp/websocket/monitoring/__init__.py +25 -0
- crackerjack/mcp/websocket/monitoring/api/__init__.py +19 -0
- crackerjack/mcp/websocket/monitoring/api/dependencies.py +141 -0
- crackerjack/mcp/websocket/monitoring/api/heatmap.py +154 -0
- crackerjack/mcp/websocket/monitoring/api/intelligence.py +199 -0
- crackerjack/mcp/websocket/monitoring/api/metrics.py +203 -0
- crackerjack/mcp/websocket/monitoring/api/telemetry.py +101 -0
- crackerjack/mcp/websocket/monitoring/dashboard.py +18 -0
- crackerjack/mcp/websocket/monitoring/factory.py +109 -0
- crackerjack/mcp/websocket/monitoring/filters.py +10 -0
- crackerjack/mcp/websocket/monitoring/metrics.py +64 -0
- crackerjack/mcp/websocket/monitoring/models.py +90 -0
- crackerjack/mcp/websocket/monitoring/utils.py +171 -0
- crackerjack/mcp/websocket/monitoring/websocket_manager.py +78 -0
- crackerjack/mcp/websocket/monitoring/websockets/__init__.py +17 -0
- crackerjack/mcp/websocket/monitoring/websockets/dependencies.py +126 -0
- crackerjack/mcp/websocket/monitoring/websockets/heatmap.py +176 -0
- crackerjack/mcp/websocket/monitoring/websockets/intelligence.py +291 -0
- crackerjack/mcp/websocket/monitoring/websockets/metrics.py +291 -0
- crackerjack/mcp/websocket/monitoring_endpoints.py +21 -0
- crackerjack/mcp/websocket/server.py +174 -0
- crackerjack/mcp/websocket/websocket_handler.py +276 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/README.md +308 -0
- crackerjack/models/__init__.py +40 -0
- crackerjack/models/config.py +730 -0
- crackerjack/models/config_adapter.py +265 -0
- crackerjack/models/protocols.py +1535 -0
- crackerjack/models/pydantic_models.py +320 -0
- crackerjack/models/qa_config.py +145 -0
- crackerjack/models/qa_results.py +134 -0
- crackerjack/models/resource_protocols.py +299 -0
- crackerjack/models/results.py +35 -0
- crackerjack/models/semantic_models.py +258 -0
- crackerjack/models/task.py +173 -0
- crackerjack/models/test_models.py +60 -0
- crackerjack/monitoring/README.md +11 -0
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +405 -0
- crackerjack/monitoring/metrics_collector.py +427 -0
- crackerjack/monitoring/regression_prevention.py +580 -0
- crackerjack/monitoring/websocket_server.py +406 -0
- crackerjack/orchestration/README.md +340 -0
- crackerjack/orchestration/__init__.py +43 -0
- crackerjack/orchestration/advanced_orchestrator.py +894 -0
- crackerjack/orchestration/cache/README.md +312 -0
- crackerjack/orchestration/cache/__init__.py +37 -0
- crackerjack/orchestration/cache/memory_cache.py +338 -0
- crackerjack/orchestration/cache/tool_proxy_cache.py +340 -0
- crackerjack/orchestration/config.py +297 -0
- crackerjack/orchestration/coverage_improvement.py +180 -0
- crackerjack/orchestration/execution_strategies.py +361 -0
- crackerjack/orchestration/hook_orchestrator.py +1398 -0
- crackerjack/orchestration/strategies/README.md +401 -0
- crackerjack/orchestration/strategies/__init__.py +39 -0
- crackerjack/orchestration/strategies/adaptive_strategy.py +630 -0
- crackerjack/orchestration/strategies/parallel_strategy.py +237 -0
- crackerjack/orchestration/strategies/sequential_strategy.py +299 -0
- crackerjack/orchestration/test_progress_streamer.py +647 -0
- crackerjack/plugins/README.md +11 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +254 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +264 -0
- crackerjack/py313.py +191 -0
- crackerjack/security/README.md +11 -0
- crackerjack/security/__init__.py +0 -0
- crackerjack/security/audit.py +197 -0
- crackerjack/services/README.md +374 -0
- crackerjack/services/__init__.py +9 -0
- crackerjack/services/ai/README.md +295 -0
- crackerjack/services/ai/__init__.py +7 -0
- crackerjack/services/ai/advanced_optimizer.py +878 -0
- crackerjack/services/ai/contextual_ai_assistant.py +542 -0
- crackerjack/services/ai/embeddings.py +444 -0
- crackerjack/services/ai/intelligent_commit.py +328 -0
- crackerjack/services/ai/predictive_analytics.py +510 -0
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +617 -0
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +530 -0
- crackerjack/services/cache.py +369 -0
- crackerjack/services/changelog_automation.py +399 -0
- crackerjack/services/command_execution_service.py +305 -0
- crackerjack/services/config_integrity.py +132 -0
- crackerjack/services/config_merge.py +546 -0
- crackerjack/services/config_service.py +198 -0
- crackerjack/services/config_template.py +493 -0
- crackerjack/services/coverage_badge_service.py +173 -0
- crackerjack/services/coverage_ratchet.py +381 -0
- crackerjack/services/debug.py +733 -0
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +622 -0
- crackerjack/services/documentation_generator.py +493 -0
- crackerjack/services/documentation_service.py +704 -0
- crackerjack/services/enhanced_filesystem.py +497 -0
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_filter.py +221 -0
- crackerjack/services/file_hasher.py +149 -0
- crackerjack/services/file_io_service.py +361 -0
- crackerjack/services/file_modifier.py +615 -0
- crackerjack/services/filesystem.py +381 -0
- crackerjack/services/git.py +422 -0
- crackerjack/services/health_metrics.py +615 -0
- crackerjack/services/heatmap_generator.py +744 -0
- crackerjack/services/incremental_executor.py +380 -0
- crackerjack/services/initialization.py +823 -0
- crackerjack/services/input_validator.py +668 -0
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +289 -0
- crackerjack/services/logging.py +228 -0
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +414 -0
- crackerjack/services/metrics.py +587 -0
- crackerjack/services/monitoring/README.md +30 -0
- crackerjack/services/monitoring/__init__.py +9 -0
- crackerjack/services/monitoring/dependency_monitor.py +678 -0
- crackerjack/services/monitoring/error_pattern_analyzer.py +676 -0
- crackerjack/services/monitoring/health_metrics.py +716 -0
- crackerjack/services/monitoring/metrics.py +587 -0
- crackerjack/services/monitoring/performance_benchmarks.py +410 -0
- crackerjack/services/monitoring/performance_cache.py +388 -0
- crackerjack/services/monitoring/performance_monitor.py +569 -0
- crackerjack/services/parallel_executor.py +527 -0
- crackerjack/services/pattern_cache.py +333 -0
- crackerjack/services/pattern_detector.py +478 -0
- crackerjack/services/patterns/__init__.py +142 -0
- crackerjack/services/patterns/agents.py +107 -0
- crackerjack/services/patterns/code/__init__.py +15 -0
- crackerjack/services/patterns/code/detection.py +118 -0
- crackerjack/services/patterns/code/imports.py +107 -0
- crackerjack/services/patterns/code/paths.py +159 -0
- crackerjack/services/patterns/code/performance.py +119 -0
- crackerjack/services/patterns/code/replacement.py +36 -0
- crackerjack/services/patterns/core.py +212 -0
- crackerjack/services/patterns/documentation/__init__.py +14 -0
- crackerjack/services/patterns/documentation/badges_markdown.py +96 -0
- crackerjack/services/patterns/documentation/comments_blocks.py +83 -0
- crackerjack/services/patterns/documentation/docstrings.py +89 -0
- crackerjack/services/patterns/formatting.py +226 -0
- crackerjack/services/patterns/operations.py +339 -0
- crackerjack/services/patterns/security/__init__.py +23 -0
- crackerjack/services/patterns/security/code_injection.py +122 -0
- crackerjack/services/patterns/security/credentials.py +190 -0
- crackerjack/services/patterns/security/path_traversal.py +221 -0
- crackerjack/services/patterns/security/unsafe_operations.py +216 -0
- crackerjack/services/patterns/templates.py +62 -0
- crackerjack/services/patterns/testing/__init__.py +18 -0
- crackerjack/services/patterns/testing/error_patterns.py +107 -0
- crackerjack/services/patterns/testing/pytest_output.py +126 -0
- crackerjack/services/patterns/tool_output/__init__.py +16 -0
- crackerjack/services/patterns/tool_output/bandit.py +72 -0
- crackerjack/services/patterns/tool_output/other.py +97 -0
- crackerjack/services/patterns/tool_output/pyright.py +67 -0
- crackerjack/services/patterns/tool_output/ruff.py +44 -0
- crackerjack/services/patterns/url_sanitization.py +114 -0
- crackerjack/services/patterns/utilities.py +42 -0
- crackerjack/services/patterns/utils.py +339 -0
- crackerjack/services/patterns/validation.py +46 -0
- crackerjack/services/patterns/versioning.py +62 -0
- crackerjack/services/predictive_analytics.py +523 -0
- crackerjack/services/profiler.py +280 -0
- crackerjack/services/quality/README.md +415 -0
- crackerjack/services/quality/__init__.py +11 -0
- crackerjack/services/quality/anomaly_detector.py +392 -0
- crackerjack/services/quality/pattern_cache.py +333 -0
- crackerjack/services/quality/pattern_detector.py +479 -0
- crackerjack/services/quality/qa_orchestrator.py +491 -0
- crackerjack/services/quality/quality_baseline.py +395 -0
- crackerjack/services/quality/quality_baseline_enhanced.py +649 -0
- crackerjack/services/quality/quality_intelligence.py +949 -0
- crackerjack/services/regex_patterns.py +58 -0
- crackerjack/services/regex_utils.py +483 -0
- crackerjack/services/secure_path_utils.py +524 -0
- crackerjack/services/secure_status_formatter.py +450 -0
- crackerjack/services/secure_subprocess.py +635 -0
- crackerjack/services/security.py +239 -0
- crackerjack/services/security_logger.py +495 -0
- crackerjack/services/server_manager.py +411 -0
- crackerjack/services/smart_scheduling.py +167 -0
- crackerjack/services/status_authentication.py +460 -0
- crackerjack/services/status_security_manager.py +315 -0
- crackerjack/services/terminal_utils.py +0 -0
- crackerjack/services/thread_safe_status_collector.py +441 -0
- crackerjack/services/tool_filter.py +368 -0
- crackerjack/services/tool_version_service.py +43 -0
- crackerjack/services/unified_config.py +115 -0
- crackerjack/services/validation_rate_limiter.py +220 -0
- crackerjack/services/vector_store.py +689 -0
- crackerjack/services/version_analyzer.py +461 -0
- crackerjack/services/version_checker.py +223 -0
- crackerjack/services/websocket_resource_limiter.py +438 -0
- crackerjack/services/zuban_lsp_service.py +391 -0
- crackerjack/slash_commands/README.md +11 -0
- crackerjack/slash_commands/__init__.py +59 -0
- crackerjack/slash_commands/init.md +112 -0
- crackerjack/slash_commands/run.md +197 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack/tools/README.md +11 -0
- crackerjack/tools/__init__.py +30 -0
- crackerjack/tools/_git_utils.py +105 -0
- crackerjack/tools/check_added_large_files.py +139 -0
- crackerjack/tools/check_ast.py +105 -0
- crackerjack/tools/check_json.py +103 -0
- crackerjack/tools/check_jsonschema.py +297 -0
- crackerjack/tools/check_toml.py +103 -0
- crackerjack/tools/check_yaml.py +110 -0
- crackerjack/tools/codespell_wrapper.py +72 -0
- crackerjack/tools/end_of_file_fixer.py +202 -0
- crackerjack/tools/format_json.py +128 -0
- crackerjack/tools/mdformat_wrapper.py +114 -0
- crackerjack/tools/trailing_whitespace.py +198 -0
- crackerjack/tools/validate_input_validator_patterns.py +236 -0
- crackerjack/tools/validate_regex_patterns.py +188 -0
- crackerjack/ui/README.md +11 -0
- crackerjack/ui/__init__.py +1 -0
- crackerjack/ui/dashboard_renderer.py +28 -0
- crackerjack/ui/templates/README.md +11 -0
- crackerjack/utils/console_utils.py +13 -0
- crackerjack/utils/dependency_guard.py +230 -0
- crackerjack/utils/retry_utils.py +275 -0
- crackerjack/workflows/README.md +590 -0
- crackerjack/workflows/__init__.py +46 -0
- crackerjack/workflows/actions.py +811 -0
- crackerjack/workflows/auto_fix.py +444 -0
- crackerjack/workflows/container_builder.py +499 -0
- crackerjack/workflows/definitions.py +443 -0
- crackerjack/workflows/engine.py +177 -0
- crackerjack/workflows/event_bridge.py +242 -0
- crackerjack-0.45.2.dist-info/METADATA +1678 -0
- crackerjack-0.45.2.dist-info/RECORD +478 -0
- {crackerjack-0.18.2.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
- crackerjack-0.45.2.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -14
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/.pre-commit-config.yaml +0 -91
- crackerjack/.pytest_cache/.gitignore +0 -2
- crackerjack/.pytest_cache/CACHEDIR.TAG +0 -4
- crackerjack/.pytest_cache/README.md +0 -8
- crackerjack/.pytest_cache/v/cache/nodeids +0 -1
- crackerjack/.pytest_cache/v/cache/stepwise +0 -1
- crackerjack/.ruff_cache/.gitignore +0 -1
- crackerjack/.ruff_cache/0.1.11/3256171999636029978 +0 -0
- crackerjack/.ruff_cache/0.1.14/602324811142551221 +0 -0
- crackerjack/.ruff_cache/0.1.4/10355199064880463147 +0 -0
- crackerjack/.ruff_cache/0.1.6/15140459877605758699 +0 -0
- crackerjack/.ruff_cache/0.1.7/1790508110482614856 +0 -0
- crackerjack/.ruff_cache/0.1.9/17041001205004563469 +0 -0
- crackerjack/.ruff_cache/0.11.2/4070660268492669020 +0 -0
- crackerjack/.ruff_cache/0.11.3/9818742842212983150 +0 -0
- crackerjack/.ruff_cache/0.11.4/9818742842212983150 +0 -0
- crackerjack/.ruff_cache/0.11.6/3557596832929915217 +0 -0
- crackerjack/.ruff_cache/0.11.7/10386934055395314831 +0 -0
- crackerjack/.ruff_cache/0.11.7/3557596832929915217 +0 -0
- crackerjack/.ruff_cache/0.11.8/530407680854991027 +0 -0
- crackerjack/.ruff_cache/0.2.0/10047773857155985907 +0 -0
- crackerjack/.ruff_cache/0.2.1/8522267973936635051 +0 -0
- crackerjack/.ruff_cache/0.2.2/18053836298936336950 +0 -0
- crackerjack/.ruff_cache/0.3.0/12548816621480535786 +0 -0
- crackerjack/.ruff_cache/0.3.3/11081883392474770722 +0 -0
- crackerjack/.ruff_cache/0.3.4/676973378459347183 +0 -0
- crackerjack/.ruff_cache/0.3.5/16311176246009842383 +0 -0
- crackerjack/.ruff_cache/0.5.7/1493622539551733492 +0 -0
- crackerjack/.ruff_cache/0.5.7/6231957614044513175 +0 -0
- crackerjack/.ruff_cache/0.5.7/9932762556785938009 +0 -0
- crackerjack/.ruff_cache/0.6.0/11982804814124138945 +0 -0
- crackerjack/.ruff_cache/0.6.0/12055761203849489982 +0 -0
- crackerjack/.ruff_cache/0.6.2/1206147804896221174 +0 -0
- crackerjack/.ruff_cache/0.6.4/1206147804896221174 +0 -0
- crackerjack/.ruff_cache/0.6.5/1206147804896221174 +0 -0
- crackerjack/.ruff_cache/0.6.7/3657366982708166874 +0 -0
- crackerjack/.ruff_cache/0.6.9/285614542852677309 +0 -0
- crackerjack/.ruff_cache/0.7.1/1024065805990144819 +0 -0
- crackerjack/.ruff_cache/0.7.1/285614542852677309 +0 -0
- crackerjack/.ruff_cache/0.7.3/16061516852537040135 +0 -0
- crackerjack/.ruff_cache/0.8.4/16354268377385700367 +0 -0
- crackerjack/.ruff_cache/0.9.10/12813592349865671909 +0 -0
- crackerjack/.ruff_cache/0.9.10/923908772239632759 +0 -0
- crackerjack/.ruff_cache/0.9.3/13948373885254993391 +0 -0
- crackerjack/.ruff_cache/0.9.9/12813592349865671909 +0 -0
- crackerjack/.ruff_cache/0.9.9/8843823720003377982 +0 -0
- crackerjack/.ruff_cache/CACHEDIR.TAG +0 -1
- crackerjack/crackerjack.py +0 -855
- crackerjack/pyproject.toml +0 -214
- crackerjack-0.18.2.dist-info/METADATA +0 -420
- crackerjack-0.18.2.dist-info/RECORD +0 -59
- crackerjack-0.18.2.dist-info/entry_points.txt +0 -4
- {crackerjack-0.18.2.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Automatic changelog generation and updates service."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from acb.console import Console
|
|
8
|
+
from acb.depends import Inject, depends
|
|
9
|
+
|
|
10
|
+
from crackerjack.models.protocols import GitServiceProtocol
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChangelogEntry:
|
|
14
|
+
"""Represents a single changelog entry."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
entry_type: str,
|
|
19
|
+
description: str,
|
|
20
|
+
commit_hash: str = "",
|
|
21
|
+
breaking_change: bool = False,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.type = entry_type
|
|
24
|
+
self.description = description
|
|
25
|
+
self.commit_hash = commit_hash
|
|
26
|
+
self.breaking_change = breaking_change
|
|
27
|
+
|
|
28
|
+
def to_markdown(self) -> str:
|
|
29
|
+
"""Convert entry to markdown format."""
|
|
30
|
+
prefix = "**BREAKING:** " if self.breaking_change else ""
|
|
31
|
+
return f"- {prefix}{self.description}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ChangelogGenerator:
|
|
35
|
+
"""Generate and update changelogs based on git commits."""
|
|
36
|
+
|
|
37
|
+
@depends.inject
|
|
38
|
+
def __init__(
|
|
39
|
+
self, console: Inject[Console], git_service: Inject[GitServiceProtocol]
|
|
40
|
+
) -> None:
|
|
41
|
+
self.console = console
|
|
42
|
+
self.git = git_service
|
|
43
|
+
|
|
44
|
+
# Conventional commit type mappings to changelog sections
|
|
45
|
+
self.type_mappings = {
|
|
46
|
+
"feat": "Added",
|
|
47
|
+
"fix": "Fixed",
|
|
48
|
+
"docs": "Documentation",
|
|
49
|
+
"style": "Changed",
|
|
50
|
+
"refactor": "Changed",
|
|
51
|
+
"test": "Testing",
|
|
52
|
+
"chore": "Internal",
|
|
53
|
+
"perf": "Performance",
|
|
54
|
+
"build": "Build",
|
|
55
|
+
"ci": "CI/CD",
|
|
56
|
+
"revert": "Reverted",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Regex patterns for parsing commit messages
|
|
60
|
+
self.conventional_commit_pattern = re.compile( # REGEX OK: conventional commit parsing
|
|
61
|
+
r"^(?P<type>\w+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s*(?P<description>.+)$"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self.breaking_change_pattern = (
|
|
65
|
+
re.compile( # REGEX OK: breaking change detection
|
|
66
|
+
r"BREAKING\s*CHANGE[:]\s*(.+)", re.IGNORECASE | re.MULTILINE
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def parse_commit_message(
|
|
71
|
+
self, commit_message: str, commit_hash: str = ""
|
|
72
|
+
) -> ChangelogEntry | None:
|
|
73
|
+
"""Parse a commit message into a changelog entry."""
|
|
74
|
+
# Split commit message into header and body
|
|
75
|
+
lines = commit_message.strip().split("\n")
|
|
76
|
+
header = lines[0].strip()
|
|
77
|
+
body = "\n".join(lines[1:]).strip() if len(lines) > 1 else ""
|
|
78
|
+
|
|
79
|
+
# Try to match conventional commit format
|
|
80
|
+
match = self.conventional_commit_pattern.match(header)
|
|
81
|
+
if not match:
|
|
82
|
+
# Fallback for non-conventional commits
|
|
83
|
+
return self._parse_non_conventional_commit(header, body, commit_hash)
|
|
84
|
+
|
|
85
|
+
commit_type = match.group("type").lower()
|
|
86
|
+
scope = match.group("scope") or ""
|
|
87
|
+
breaking_marker = match.group("breaking") == "!"
|
|
88
|
+
description = match.group("description").strip()
|
|
89
|
+
|
|
90
|
+
# Check for breaking changes in body
|
|
91
|
+
breaking_in_body = bool(self.breaking_change_pattern.search(body))
|
|
92
|
+
breaking_change = breaking_marker or breaking_in_body
|
|
93
|
+
|
|
94
|
+
# Map commit type to changelog section
|
|
95
|
+
changelog_section = self.type_mappings.get(commit_type, "Changed")
|
|
96
|
+
|
|
97
|
+
# Format description
|
|
98
|
+
formatted_description = self._format_description(
|
|
99
|
+
description, scope, commit_type
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return ChangelogEntry(
|
|
103
|
+
entry_type=changelog_section,
|
|
104
|
+
description=formatted_description,
|
|
105
|
+
commit_hash=commit_hash,
|
|
106
|
+
breaking_change=breaking_change,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def _parse_non_conventional_commit(
|
|
110
|
+
self, header: str, body: str, commit_hash: str
|
|
111
|
+
) -> ChangelogEntry | None:
|
|
112
|
+
"""Parse non-conventional commit messages."""
|
|
113
|
+
# Simple heuristics for non-conventional commits
|
|
114
|
+
header_lower = header.lower()
|
|
115
|
+
|
|
116
|
+
if any(
|
|
117
|
+
keyword in header_lower for keyword in ("add", "new", "create", "implement")
|
|
118
|
+
):
|
|
119
|
+
entry_type = "Added"
|
|
120
|
+
elif any(
|
|
121
|
+
keyword in header_lower for keyword in ("fix", "bug", "resolve", "correct")
|
|
122
|
+
):
|
|
123
|
+
entry_type = "Fixed"
|
|
124
|
+
elif any(
|
|
125
|
+
keyword in header_lower
|
|
126
|
+
for keyword in ("update", "change", "modify", "improve")
|
|
127
|
+
):
|
|
128
|
+
entry_type = "Changed"
|
|
129
|
+
elif any(keyword in header_lower for keyword in ("remove", "delete", "drop")):
|
|
130
|
+
entry_type = "Removed"
|
|
131
|
+
elif any(keyword in header_lower for keyword in ("doc", "readme", "comment")):
|
|
132
|
+
entry_type = "Documentation"
|
|
133
|
+
else:
|
|
134
|
+
entry_type = "Changed"
|
|
135
|
+
|
|
136
|
+
# Check for breaking changes
|
|
137
|
+
breaking_change = bool(self.breaking_change_pattern.search(body))
|
|
138
|
+
|
|
139
|
+
return ChangelogEntry(
|
|
140
|
+
entry_type=entry_type,
|
|
141
|
+
description=header,
|
|
142
|
+
commit_hash=commit_hash,
|
|
143
|
+
breaking_change=breaking_change,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _format_description(
|
|
147
|
+
self, description: str, scope: str, commit_type: str
|
|
148
|
+
) -> str:
|
|
149
|
+
"""Format the changelog description."""
|
|
150
|
+
# Capitalize first letter
|
|
151
|
+
description = description[0].upper() + description[1:] if description else ""
|
|
152
|
+
|
|
153
|
+
# Add scope context if present
|
|
154
|
+
if scope:
|
|
155
|
+
# Only add scope if it's not already mentioned in description
|
|
156
|
+
if scope.lower() not in description.lower():
|
|
157
|
+
description = f"{scope}: {description}"
|
|
158
|
+
|
|
159
|
+
return description
|
|
160
|
+
|
|
161
|
+
def generate_changelog_entries(
|
|
162
|
+
self, since_version: str | None = None, target_file: Path | None = None
|
|
163
|
+
) -> dict[str, list[ChangelogEntry]]:
|
|
164
|
+
"""Generate changelog entries from git commits."""
|
|
165
|
+
try:
|
|
166
|
+
# Get git commits
|
|
167
|
+
git_result = self._get_git_commits(since_version)
|
|
168
|
+
if not git_result:
|
|
169
|
+
return {}
|
|
170
|
+
|
|
171
|
+
# Parse commits into entries
|
|
172
|
+
return self._parse_commits_to_entries(git_result)
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
self.console.print(f"[red]❌[/red] Error generating changelog entries: {e}")
|
|
176
|
+
return {}
|
|
177
|
+
|
|
178
|
+
def _get_git_commits(self, since_version: str | None = None) -> str | None:
|
|
179
|
+
"""Get git commit log output."""
|
|
180
|
+
# Build git command
|
|
181
|
+
git_command = self._build_git_log_command(since_version)
|
|
182
|
+
|
|
183
|
+
# Execute git command
|
|
184
|
+
result = self.git._run_git_command(git_command)
|
|
185
|
+
if result.returncode != 0:
|
|
186
|
+
self.console.print(
|
|
187
|
+
f"[yellow]⚠️[/yellow] Failed to get git log: {result.stderr}"
|
|
188
|
+
)
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
return result.stdout
|
|
192
|
+
|
|
193
|
+
def _build_git_log_command(self, since_version: str | None = None) -> list[str]:
|
|
194
|
+
"""Build the git log command based on parameters."""
|
|
195
|
+
if since_version:
|
|
196
|
+
return [
|
|
197
|
+
"log",
|
|
198
|
+
f"{since_version}..HEAD",
|
|
199
|
+
"--oneline",
|
|
200
|
+
"--no-merges",
|
|
201
|
+
]
|
|
202
|
+
# Get commits since last release tag or last 50 commits
|
|
203
|
+
return ["log", "-50", "--oneline", "--no-merges"]
|
|
204
|
+
|
|
205
|
+
def _parse_commits_to_entries(
|
|
206
|
+
self, git_output: str
|
|
207
|
+
) -> dict[str, list[ChangelogEntry]]:
|
|
208
|
+
"""Parse git commit output into changelog entries."""
|
|
209
|
+
entries_by_type: dict[str, list[ChangelogEntry]] = {}
|
|
210
|
+
|
|
211
|
+
for line in git_output.strip().split("\n"):
|
|
212
|
+
if not line.strip():
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
entry = self._process_commit_line(line)
|
|
216
|
+
if entry:
|
|
217
|
+
self._add_entry_to_collection(entry, entries_by_type)
|
|
218
|
+
|
|
219
|
+
return entries_by_type
|
|
220
|
+
|
|
221
|
+
def _process_commit_line(self, line: str) -> ChangelogEntry | None:
|
|
222
|
+
"""Process a single commit line into a changelog entry."""
|
|
223
|
+
# Parse commit hash and message
|
|
224
|
+
parts = line.strip().split(" ", 1)
|
|
225
|
+
if len(parts) < 2:
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
commit_hash = parts[0]
|
|
229
|
+
commit_message = parts[1]
|
|
230
|
+
|
|
231
|
+
# Get full commit message
|
|
232
|
+
full_message = self._get_full_commit_message(commit_hash, commit_message)
|
|
233
|
+
|
|
234
|
+
# Parse into changelog entry
|
|
235
|
+
return self.parse_commit_message(full_message, commit_hash)
|
|
236
|
+
|
|
237
|
+
def _get_full_commit_message(self, commit_hash: str, fallback_message: str) -> str:
|
|
238
|
+
"""Get the full commit message for detailed parsing."""
|
|
239
|
+
full_commit_result = self.git._run_git_command(
|
|
240
|
+
["show", "--format=%B", "--no-patch", commit_hash]
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
full_commit_result.stdout
|
|
245
|
+
if full_commit_result.returncode == 0
|
|
246
|
+
else fallback_message
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def _add_entry_to_collection(
|
|
250
|
+
self, entry: ChangelogEntry, entries_by_type: dict[str, list[ChangelogEntry]]
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Add a changelog entry to the appropriate type collection."""
|
|
253
|
+
if entry.type not in entries_by_type:
|
|
254
|
+
entries_by_type[entry.type] = []
|
|
255
|
+
entries_by_type[entry.type].append(entry)
|
|
256
|
+
|
|
257
|
+
def update_changelog(
|
|
258
|
+
self,
|
|
259
|
+
changelog_path: Path,
|
|
260
|
+
new_version: str,
|
|
261
|
+
entries_by_type: dict[str, list[ChangelogEntry]] | None = None,
|
|
262
|
+
) -> bool:
|
|
263
|
+
"""Update the changelog file with new entries."""
|
|
264
|
+
try:
|
|
265
|
+
if entries_by_type is None:
|
|
266
|
+
entries_by_type = self.generate_changelog_entries()
|
|
267
|
+
|
|
268
|
+
if not entries_by_type:
|
|
269
|
+
self.console.print("[yellow]ℹ️[/yellow] No new changelog entries to add")
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
# Read existing changelog
|
|
273
|
+
existing_content = ""
|
|
274
|
+
if changelog_path.exists():
|
|
275
|
+
existing_content = changelog_path.read_text(encoding="utf-8")
|
|
276
|
+
|
|
277
|
+
# Generate new section
|
|
278
|
+
new_section = self._generate_changelog_section(new_version, entries_by_type)
|
|
279
|
+
|
|
280
|
+
# Insert new section
|
|
281
|
+
updated_content = self._insert_new_section(existing_content, new_section)
|
|
282
|
+
|
|
283
|
+
# Write updated changelog
|
|
284
|
+
changelog_path.write_text(updated_content, encoding="utf-8")
|
|
285
|
+
|
|
286
|
+
self.console.print(
|
|
287
|
+
f"[green]✅[/green] Updated {changelog_path.name} with {len(entries_by_type)} sections"
|
|
288
|
+
)
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
self.console.print(f"[red]❌[/red] Failed to update changelog: {e}")
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
def _generate_changelog_section(
|
|
296
|
+
self, version: str, entries_by_type: dict[str, list[ChangelogEntry]]
|
|
297
|
+
) -> str:
|
|
298
|
+
"""Generate a new changelog section."""
|
|
299
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
300
|
+
section_lines = [f"## [{version}] - {today}", ""]
|
|
301
|
+
|
|
302
|
+
# Order sections by importance
|
|
303
|
+
section_order = [
|
|
304
|
+
"Added",
|
|
305
|
+
"Changed",
|
|
306
|
+
"Fixed",
|
|
307
|
+
"Removed",
|
|
308
|
+
"Performance",
|
|
309
|
+
"Security",
|
|
310
|
+
"Deprecated",
|
|
311
|
+
"Documentation",
|
|
312
|
+
"Testing",
|
|
313
|
+
"Build",
|
|
314
|
+
"CI/CD",
|
|
315
|
+
"Internal",
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
for section_name in section_order:
|
|
319
|
+
if section_name in entries_by_type:
|
|
320
|
+
entries = entries_by_type[section_name]
|
|
321
|
+
if entries:
|
|
322
|
+
section_lines.extend((f"### {section_name}", ""))
|
|
323
|
+
|
|
324
|
+
# Sort entries: breaking changes first, then alphabetically
|
|
325
|
+
entries.sort(
|
|
326
|
+
key=lambda e: (not e.breaking_change, e.description.lower())
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
for entry in entries:
|
|
330
|
+
section_lines.append(entry.to_markdown())
|
|
331
|
+
section_lines.append("")
|
|
332
|
+
|
|
333
|
+
return "\n".join(section_lines)
|
|
334
|
+
|
|
335
|
+
def _insert_new_section(self, existing_content: str, new_section: str) -> str:
|
|
336
|
+
"""Insert new section into existing changelog content."""
|
|
337
|
+
if not existing_content.strip():
|
|
338
|
+
# Create new changelog
|
|
339
|
+
header = """# Changelog
|
|
340
|
+
|
|
341
|
+
All notable changes to this project will be documented in this file.
|
|
342
|
+
|
|
343
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
344
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
345
|
+
|
|
346
|
+
"""
|
|
347
|
+
return header + new_section
|
|
348
|
+
|
|
349
|
+
# Find where to insert (after header, before first existing version)
|
|
350
|
+
lines = existing_content.split("\n")
|
|
351
|
+
insert_index = 0
|
|
352
|
+
|
|
353
|
+
# Find the insertion point (after the header, before first version)
|
|
354
|
+
for i, line in enumerate(lines):
|
|
355
|
+
if line.strip().startswith("## ["):
|
|
356
|
+
insert_index = i
|
|
357
|
+
break
|
|
358
|
+
else:
|
|
359
|
+
# No existing version sections found, insert at end
|
|
360
|
+
insert_index = len(lines)
|
|
361
|
+
|
|
362
|
+
# Insert new section
|
|
363
|
+
new_lines = (
|
|
364
|
+
lines[:insert_index] + new_section.split("\n") + lines[insert_index:]
|
|
365
|
+
)
|
|
366
|
+
return "\n".join(new_lines)
|
|
367
|
+
|
|
368
|
+
def generate_changelog_from_commits(
|
|
369
|
+
self, changelog_path: Path, version: str, since_version: str | None = None
|
|
370
|
+
) -> bool:
|
|
371
|
+
"""Generate and update changelog from git commits."""
|
|
372
|
+
self.console.print(
|
|
373
|
+
f"[cyan]📝[/cyan] Generating changelog entries for version {version}..."
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
entries = self.generate_changelog_entries(since_version)
|
|
377
|
+
if not entries:
|
|
378
|
+
self.console.print("[yellow]ℹ️[/yellow] No changelog entries generated")
|
|
379
|
+
return True
|
|
380
|
+
|
|
381
|
+
# Display preview
|
|
382
|
+
self._display_changelog_preview(entries)
|
|
383
|
+
|
|
384
|
+
return self.update_changelog(changelog_path, version, entries)
|
|
385
|
+
|
|
386
|
+
def _display_changelog_preview(
|
|
387
|
+
self, entries_by_type: dict[str, list[ChangelogEntry]]
|
|
388
|
+
) -> None:
|
|
389
|
+
"""Display a preview of generated changelog entries."""
|
|
390
|
+
self.console.print("[cyan]📋[/cyan] Changelog preview:")
|
|
391
|
+
|
|
392
|
+
for section_name, entries in entries_by_type.items():
|
|
393
|
+
if entries:
|
|
394
|
+
self.console.print(f"[bold]{section_name}:[/bold]")
|
|
395
|
+
for entry in entries[:3]: # Show first 3 entries
|
|
396
|
+
self.console.print(f" {entry.to_markdown()}")
|
|
397
|
+
if len(entries) > 3:
|
|
398
|
+
self.console.print(f" [dim]... and {len(entries) - 3} more[/dim]")
|
|
399
|
+
self.console.print()
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Unified command execution service with consistent error handling and timeouts."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CommandExecutionService:
|
|
11
|
+
"""Unified command execution with consistent error handling, timeouts, and caching."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, default_timeout: int = 30):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the command execution service.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
default_timeout: Default timeout in seconds for commands
|
|
19
|
+
"""
|
|
20
|
+
self.default_timeout = default_timeout
|
|
21
|
+
|
|
22
|
+
async def run_command(
|
|
23
|
+
self,
|
|
24
|
+
cmd: str | list[str],
|
|
25
|
+
cwd: str | Path | None = None,
|
|
26
|
+
env: dict[str, str] | None = None,
|
|
27
|
+
timeout: int | None = None,
|
|
28
|
+
capture_output: bool = True,
|
|
29
|
+
check: bool = True,
|
|
30
|
+
) -> subprocess.CompletedProcess:
|
|
31
|
+
"""
|
|
32
|
+
Run a command with timeout and error handling.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
cmd: Command to run as a string or list of strings
|
|
36
|
+
cwd: Working directory to run the command in
|
|
37
|
+
env: Environment variables to use
|
|
38
|
+
timeout: Timeout in seconds (uses default if not specified)
|
|
39
|
+
capture_output: Whether to capture stdout/stderr
|
|
40
|
+
check: If True, raises exception on non-zero exit code
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
CompletedProcess instance with results
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
subprocess.TimeoutExpired: If command times out
|
|
47
|
+
subprocess.CalledProcessError: If command fails and check=True
|
|
48
|
+
"""
|
|
49
|
+
timeout = timeout or self.default_timeout
|
|
50
|
+
str_cmd = " ".join(cmd) if isinstance(cmd, list) else cmd
|
|
51
|
+
logger.debug(f"Executing command: {str_cmd}")
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
self._get_executable(cmd)
|
|
55
|
+
process = await self._create_subprocess(cmd, capture_output, cwd, env)
|
|
56
|
+
return await self._execute_process(
|
|
57
|
+
process, cmd, str_cmd, timeout, check, capture_output
|
|
58
|
+
)
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
logger.error(f"Command not found: {self._get_executable(cmd)}")
|
|
61
|
+
raise
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"Command execution failed: {str_cmd}, Error: {e}")
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
def _get_executable(self, cmd: str | list[str]) -> str:
|
|
67
|
+
"""Extract executable name from command."""
|
|
68
|
+
if isinstance(cmd, list):
|
|
69
|
+
return cmd[0] if cmd else ""
|
|
70
|
+
else:
|
|
71
|
+
parts = cmd.split()
|
|
72
|
+
return parts[0] if parts else ""
|
|
73
|
+
|
|
74
|
+
async def _create_subprocess(
|
|
75
|
+
self,
|
|
76
|
+
cmd: str | list[str],
|
|
77
|
+
capture_output: bool,
|
|
78
|
+
cwd: str | Path | None,
|
|
79
|
+
env: dict[str, str] | None,
|
|
80
|
+
) -> asyncio.subprocess.Process:
|
|
81
|
+
"""Create subprocess with proper configuration."""
|
|
82
|
+
return await asyncio.create_subprocess_exec(
|
|
83
|
+
*(cmd if isinstance(cmd, list) else cmd.split()),
|
|
84
|
+
stdout=asyncio.subprocess.PIPE if capture_output else None,
|
|
85
|
+
stderr=asyncio.subprocess.PIPE if capture_output else None,
|
|
86
|
+
cwd=cwd,
|
|
87
|
+
env=env,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def _execute_process(
|
|
91
|
+
self,
|
|
92
|
+
process: asyncio.subprocess.Process,
|
|
93
|
+
cmd: str | list[str],
|
|
94
|
+
str_cmd: str,
|
|
95
|
+
timeout: int,
|
|
96
|
+
check: bool,
|
|
97
|
+
capture_output: bool,
|
|
98
|
+
) -> subprocess.CompletedProcess:
|
|
99
|
+
"""Execute the process and handle the result."""
|
|
100
|
+
try:
|
|
101
|
+
stdout, stderr = await asyncio.wait_for(
|
|
102
|
+
process.communicate(), timeout=timeout
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return_code = process.returncode if process.returncode is not None else 0
|
|
106
|
+
completed_process = subprocess.CompletedProcess(
|
|
107
|
+
cmd,
|
|
108
|
+
return_code,
|
|
109
|
+
stdout.decode() if stdout else None,
|
|
110
|
+
stderr.decode() if stderr else None,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if check and process.returncode != 0:
|
|
114
|
+
logger.error(
|
|
115
|
+
f"Command failed with exit code {process.returncode}: {str_cmd}"
|
|
116
|
+
)
|
|
117
|
+
raise subprocess.CalledProcessError(
|
|
118
|
+
return_code,
|
|
119
|
+
cmd,
|
|
120
|
+
output=completed_process.stdout,
|
|
121
|
+
stderr=completed_process.stderr,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
logger.debug(f"Command completed successfully: {str_cmd}")
|
|
125
|
+
return completed_process
|
|
126
|
+
|
|
127
|
+
except TimeoutError:
|
|
128
|
+
# Handle timeout
|
|
129
|
+
process.kill()
|
|
130
|
+
await process.wait() # Ensure process is cleaned up
|
|
131
|
+
logger.error(f"Command timed out after {timeout}s: {str_cmd}")
|
|
132
|
+
raise subprocess.TimeoutExpired(cmd, timeout)
|
|
133
|
+
|
|
134
|
+
def run_command_sync(
|
|
135
|
+
self,
|
|
136
|
+
cmd: str | list[str],
|
|
137
|
+
cwd: str | Path | None = None,
|
|
138
|
+
env: dict[str, str] | None = None,
|
|
139
|
+
timeout: int | None = None,
|
|
140
|
+
capture_output: bool = True,
|
|
141
|
+
check: bool = True,
|
|
142
|
+
) -> subprocess.CompletedProcess:
|
|
143
|
+
"""
|
|
144
|
+
Synchronous version of run_command.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
cmd: Command to run as a string or list of strings
|
|
148
|
+
cwd: Working directory to run the command in
|
|
149
|
+
env: Environment variables to use
|
|
150
|
+
timeout: Timeout in seconds (uses default if not specified)
|
|
151
|
+
capture_output: Whether to capture stdout/stderr
|
|
152
|
+
check: If True, raises exception on non-zero exit code
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
CompletedProcess instance with results
|
|
156
|
+
"""
|
|
157
|
+
timeout = timeout or self.default_timeout
|
|
158
|
+
str_cmd = " ".join(cmd) if isinstance(cmd, list) else cmd
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
logger.debug(f"Executing sync command: {str_cmd}")
|
|
162
|
+
|
|
163
|
+
result = subprocess.run(
|
|
164
|
+
cmd,
|
|
165
|
+
cwd=cwd,
|
|
166
|
+
env=env,
|
|
167
|
+
timeout=timeout,
|
|
168
|
+
capture_output=capture_output,
|
|
169
|
+
text=True, # Return strings instead of bytes
|
|
170
|
+
check=check,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
logger.debug(f"Sync command completed: {str_cmd}")
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
except subprocess.TimeoutExpired:
|
|
177
|
+
logger.error(f"Sync command timed out after {timeout}s: {str_cmd}")
|
|
178
|
+
raise
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.error(f"Sync command execution failed: {str_cmd}, Error: {e}")
|
|
181
|
+
raise
|
|
182
|
+
|
|
183
|
+
async def run_multiple_commands(
|
|
184
|
+
self,
|
|
185
|
+
commands: list[str | list[str]],
|
|
186
|
+
cwd: str | Path | None = None,
|
|
187
|
+
env: dict[str, str] | None = None,
|
|
188
|
+
timeout: int | None = None,
|
|
189
|
+
parallel: bool = False,
|
|
190
|
+
) -> list[subprocess.CompletedProcess]:
|
|
191
|
+
"""
|
|
192
|
+
Run multiple commands sequentially or in parallel.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
commands: List of commands to run
|
|
196
|
+
cwd: Working directory to run the commands in
|
|
197
|
+
env: Environment variables to use
|
|
198
|
+
timeout: Timeout in seconds for each command
|
|
199
|
+
parallel: If True, run commands in parallel; otherwise sequentially
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
List of CompletedProcess instances with results
|
|
203
|
+
"""
|
|
204
|
+
results = []
|
|
205
|
+
|
|
206
|
+
if parallel:
|
|
207
|
+
# Run commands in parallel
|
|
208
|
+
tasks = [
|
|
209
|
+
self.run_command(cmd, cwd=cwd, env=env, timeout=timeout)
|
|
210
|
+
for cmd in commands
|
|
211
|
+
]
|
|
212
|
+
results = await asyncio.gather(*tasks)
|
|
213
|
+
else:
|
|
214
|
+
# Run commands sequentially
|
|
215
|
+
for cmd in commands:
|
|
216
|
+
result = await self.run_command(cmd, cwd=cwd, env=env, timeout=timeout)
|
|
217
|
+
results.append(result)
|
|
218
|
+
|
|
219
|
+
return results
|
|
220
|
+
|
|
221
|
+
async def command_exists(self, command: str) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Check if a command exists in the system.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
command: Command to check
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
True if command exists, False otherwise
|
|
230
|
+
"""
|
|
231
|
+
try:
|
|
232
|
+
# Try running 'which' on Unix-like systems or 'where' on Windows
|
|
233
|
+
import platform
|
|
234
|
+
|
|
235
|
+
if platform.system() == "Windows":
|
|
236
|
+
check_cmd = ["where", command]
|
|
237
|
+
else:
|
|
238
|
+
check_cmd = ["which", command]
|
|
239
|
+
|
|
240
|
+
result = await self.run_command(
|
|
241
|
+
check_cmd,
|
|
242
|
+
capture_output=True,
|
|
243
|
+
check=False, # Don't raise on non-zero exit (which returns 1 if not found)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return result.returncode == 0
|
|
247
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
248
|
+
# If the check command itself fails, command likely doesn't exist
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
async def run_command_with_retries(
|
|
252
|
+
self,
|
|
253
|
+
cmd: str | list[str],
|
|
254
|
+
max_retries: int = 3,
|
|
255
|
+
cwd: str | Path | None = None,
|
|
256
|
+
env: dict[str, str] | None = None,
|
|
257
|
+
timeout: int | None = None,
|
|
258
|
+
capture_output: bool = True,
|
|
259
|
+
check: bool = True,
|
|
260
|
+
backoff_factor: float = 1.0,
|
|
261
|
+
) -> subprocess.CompletedProcess:
|
|
262
|
+
"""
|
|
263
|
+
Run a command with retry logic.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
cmd: Command to run
|
|
267
|
+
max_retries: Maximum number of retry attempts
|
|
268
|
+
cwd: Working directory
|
|
269
|
+
env: Environment variables
|
|
270
|
+
timeout: Timeout for each attempt
|
|
271
|
+
capture_output: Whether to capture output
|
|
272
|
+
check: Whether to check return code
|
|
273
|
+
backoff_factor: Factor by which to multiply wait time between retries
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
CompletedProcess instance with results
|
|
277
|
+
"""
|
|
278
|
+
for attempt in range(
|
|
279
|
+
max_retries + 1
|
|
280
|
+
): # +1 because first attempt doesn't count as retry
|
|
281
|
+
try:
|
|
282
|
+
return await self.run_command(
|
|
283
|
+
cmd,
|
|
284
|
+
cwd=cwd,
|
|
285
|
+
env=env,
|
|
286
|
+
timeout=timeout,
|
|
287
|
+
capture_output=capture_output,
|
|
288
|
+
check=check,
|
|
289
|
+
)
|
|
290
|
+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError) as e:
|
|
291
|
+
if attempt == max_retries:
|
|
292
|
+
# Last attempt, re-raise the exception
|
|
293
|
+
logger.error(f"Command failed after {max_retries} retries: {cmd}")
|
|
294
|
+
raise
|
|
295
|
+
else:
|
|
296
|
+
# Wait before retrying with exponential backoff
|
|
297
|
+
wait_time = backoff_factor * (2**attempt)
|
|
298
|
+
logger.warning(
|
|
299
|
+
f"Command failed on attempt {attempt + 1}, "
|
|
300
|
+
f"retrying in {wait_time}s: {cmd}. Error: {e}"
|
|
301
|
+
)
|
|
302
|
+
await asyncio.sleep(wait_time)
|
|
303
|
+
|
|
304
|
+
# This line should never be reached due to the loop logic
|
|
305
|
+
raise RuntimeError("Unexpected error in run_command_with_retries")
|