crackerjack 0.37.9__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 +30 -1
- crackerjack/__main__.py +342 -1263
- crackerjack/adapters/README.md +18 -0
- crackerjack/adapters/__init__.py +27 -5
- 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/{rust_tool_manager.py → lsp/_manager.py} +3 -3
- crackerjack/adapters/{skylos_adapter.py → lsp/skylos.py} +59 -7
- crackerjack/adapters/{zuban_adapter.py → lsp/zuban.py} +3 -6
- 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 +40 -12
- crackerjack/agents/base.py +1 -0
- crackerjack/agents/claude_code_bridge.py +641 -0
- crackerjack/agents/coordinator.py +49 -53
- crackerjack/agents/dry_agent.py +187 -3
- 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 +6 -8
- 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/performance_agent.py +121 -1152
- crackerjack/agents/refactoring_agent.py +156 -655
- crackerjack/agents/semantic_agent.py +479 -0
- crackerjack/agents/semantic_helpers.py +356 -0
- crackerjack/agents/test_creation_agent.py +19 -1605
- crackerjack/api.py +5 -7
- crackerjack/cli/README.md +394 -0
- crackerjack/cli/__init__.py +1 -1
- crackerjack/cli/cache_handlers.py +23 -18
- crackerjack/cli/cache_handlers_enhanced.py +1 -4
- crackerjack/cli/facade.py +70 -8
- 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 +249 -49
- crackerjack/cli/interactive.py +8 -5
- crackerjack/cli/options.py +203 -110
- crackerjack/cli/semantic_handlers.py +292 -0
- crackerjack/cli/version.py +19 -0
- crackerjack/code_cleaner.py +60 -24
- crackerjack/config/README.md +472 -0
- crackerjack/config/__init__.py +256 -0
- crackerjack/config/global_lock_config.py +191 -54
- crackerjack/config/hooks.py +188 -16
- 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/async_workflow_orchestrator.py +79 -53
- crackerjack/core/autofix_coordinator.py +22 -9
- crackerjack/core/container.py +10 -9
- crackerjack/core/enhanced_container.py +9 -9
- crackerjack/core/performance.py +1 -1
- crackerjack/core/performance_monitor.py +5 -3
- crackerjack/core/phase_coordinator.py +1018 -634
- crackerjack/core/proactive_workflow.py +3 -3
- crackerjack/core/retry.py +275 -0
- crackerjack/core/service_watchdog.py +167 -23
- crackerjack/core/session_coordinator.py +187 -382
- crackerjack/core/timeout_manager.py +161 -44
- 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 +1247 -953
- 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/README.md +11 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +1 -1
- crackerjack/documentation/README.md +11 -0
- crackerjack/documentation/ai_templates.py +1 -1
- crackerjack/documentation/dual_output_generator.py +11 -9
- crackerjack/documentation/reference_generator.py +104 -59
- crackerjack/dynamic_config.py +52 -61
- crackerjack/errors.py +1 -1
- 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 +2 -0
- crackerjack/executors/async_hook_executor.py +539 -77
- crackerjack/executors/cached_hook_executor.py +3 -3
- crackerjack/executors/hook_executor.py +967 -102
- crackerjack/executors/hook_lock_manager.py +31 -22
- crackerjack/executors/individual_hook_executor.py +66 -32
- crackerjack/executors/lsp_aware_hook_executor.py +136 -57
- crackerjack/executors/progress_hook_executor.py +282 -0
- crackerjack/executors/tool_proxy.py +23 -7
- crackerjack/hooks/README.md +485 -0
- crackerjack/hooks/lsp_hook.py +8 -9
- crackerjack/intelligence/README.md +557 -0
- crackerjack/interactive.py +37 -10
- crackerjack/managers/README.md +369 -0
- crackerjack/managers/async_hook_manager.py +41 -57
- crackerjack/managers/hook_manager.py +449 -79
- crackerjack/managers/publish_manager.py +81 -36
- crackerjack/managers/test_command_builder.py +290 -12
- crackerjack/managers/test_executor.py +93 -8
- crackerjack/managers/test_manager.py +1082 -75
- crackerjack/managers/test_progress.py +118 -26
- crackerjack/mcp/README.md +374 -0
- crackerjack/mcp/cache.py +25 -2
- crackerjack/mcp/client_runner.py +35 -18
- crackerjack/mcp/context.py +9 -9
- crackerjack/mcp/dashboard.py +24 -8
- crackerjack/mcp/enhanced_progress_monitor.py +34 -23
- crackerjack/mcp/file_monitor.py +27 -6
- crackerjack/mcp/progress_components.py +45 -34
- crackerjack/mcp/progress_monitor.py +6 -9
- crackerjack/mcp/rate_limiter.py +11 -7
- crackerjack/mcp/server.py +2 -0
- crackerjack/mcp/server_core.py +187 -55
- crackerjack/mcp/service_watchdog.py +12 -9
- crackerjack/mcp/task_manager.py +2 -2
- crackerjack/mcp/tools/README.md +27 -0
- crackerjack/mcp/tools/__init__.py +2 -0
- crackerjack/mcp/tools/core_tools.py +75 -52
- crackerjack/mcp/tools/execution_tools.py +87 -31
- crackerjack/mcp/tools/intelligence_tools.py +2 -2
- crackerjack/mcp/tools/proactive_tools.py +1 -1
- crackerjack/mcp/tools/semantic_tools.py +584 -0
- crackerjack/mcp/tools/utility_tools.py +180 -132
- crackerjack/mcp/tools/workflow_executor.py +87 -46
- crackerjack/mcp/websocket/README.md +31 -0
- crackerjack/mcp/websocket/app.py +11 -1
- crackerjack/mcp/websocket/event_bridge.py +188 -0
- crackerjack/mcp/websocket/jobs.py +27 -4
- 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 +16 -2930
- crackerjack/mcp/websocket/server.py +1 -3
- crackerjack/mcp/websocket/websocket_handler.py +107 -6
- crackerjack/models/README.md +308 -0
- crackerjack/models/__init__.py +10 -1
- crackerjack/models/config.py +639 -22
- crackerjack/models/config_adapter.py +6 -6
- crackerjack/models/protocols.py +1167 -23
- crackerjack/models/pydantic_models.py +320 -0
- crackerjack/models/qa_config.py +145 -0
- crackerjack/models/qa_results.py +134 -0
- crackerjack/models/results.py +35 -0
- crackerjack/models/semantic_models.py +258 -0
- crackerjack/models/task.py +19 -3
- crackerjack/models/test_models.py +60 -0
- crackerjack/monitoring/README.md +11 -0
- crackerjack/monitoring/ai_agent_watchdog.py +5 -4
- crackerjack/monitoring/metrics_collector.py +4 -3
- crackerjack/monitoring/regression_prevention.py +4 -3
- crackerjack/monitoring/websocket_server.py +4 -241
- crackerjack/orchestration/README.md +340 -0
- crackerjack/orchestration/__init__.py +43 -0
- crackerjack/orchestration/advanced_orchestrator.py +20 -67
- 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 +13 -6
- crackerjack/orchestration/execution_strategies.py +6 -6
- 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 +1 -1
- crackerjack/plugins/README.md +11 -0
- crackerjack/plugins/hooks.py +3 -2
- crackerjack/plugins/loader.py +3 -3
- crackerjack/plugins/managers.py +1 -1
- crackerjack/py313.py +191 -0
- crackerjack/security/README.md +11 -0
- crackerjack/services/README.md +374 -0
- crackerjack/services/__init__.py +8 -21
- crackerjack/services/ai/README.md +295 -0
- crackerjack/services/ai/__init__.py +7 -0
- crackerjack/services/ai/advanced_optimizer.py +878 -0
- crackerjack/services/{contextual_ai_assistant.py → ai/contextual_ai_assistant.py} +5 -3
- 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/api_extractor.py +5 -3
- crackerjack/services/bounded_status_operations.py +45 -5
- crackerjack/services/cache.py +249 -318
- crackerjack/services/changelog_automation.py +7 -3
- crackerjack/services/command_execution_service.py +305 -0
- crackerjack/services/config_integrity.py +83 -39
- crackerjack/services/config_merge.py +9 -6
- crackerjack/services/config_service.py +198 -0
- crackerjack/services/config_template.py +13 -26
- crackerjack/services/coverage_badge_service.py +6 -4
- crackerjack/services/coverage_ratchet.py +53 -27
- crackerjack/services/debug.py +18 -7
- crackerjack/services/dependency_analyzer.py +4 -4
- crackerjack/services/dependency_monitor.py +13 -13
- crackerjack/services/documentation_generator.py +4 -2
- crackerjack/services/documentation_service.py +62 -33
- crackerjack/services/enhanced_filesystem.py +81 -27
- crackerjack/services/enterprise_optimizer.py +1 -1
- crackerjack/services/error_pattern_analyzer.py +10 -10
- crackerjack/services/file_filter.py +221 -0
- crackerjack/services/file_hasher.py +5 -7
- crackerjack/services/file_io_service.py +361 -0
- crackerjack/services/file_modifier.py +615 -0
- crackerjack/services/filesystem.py +80 -109
- crackerjack/services/git.py +99 -5
- crackerjack/services/health_metrics.py +4 -6
- crackerjack/services/heatmap_generator.py +12 -3
- crackerjack/services/incremental_executor.py +380 -0
- crackerjack/services/initialization.py +101 -49
- crackerjack/services/log_manager.py +2 -2
- crackerjack/services/logging.py +120 -68
- crackerjack/services/lsp_client.py +12 -12
- crackerjack/services/memory_optimizer.py +27 -22
- 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/{performance_benchmarks.py → monitoring/performance_benchmarks.py} +100 -14
- crackerjack/services/{performance_cache.py → monitoring/performance_cache.py} +21 -15
- crackerjack/services/{performance_monitor.py → monitoring/performance_monitor.py} +10 -6
- crackerjack/services/parallel_executor.py +166 -55
- 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 +21 -8
- 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_baseline.py → quality/quality_baseline.py} +163 -2
- crackerjack/services/{quality_baseline_enhanced.py → quality/quality_baseline_enhanced.py} +4 -1
- crackerjack/services/{quality_intelligence.py → quality/quality_intelligence.py} +180 -16
- crackerjack/services/regex_patterns.py +58 -2987
- crackerjack/services/regex_utils.py +55 -29
- crackerjack/services/secure_status_formatter.py +42 -15
- crackerjack/services/secure_subprocess.py +35 -2
- crackerjack/services/security.py +16 -8
- crackerjack/services/server_manager.py +40 -51
- crackerjack/services/smart_scheduling.py +46 -6
- crackerjack/services/status_authentication.py +3 -3
- crackerjack/services/thread_safe_status_collector.py +1 -0
- crackerjack/services/tool_filter.py +368 -0
- crackerjack/services/tool_version_service.py +9 -5
- crackerjack/services/unified_config.py +43 -351
- crackerjack/services/vector_store.py +689 -0
- crackerjack/services/version_analyzer.py +6 -4
- crackerjack/services/version_checker.py +14 -8
- crackerjack/services/zuban_lsp_service.py +5 -4
- crackerjack/slash_commands/README.md +11 -0
- crackerjack/slash_commands/init.md +2 -12
- crackerjack/slash_commands/run.md +84 -50
- 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_regex_patterns.py +7 -3
- crackerjack/ui/README.md +11 -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.37.9.dist-info → crackerjack-0.45.2.dist-info}/METADATA +678 -98
- crackerjack-0.45.2.dist-info/RECORD +478 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
- crackerjack/managers/test_manager_backup.py +0 -1075
- crackerjack/mcp/tools/execution_tools_backup.py +0 -1011
- crackerjack/mixins/__init__.py +0 -3
- crackerjack/mixins/error_handling.py +0 -145
- crackerjack/services/config.py +0 -358
- crackerjack/ui/server_panels.py +0 -125
- crackerjack-0.37.9.dist-info/RECORD +0 -231
- /crackerjack/adapters/{rust_tool_adapter.py → lsp/_base.py} +0 -0
- /crackerjack/adapters/{lsp_client.py → lsp/_client.py} +0 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
"""Safe file modification service with comprehensive security checks.
|
|
2
|
+
|
|
3
|
+
This module provides SafeFileModifier, a service for safely modifying files
|
|
4
|
+
with automatic backups, validation, and atomic operations.
|
|
5
|
+
|
|
6
|
+
Security Features:
|
|
7
|
+
- Symlink detection and blocking (both direct and in path chain)
|
|
8
|
+
- Path traversal prevention (must be within project)
|
|
9
|
+
- File size limits (10MB default, configurable)
|
|
10
|
+
- Forbidden file patterns (.env, .git/*, *.key, etc.)
|
|
11
|
+
- Atomic write operations using tempfile
|
|
12
|
+
- Permission preservation
|
|
13
|
+
- Automatic rollback on errors
|
|
14
|
+
|
|
15
|
+
Features:
|
|
16
|
+
- Automatic backup creation with timestamps
|
|
17
|
+
- Diff generation for review
|
|
18
|
+
- Dry-run mode for previewing changes
|
|
19
|
+
- Rollback on errors
|
|
20
|
+
- Validation of file existence and permissions
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import difflib
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import tempfile
|
|
27
|
+
import typing as t
|
|
28
|
+
from contextlib import suppress
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from fnmatch import fnmatch
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from loguru import logger
|
|
34
|
+
|
|
35
|
+
from crackerjack.models.protocols import SafeFileModifierProtocol, ServiceProtocol
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SafeFileModifier(SafeFileModifierProtocol, ServiceProtocol):
|
|
39
|
+
"""Safely modify files with backups and validation.
|
|
40
|
+
|
|
41
|
+
Features:
|
|
42
|
+
- Automatic backup creation with timestamps
|
|
43
|
+
- Diff generation for review
|
|
44
|
+
- Dry-run mode for previewing changes
|
|
45
|
+
- Rollback on errors
|
|
46
|
+
- Validation of file existence and permissions
|
|
47
|
+
- Atomic file operations to prevent partial writes
|
|
48
|
+
- Symlink protection to prevent following malicious links
|
|
49
|
+
- File size limits to prevent DoS attacks
|
|
50
|
+
|
|
51
|
+
Security:
|
|
52
|
+
- All file operations use atomic writes (write to temp, then rename)
|
|
53
|
+
- Symlinks are detected and blocked (both direct and in path chain)
|
|
54
|
+
- Path traversal attacks are prevented
|
|
55
|
+
- File size limits enforced
|
|
56
|
+
- Forbidden file patterns blocked (.env, .git/*, etc.)
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
>>> modifier = SafeFileModifier()
|
|
60
|
+
>>> result = await modifier.apply_fix(
|
|
61
|
+
... file_path="myfile.py",
|
|
62
|
+
... fixed_content="print('hello')",
|
|
63
|
+
... dry_run=False,
|
|
64
|
+
... )
|
|
65
|
+
>>> if result["success"]:
|
|
66
|
+
... print(f"Applied fix, backup at: {result['backup_path']}")
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Forbidden file patterns for security
|
|
70
|
+
FORBIDDEN_PATTERNS = [
|
|
71
|
+
".env*", # .env, .env.local, .env.production, .env.test, etc.
|
|
72
|
+
".git/*",
|
|
73
|
+
"*.key",
|
|
74
|
+
"*.pem",
|
|
75
|
+
"*.crt",
|
|
76
|
+
"*_rsa",
|
|
77
|
+
"*_dsa",
|
|
78
|
+
"*_ed25519",
|
|
79
|
+
"*.p12",
|
|
80
|
+
"*.pfx",
|
|
81
|
+
"id_rsa*",
|
|
82
|
+
"*.secret",
|
|
83
|
+
"secrets.*",
|
|
84
|
+
".ssh/*",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
backup_dir: Path | None = None,
|
|
90
|
+
max_file_size: int = 10_485_760, # 10MB default
|
|
91
|
+
):
|
|
92
|
+
"""Initialize SafeFileModifier.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
backup_dir: Directory for backups (default: .backups)
|
|
96
|
+
max_file_size: Maximum file size in bytes (default: 10MB)
|
|
97
|
+
"""
|
|
98
|
+
self._backup_dir = backup_dir or Path(".backups")
|
|
99
|
+
self._max_file_size = max_file_size
|
|
100
|
+
self._ensure_backup_dir()
|
|
101
|
+
|
|
102
|
+
def initialize(self) -> None:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def cleanup(self) -> None:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def health_check(self) -> bool:
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
def shutdown(self) -> None:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
def metrics(self) -> dict[str, t.Any]:
|
|
115
|
+
return {}
|
|
116
|
+
|
|
117
|
+
def is_healthy(self) -> bool:
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
def register_resource(self, resource: t.Any) -> None:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
def cleanup_resource(self, resource: t.Any) -> None:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
def record_error(self, error: Exception) -> None:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
def increment_requests(self) -> None:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
def get_custom_metric(self, name: str) -> t.Any:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def set_custom_metric(self, name: str, value: t.Any) -> None:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
def _ensure_backup_dir(self) -> None:
|
|
139
|
+
"""Create backup directory if it doesn't exist."""
|
|
140
|
+
if not self._backup_dir.exists():
|
|
141
|
+
self._backup_dir.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
logger.debug(f"Created backup directory: {self._backup_dir}")
|
|
143
|
+
|
|
144
|
+
async def apply_fix(
|
|
145
|
+
self,
|
|
146
|
+
file_path: str,
|
|
147
|
+
fixed_content: str,
|
|
148
|
+
dry_run: bool = False,
|
|
149
|
+
create_backup: bool = True,
|
|
150
|
+
) -> dict[str, t.Any]:
|
|
151
|
+
"""Apply code fix with safety checks.
|
|
152
|
+
|
|
153
|
+
This is the main public API for applying fixes to files.
|
|
154
|
+
Performs comprehensive validation, creates backups, and applies
|
|
155
|
+
changes atomically.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
file_path: Path to file to modify
|
|
159
|
+
fixed_content: New content to write
|
|
160
|
+
dry_run: If True, only generate diff without modifying
|
|
161
|
+
create_backup: If True, create backup before modifying
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dictionary with keys:
|
|
165
|
+
- success: bool - Whether operation succeeded
|
|
166
|
+
- diff: str - Unified diff of changes
|
|
167
|
+
- backup_path: str | None - Path to backup file
|
|
168
|
+
- dry_run: bool - Whether this was a dry run
|
|
169
|
+
- message: str - Human-readable message
|
|
170
|
+
- error: str - Error message if failed
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
>>> result = await modifier.apply_fix(
|
|
174
|
+
... "test.py", "print('fixed')", dry_run=True
|
|
175
|
+
... )
|
|
176
|
+
>>> print(result["diff"])
|
|
177
|
+
"""
|
|
178
|
+
return await self._apply_fix(file_path, fixed_content, dry_run, create_backup)
|
|
179
|
+
|
|
180
|
+
async def _apply_fix(
|
|
181
|
+
self,
|
|
182
|
+
file_path: str,
|
|
183
|
+
fixed_content: str,
|
|
184
|
+
dry_run: bool,
|
|
185
|
+
create_backup: bool,
|
|
186
|
+
) -> dict[str, t.Any]:
|
|
187
|
+
"""Internal implementation of fix application with atomic writes.
|
|
188
|
+
|
|
189
|
+
Security features:
|
|
190
|
+
1. Validates file path (symlinks, traversal, size)
|
|
191
|
+
2. Validates content size
|
|
192
|
+
3. Creates backup before modification
|
|
193
|
+
4. Uses atomic write (temp file + rename)
|
|
194
|
+
5. Preserves permissions
|
|
195
|
+
6. Rollback on errors
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
file_path: Path to file to modify
|
|
199
|
+
fixed_content: New content to write
|
|
200
|
+
dry_run: If True, only generate diff
|
|
201
|
+
create_backup: If True, create backup
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Result dictionary with success status and details
|
|
205
|
+
"""
|
|
206
|
+
path = Path(file_path)
|
|
207
|
+
|
|
208
|
+
# Validation
|
|
209
|
+
result: dict[str, t.Any] = self._validate_fix_inputs(path, fixed_content)
|
|
210
|
+
if not result["success"]:
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
# Read original content
|
|
214
|
+
result = self._read_original_content(path)
|
|
215
|
+
if not result["success"]:
|
|
216
|
+
return result
|
|
217
|
+
original_content = result["content"]
|
|
218
|
+
|
|
219
|
+
# Generate diff
|
|
220
|
+
# Type assertion: content is str when success is True
|
|
221
|
+
assert isinstance(original_content, str)
|
|
222
|
+
diff = self._generate_diff(original_content, fixed_content, file_path)
|
|
223
|
+
|
|
224
|
+
# Dry-run mode - just return diff
|
|
225
|
+
if dry_run:
|
|
226
|
+
return self._create_dry_run_result(diff)
|
|
227
|
+
|
|
228
|
+
# Create backup if requested
|
|
229
|
+
# Type assertion: content is str when success is True
|
|
230
|
+
assert isinstance(original_content, str)
|
|
231
|
+
result = self._handle_backup(path, original_content, create_backup, diff)
|
|
232
|
+
if not result["success"]:
|
|
233
|
+
return result
|
|
234
|
+
backup_path = result.get("backup_path")
|
|
235
|
+
# Type assertion: backup_path is Path | None after successful backup creation
|
|
236
|
+
assert backup_path is None or isinstance(backup_path, Path)
|
|
237
|
+
|
|
238
|
+
# Apply the fix atomically
|
|
239
|
+
return self._atomic_write_fix(path, fixed_content, diff, backup_path, file_path)
|
|
240
|
+
|
|
241
|
+
def _validate_fix_inputs(
|
|
242
|
+
self, path: Path, fixed_content: str
|
|
243
|
+
) -> dict[str, str | bool | None]:
|
|
244
|
+
"""Validate file path and content size."""
|
|
245
|
+
validation_result = self._validate_file_path(path)
|
|
246
|
+
if not validation_result["valid"]:
|
|
247
|
+
return {
|
|
248
|
+
"success": False,
|
|
249
|
+
"error": validation_result["error"],
|
|
250
|
+
"diff": "",
|
|
251
|
+
"backup_path": None,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if len(fixed_content) > self._max_file_size:
|
|
255
|
+
return {
|
|
256
|
+
"success": False,
|
|
257
|
+
"error": f"Content size {len(fixed_content)} exceeds limit {self._max_file_size}",
|
|
258
|
+
"diff": "",
|
|
259
|
+
"backup_path": None,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {"success": True}
|
|
263
|
+
|
|
264
|
+
def _read_original_content(self, path: Path) -> dict[str, str | bool | None]:
|
|
265
|
+
"""Read original file content with error handling."""
|
|
266
|
+
try:
|
|
267
|
+
original_content = path.read_text(encoding="utf-8")
|
|
268
|
+
return {"success": True, "content": original_content}
|
|
269
|
+
except UnicodeDecodeError:
|
|
270
|
+
return {
|
|
271
|
+
"success": False,
|
|
272
|
+
"error": f"File is not valid UTF-8: {path}",
|
|
273
|
+
"diff": "",
|
|
274
|
+
"backup_path": None,
|
|
275
|
+
}
|
|
276
|
+
except Exception as e:
|
|
277
|
+
return {
|
|
278
|
+
"success": False,
|
|
279
|
+
"error": f"Failed to read file: {e}",
|
|
280
|
+
"diff": "",
|
|
281
|
+
"backup_path": None,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
def _create_dry_run_result(self, diff: str) -> dict[str, str | bool | None]:
|
|
285
|
+
"""Create result dictionary for dry-run mode."""
|
|
286
|
+
return {
|
|
287
|
+
"success": True,
|
|
288
|
+
"diff": diff,
|
|
289
|
+
"backup_path": None,
|
|
290
|
+
"dry_run": True,
|
|
291
|
+
"message": "Dry-run: Changes not applied",
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
def _handle_backup(
|
|
295
|
+
self, path: Path, original_content: str, create_backup: bool, diff: str
|
|
296
|
+
) -> dict[str, str | bool | Path | None]:
|
|
297
|
+
"""Create backup if requested."""
|
|
298
|
+
if not create_backup:
|
|
299
|
+
return {"success": True, "backup_path": None}
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
backup_path = self._create_backup(path, original_content)
|
|
303
|
+
return {"success": True, "backup_path": backup_path}
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.error(f"Failed to create backup: {e}")
|
|
306
|
+
return {
|
|
307
|
+
"success": False,
|
|
308
|
+
"error": f"Backup creation failed: {e}",
|
|
309
|
+
"diff": diff,
|
|
310
|
+
"backup_path": None,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
def _atomic_write_fix(
|
|
314
|
+
self,
|
|
315
|
+
path: Path,
|
|
316
|
+
fixed_content: str,
|
|
317
|
+
diff: str,
|
|
318
|
+
backup_path: Path | None,
|
|
319
|
+
file_path: str,
|
|
320
|
+
) -> dict[str, str | bool | None]:
|
|
321
|
+
"""Write fix atomically with rollback on error."""
|
|
322
|
+
try:
|
|
323
|
+
temp_fd, temp_path_str = tempfile.mkstemp(
|
|
324
|
+
dir=path.parent,
|
|
325
|
+
prefix=f".{path.name}.",
|
|
326
|
+
suffix=".tmp",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
with os.fdopen(temp_fd, "w", encoding="utf-8") as f:
|
|
331
|
+
f.write(fixed_content)
|
|
332
|
+
f.flush()
|
|
333
|
+
os.fsync(f.fileno())
|
|
334
|
+
|
|
335
|
+
original_stat = path.stat()
|
|
336
|
+
os.chmod(temp_path_str, original_stat.st_mode)
|
|
337
|
+
shutil.move(temp_path_str, path)
|
|
338
|
+
|
|
339
|
+
logger.info(f"Successfully applied fix to {file_path}")
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
"success": True,
|
|
343
|
+
"diff": diff,
|
|
344
|
+
"backup_path": str(backup_path) if backup_path else None,
|
|
345
|
+
"dry_run": False,
|
|
346
|
+
"message": f"Fix applied successfully to {file_path}",
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
except Exception:
|
|
350
|
+
with suppress(Exception):
|
|
351
|
+
Path(temp_path_str).unlink()
|
|
352
|
+
raise
|
|
353
|
+
|
|
354
|
+
except Exception as e:
|
|
355
|
+
if backup_path:
|
|
356
|
+
logger.warning(f"Fix failed, restoring from backup: {e}")
|
|
357
|
+
try:
|
|
358
|
+
self._restore_backup(path, backup_path)
|
|
359
|
+
except Exception as restore_error:
|
|
360
|
+
logger.error(f"Rollback failed: {restore_error}")
|
|
361
|
+
return {
|
|
362
|
+
"success": False,
|
|
363
|
+
"error": f"Failed to write file AND rollback failed: {e} (rollback: {restore_error})",
|
|
364
|
+
"diff": diff,
|
|
365
|
+
"backup_path": str(backup_path) if backup_path else None,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
"success": False,
|
|
370
|
+
"error": f"Failed to write file: {e}",
|
|
371
|
+
"diff": diff,
|
|
372
|
+
"backup_path": str(backup_path) if backup_path else None,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
def _check_file_exists(self, path: Path) -> dict[str, bool | str]:
|
|
376
|
+
"""Check if the file exists."""
|
|
377
|
+
if not path.exists():
|
|
378
|
+
return {
|
|
379
|
+
"valid": False,
|
|
380
|
+
"error": f"File does not exist: {path}",
|
|
381
|
+
}
|
|
382
|
+
return {"valid": True, "error": ""}
|
|
383
|
+
|
|
384
|
+
def _check_is_symlink(self, path: Path) -> dict[str, bool | str]:
|
|
385
|
+
"""Check if the file is a symlink."""
|
|
386
|
+
if path.is_symlink():
|
|
387
|
+
return {
|
|
388
|
+
"valid": False,
|
|
389
|
+
"error": f"Symlinks are not allowed for security reasons: {path}",
|
|
390
|
+
}
|
|
391
|
+
return {"valid": True, "error": ""}
|
|
392
|
+
|
|
393
|
+
def _check_is_file(self, path: Path) -> dict[str, bool | str]:
|
|
394
|
+
"""Check if the path is a file."""
|
|
395
|
+
if not path.is_file():
|
|
396
|
+
return {
|
|
397
|
+
"valid": False,
|
|
398
|
+
"error": f"Path is not a file: {path}",
|
|
399
|
+
}
|
|
400
|
+
return {"valid": True, "error": ""}
|
|
401
|
+
|
|
402
|
+
def _check_forbidden_patterns(self, path: Path) -> dict[str, bool | str]:
|
|
403
|
+
"""Check if the file matches forbidden patterns."""
|
|
404
|
+
file_str = str(path)
|
|
405
|
+
for pattern in self.FORBIDDEN_PATTERNS:
|
|
406
|
+
if fnmatch(file_str, pattern) or fnmatch(path.name, pattern):
|
|
407
|
+
return {
|
|
408
|
+
"valid": False,
|
|
409
|
+
"error": f"File matches forbidden pattern '{pattern}': {path}",
|
|
410
|
+
}
|
|
411
|
+
return {"valid": True, "error": ""}
|
|
412
|
+
|
|
413
|
+
def _check_file_size(self, path: Path) -> dict[str, bool | str]:
|
|
414
|
+
"""Check the file size against the limit."""
|
|
415
|
+
try:
|
|
416
|
+
file_size = path.stat().st_size
|
|
417
|
+
if file_size > self._max_file_size:
|
|
418
|
+
return {
|
|
419
|
+
"valid": False,
|
|
420
|
+
"error": f"File size {file_size} exceeds limit {self._max_file_size}",
|
|
421
|
+
}
|
|
422
|
+
except Exception as e:
|
|
423
|
+
return {
|
|
424
|
+
"valid": False,
|
|
425
|
+
"error": f"Failed to check file size: {e}",
|
|
426
|
+
}
|
|
427
|
+
return {"valid": True, "error": ""}
|
|
428
|
+
|
|
429
|
+
def _check_file_writable(self, path: Path) -> dict[str, bool | str]:
|
|
430
|
+
"""Check if the file is writable."""
|
|
431
|
+
if not os.access(path, os.W_OK):
|
|
432
|
+
return {
|
|
433
|
+
"valid": False,
|
|
434
|
+
"error": f"File is not writable: {path}",
|
|
435
|
+
}
|
|
436
|
+
return {"valid": True, "error": ""}
|
|
437
|
+
|
|
438
|
+
def _check_path_traversal(self, path: Path) -> dict[str, bool | str]:
|
|
439
|
+
"""Check for path traversal attacks."""
|
|
440
|
+
try:
|
|
441
|
+
resolved_path = path.resolve()
|
|
442
|
+
project_root = Path.cwd().resolve()
|
|
443
|
+
|
|
444
|
+
# Ensure the resolved path is within the project directory
|
|
445
|
+
resolved_path.relative_to(project_root)
|
|
446
|
+
|
|
447
|
+
except ValueError:
|
|
448
|
+
return {
|
|
449
|
+
"valid": False,
|
|
450
|
+
"error": f"File path outside project directory: {path}",
|
|
451
|
+
}
|
|
452
|
+
return {"valid": True, "error": ""}
|
|
453
|
+
|
|
454
|
+
def _check_symlinks_in_path_chain(self, path: Path) -> dict[str, bool | str]:
|
|
455
|
+
"""Check for symlinks in the path chain."""
|
|
456
|
+
current = path
|
|
457
|
+
while current != current.parent:
|
|
458
|
+
if current.is_symlink():
|
|
459
|
+
return {
|
|
460
|
+
"valid": False,
|
|
461
|
+
"error": f"Symlink in path chain not allowed: {current}",
|
|
462
|
+
}
|
|
463
|
+
current = current.parent
|
|
464
|
+
return {"valid": True, "error": ""}
|
|
465
|
+
|
|
466
|
+
def _validate_file_path(self, path: Path) -> dict[str, bool | str]:
|
|
467
|
+
"""Validate file path before modification with comprehensive security checks.
|
|
468
|
+
|
|
469
|
+
Security checks:
|
|
470
|
+
1. File existence and type validation
|
|
471
|
+
2. Symlink detection (blocks symlinks to prevent malicious redirects)
|
|
472
|
+
3. Path traversal prevention (must be within project)
|
|
473
|
+
4. File size limits
|
|
474
|
+
5. Permission checks
|
|
475
|
+
6. Forbidden file pattern checks
|
|
476
|
+
7. Path chain symlink validation
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
path: Path to validate
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Dictionary with:
|
|
483
|
+
- valid: bool - Whether path is valid
|
|
484
|
+
- error: str - Error message if invalid
|
|
485
|
+
|
|
486
|
+
Example:
|
|
487
|
+
>>> result = modifier._validate_file_path(Path("test.py"))
|
|
488
|
+
>>> if not result["valid"]:
|
|
489
|
+
... print(result["error"])
|
|
490
|
+
"""
|
|
491
|
+
# Must exist
|
|
492
|
+
result = self._check_file_exists(path)
|
|
493
|
+
if not result["valid"]:
|
|
494
|
+
return result
|
|
495
|
+
|
|
496
|
+
# SECURITY: Block symlinks to prevent following malicious links
|
|
497
|
+
result = self._check_is_symlink(path)
|
|
498
|
+
if not result["valid"]:
|
|
499
|
+
return result
|
|
500
|
+
|
|
501
|
+
# Must be a file (not directory)
|
|
502
|
+
result = self._check_is_file(path)
|
|
503
|
+
if not result["valid"]:
|
|
504
|
+
return result
|
|
505
|
+
|
|
506
|
+
# SECURITY: Check forbidden file patterns
|
|
507
|
+
result = self._check_forbidden_patterns(path)
|
|
508
|
+
if not result["valid"]:
|
|
509
|
+
return result
|
|
510
|
+
|
|
511
|
+
# SECURITY: Check file size before processing
|
|
512
|
+
result = self._check_file_size(path)
|
|
513
|
+
if not result["valid"]:
|
|
514
|
+
return result
|
|
515
|
+
|
|
516
|
+
# Must be writable
|
|
517
|
+
result = self._check_file_writable(path)
|
|
518
|
+
if not result["valid"]:
|
|
519
|
+
return result
|
|
520
|
+
|
|
521
|
+
# SECURITY: Prevent path traversal attacks
|
|
522
|
+
result = self._check_path_traversal(path)
|
|
523
|
+
if not result["valid"]:
|
|
524
|
+
return result
|
|
525
|
+
|
|
526
|
+
# SECURITY: Additional check - ensure no symlinks in the path chain
|
|
527
|
+
return self._check_symlinks_in_path_chain(path)
|
|
528
|
+
|
|
529
|
+
def _create_backup(
|
|
530
|
+
self,
|
|
531
|
+
file_path: Path,
|
|
532
|
+
content: str,
|
|
533
|
+
) -> Path:
|
|
534
|
+
"""Create timestamped backup file.
|
|
535
|
+
|
|
536
|
+
Backup naming: .backups/<filename>_<timestamp>.bak
|
|
537
|
+
Example: .backups/myfile.py_20250103_143022.bak
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
file_path: Original file path
|
|
541
|
+
content: Content to backup
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
Path to backup file
|
|
545
|
+
|
|
546
|
+
Raises:
|
|
547
|
+
IOError: If backup creation fails
|
|
548
|
+
"""
|
|
549
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
550
|
+
backup_name = f"{file_path.name}_{timestamp}.bak"
|
|
551
|
+
backup_path = self._backup_dir / backup_name
|
|
552
|
+
|
|
553
|
+
# Write backup
|
|
554
|
+
backup_path.write_text(content, encoding="utf-8")
|
|
555
|
+
|
|
556
|
+
logger.debug(f"Created backup: {backup_path}")
|
|
557
|
+
|
|
558
|
+
return backup_path
|
|
559
|
+
|
|
560
|
+
def _restore_backup(
|
|
561
|
+
self,
|
|
562
|
+
file_path: Path,
|
|
563
|
+
backup_path: Path,
|
|
564
|
+
) -> None:
|
|
565
|
+
"""Restore file from backup.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
file_path: File to restore
|
|
569
|
+
backup_path: Backup file to restore from
|
|
570
|
+
|
|
571
|
+
Raises:
|
|
572
|
+
IOError: If restoration fails
|
|
573
|
+
"""
|
|
574
|
+
try:
|
|
575
|
+
backup_content = backup_path.read_text(encoding="utf-8")
|
|
576
|
+
file_path.write_text(backup_content, encoding="utf-8")
|
|
577
|
+
|
|
578
|
+
logger.info(f"Restored {file_path} from backup")
|
|
579
|
+
|
|
580
|
+
except Exception as e:
|
|
581
|
+
logger.error(f"Failed to restore backup: {e}")
|
|
582
|
+
raise
|
|
583
|
+
|
|
584
|
+
def _generate_diff(
|
|
585
|
+
self,
|
|
586
|
+
original: str,
|
|
587
|
+
fixed: str,
|
|
588
|
+
filename: str,
|
|
589
|
+
) -> str:
|
|
590
|
+
"""Generate unified diff for review.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
original: Original file content
|
|
594
|
+
fixed: Fixed file content
|
|
595
|
+
filename: Name for diff headers
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
Unified diff string
|
|
599
|
+
|
|
600
|
+
Example:
|
|
601
|
+
>>> diff = modifier._generate_diff("old content", "new content", "test.py")
|
|
602
|
+
>>> print(diff)
|
|
603
|
+
"""
|
|
604
|
+
original_lines = original.splitlines(keepends=True)
|
|
605
|
+
fixed_lines = fixed.splitlines(keepends=True)
|
|
606
|
+
|
|
607
|
+
diff = difflib.unified_diff(
|
|
608
|
+
original_lines,
|
|
609
|
+
fixed_lines,
|
|
610
|
+
fromfile=f"{filename} (original)",
|
|
611
|
+
tofile=f"{filename} (fixed)",
|
|
612
|
+
lineterm="",
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
return "".join(diff)
|