crackerjack 0.33.0__py3-none-any.whl → 0.33.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.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/__main__.py +1350 -34
- crackerjack/adapters/__init__.py +17 -0
- crackerjack/adapters/lsp_client.py +358 -0
- crackerjack/adapters/rust_tool_adapter.py +194 -0
- crackerjack/adapters/rust_tool_manager.py +193 -0
- crackerjack/adapters/skylos_adapter.py +231 -0
- crackerjack/adapters/zuban_adapter.py +560 -0
- crackerjack/agents/base.py +7 -3
- crackerjack/agents/coordinator.py +271 -33
- crackerjack/agents/documentation_agent.py +9 -15
- crackerjack/agents/dry_agent.py +3 -15
- crackerjack/agents/formatting_agent.py +1 -1
- crackerjack/agents/import_optimization_agent.py +36 -180
- crackerjack/agents/performance_agent.py +17 -98
- crackerjack/agents/performance_helpers.py +7 -31
- crackerjack/agents/proactive_agent.py +1 -3
- crackerjack/agents/refactoring_agent.py +16 -85
- crackerjack/agents/refactoring_helpers.py +7 -42
- crackerjack/agents/security_agent.py +9 -48
- crackerjack/agents/test_creation_agent.py +356 -513
- crackerjack/agents/test_specialist_agent.py +0 -4
- crackerjack/api.py +6 -25
- crackerjack/cli/cache_handlers.py +204 -0
- crackerjack/cli/cache_handlers_enhanced.py +683 -0
- crackerjack/cli/facade.py +100 -0
- crackerjack/cli/handlers.py +224 -9
- crackerjack/cli/interactive.py +6 -4
- crackerjack/cli/options.py +642 -55
- crackerjack/cli/utils.py +2 -1
- crackerjack/code_cleaner.py +58 -117
- crackerjack/config/global_lock_config.py +8 -48
- crackerjack/config/hooks.py +53 -62
- crackerjack/core/async_workflow_orchestrator.py +24 -34
- crackerjack/core/autofix_coordinator.py +3 -17
- crackerjack/core/enhanced_container.py +4 -13
- crackerjack/core/file_lifecycle.py +12 -89
- crackerjack/core/performance.py +2 -2
- crackerjack/core/performance_monitor.py +15 -55
- crackerjack/core/phase_coordinator.py +104 -204
- crackerjack/core/resource_manager.py +14 -90
- crackerjack/core/service_watchdog.py +62 -95
- crackerjack/core/session_coordinator.py +149 -0
- crackerjack/core/timeout_manager.py +14 -72
- crackerjack/core/websocket_lifecycle.py +13 -78
- crackerjack/core/workflow_orchestrator.py +171 -174
- crackerjack/docs/INDEX.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/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +765 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +977 -0
- crackerjack/dynamic_config.py +55 -50
- crackerjack/executors/async_hook_executor.py +10 -15
- crackerjack/executors/cached_hook_executor.py +117 -43
- crackerjack/executors/hook_executor.py +8 -34
- crackerjack/executors/hook_lock_manager.py +26 -183
- crackerjack/executors/individual_hook_executor.py +13 -11
- crackerjack/executors/lsp_aware_hook_executor.py +270 -0
- crackerjack/executors/tool_proxy.py +417 -0
- crackerjack/hooks/lsp_hook.py +79 -0
- crackerjack/intelligence/adaptive_learning.py +25 -10
- crackerjack/intelligence/agent_orchestrator.py +2 -5
- crackerjack/intelligence/agent_registry.py +34 -24
- crackerjack/intelligence/agent_selector.py +5 -7
- crackerjack/interactive.py +17 -6
- crackerjack/managers/async_hook_manager.py +0 -1
- crackerjack/managers/hook_manager.py +79 -1
- crackerjack/managers/publish_manager.py +44 -8
- crackerjack/managers/test_command_builder.py +1 -15
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +98 -7
- crackerjack/managers/test_manager_backup.py +10 -9
- crackerjack/mcp/cache.py +2 -2
- crackerjack/mcp/client_runner.py +1 -1
- crackerjack/mcp/context.py +191 -68
- crackerjack/mcp/dashboard.py +7 -5
- crackerjack/mcp/enhanced_progress_monitor.py +31 -28
- crackerjack/mcp/file_monitor.py +30 -23
- crackerjack/mcp/progress_components.py +31 -21
- crackerjack/mcp/progress_monitor.py +50 -53
- crackerjack/mcp/rate_limiter.py +6 -6
- crackerjack/mcp/server_core.py +17 -16
- crackerjack/mcp/service_watchdog.py +2 -1
- crackerjack/mcp/state.py +4 -7
- crackerjack/mcp/task_manager.py +11 -9
- crackerjack/mcp/tools/core_tools.py +173 -32
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +8 -10
- crackerjack/mcp/tools/execution_tools_backup.py +42 -30
- crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
- crackerjack/mcp/tools/intelligence_tools.py +5 -2
- crackerjack/mcp/tools/monitoring_tools.py +33 -70
- crackerjack/mcp/tools/proactive_tools.py +24 -11
- crackerjack/mcp/tools/progress_tools.py +5 -8
- crackerjack/mcp/tools/utility_tools.py +20 -14
- crackerjack/mcp/tools/workflow_executor.py +62 -40
- crackerjack/mcp/websocket/app.py +8 -0
- crackerjack/mcp/websocket/endpoints.py +352 -357
- crackerjack/mcp/websocket/jobs.py +40 -57
- crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
- crackerjack/mcp/websocket/server.py +7 -25
- crackerjack/mcp/websocket/websocket_handler.py +6 -17
- crackerjack/mixins/__init__.py +0 -2
- crackerjack/mixins/error_handling.py +1 -70
- crackerjack/models/config.py +12 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +122 -122
- crackerjack/models/resource_protocols.py +55 -210
- crackerjack/monitoring/ai_agent_watchdog.py +13 -13
- crackerjack/monitoring/metrics_collector.py +426 -0
- crackerjack/monitoring/regression_prevention.py +8 -8
- crackerjack/monitoring/websocket_server.py +643 -0
- crackerjack/orchestration/advanced_orchestrator.py +11 -6
- crackerjack/orchestration/coverage_improvement.py +3 -3
- crackerjack/orchestration/execution_strategies.py +26 -6
- crackerjack/orchestration/test_progress_streamer.py +8 -5
- crackerjack/plugins/base.py +2 -2
- crackerjack/plugins/hooks.py +7 -0
- crackerjack/plugins/managers.py +11 -8
- crackerjack/security/__init__.py +0 -1
- crackerjack/security/audit.py +6 -35
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +615 -0
- crackerjack/services/backup_service.py +2 -2
- crackerjack/services/bounded_status_operations.py +15 -152
- crackerjack/services/cache.py +127 -1
- crackerjack/services/changelog_automation.py +395 -0
- crackerjack/services/config.py +15 -9
- crackerjack/services/config_merge.py +19 -80
- crackerjack/services/config_template.py +506 -0
- crackerjack/services/contextual_ai_assistant.py +48 -22
- crackerjack/services/coverage_badge_service.py +171 -0
- crackerjack/services/coverage_ratchet.py +27 -25
- crackerjack/services/debug.py +3 -3
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +14 -11
- crackerjack/services/documentation_generator.py +491 -0
- crackerjack/services/documentation_service.py +675 -0
- crackerjack/services/enhanced_filesystem.py +6 -5
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/git.py +8 -25
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +11 -30
- crackerjack/services/input_validator.py +5 -97
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +15 -12
- crackerjack/services/logging.py +4 -3
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +19 -87
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +9 -67
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +18 -59
- crackerjack/services/performance_cache.py +20 -81
- crackerjack/services/performance_monitor.py +27 -95
- crackerjack/services/predictive_analytics.py +510 -0
- crackerjack/services/quality_baseline.py +234 -0
- crackerjack/services/quality_baseline_enhanced.py +646 -0
- crackerjack/services/quality_intelligence.py +785 -0
- crackerjack/services/regex_patterns.py +618 -524
- crackerjack/services/regex_utils.py +43 -123
- crackerjack/services/secure_path_utils.py +5 -164
- crackerjack/services/secure_status_formatter.py +30 -141
- crackerjack/services/secure_subprocess.py +11 -92
- crackerjack/services/security.py +9 -41
- crackerjack/services/security_logger.py +12 -24
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- crackerjack/services/thread_safe_status_collector.py +19 -125
- crackerjack/services/unified_config.py +21 -13
- crackerjack/services/validation_rate_limiter.py +5 -54
- crackerjack/services/version_analyzer.py +459 -0
- crackerjack/services/version_checker.py +1 -1
- crackerjack/services/websocket_resource_limiter.py +10 -144
- crackerjack/services/zuban_lsp_service.py +390 -0
- crackerjack/slash_commands/__init__.py +2 -7
- crackerjack/slash_commands/run.md +2 -2
- crackerjack/tools/validate_input_validator_patterns.py +14 -40
- crackerjack/tools/validate_regex_patterns.py +19 -48
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/METADATA +196 -25
- crackerjack-0.33.2.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.33.0.dist-info/RECORD +0 -187
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/WHEEL +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/licenses/LICENSE +0 -0
crackerjack/cli/utils.py
CHANGED
|
@@ -13,6 +13,7 @@ def get_package_version() -> str:
|
|
|
13
13
|
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
|
|
14
14
|
with pyproject_path.open("rb") as f:
|
|
15
15
|
pyproject_data = tomllib.load(f)
|
|
16
|
-
|
|
16
|
+
version_value = pyproject_data["project"]["version"]
|
|
17
|
+
return str(version_value)
|
|
17
18
|
except Exception:
|
|
18
19
|
return "unknown"
|
crackerjack/code_cleaner.py
CHANGED
|
@@ -22,31 +22,31 @@ from .services.security_logger import (
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class SafePatternApplicator:
|
|
25
|
-
"""Safe pattern applicator using centralized SAFE_PATTERNS."""
|
|
26
|
-
|
|
27
25
|
def apply_docstring_patterns(self, code: str) -> str:
|
|
28
|
-
"""Apply docstring removal patterns safely."""
|
|
29
26
|
result = code
|
|
30
27
|
result = SAFE_PATTERNS["docstring_triple_double"].apply(result)
|
|
31
28
|
result = SAFE_PATTERNS["docstring_triple_single"].apply(result)
|
|
32
29
|
return result
|
|
33
30
|
|
|
34
31
|
def apply_formatting_patterns(self, content: str) -> str:
|
|
35
|
-
"""Apply formatting patterns safely."""
|
|
36
|
-
# Apply spacing patterns
|
|
37
32
|
content = SAFE_PATTERNS["spacing_after_comma"].apply(content)
|
|
38
33
|
content = SAFE_PATTERNS["spacing_after_colon"].apply(content)
|
|
39
34
|
content = SAFE_PATTERNS["multiple_spaces"].apply(content)
|
|
40
35
|
return content
|
|
41
36
|
|
|
42
37
|
def has_preserved_comment(self, line: str) -> bool:
|
|
43
|
-
"""Check if a line contains preserved comments."""
|
|
44
38
|
if line.strip().startswith("#! /"):
|
|
45
39
|
return True
|
|
46
40
|
|
|
47
|
-
# Check for preserved comment keywords
|
|
48
41
|
line_lower = line.lower()
|
|
49
|
-
preserved_keywords = [
|
|
42
|
+
preserved_keywords = [
|
|
43
|
+
"coding: ",
|
|
44
|
+
"encoding: ",
|
|
45
|
+
"type: ",
|
|
46
|
+
"noqa",
|
|
47
|
+
"pragma",
|
|
48
|
+
"regex ok",
|
|
49
|
+
]
|
|
50
50
|
return any(keyword in line_lower for keyword in preserved_keywords)
|
|
51
51
|
|
|
52
52
|
|
|
@@ -421,22 +421,13 @@ class CodeCleaner(BaseModel):
|
|
|
421
421
|
self._create_formatting_step(),
|
|
422
422
|
]
|
|
423
423
|
|
|
424
|
-
|
|
424
|
+
result = self.pipeline.clean_file(file_path, cleaning_steps)
|
|
425
|
+
return t.cast(CleaningResult, result)
|
|
425
426
|
|
|
426
427
|
def clean_files(
|
|
427
428
|
self, pkg_dir: Path | None = None, use_backup: bool = True
|
|
428
429
|
) -> list[CleaningResult] | PackageCleaningResult:
|
|
429
|
-
"""Clean package files with optional backup protection.
|
|
430
|
-
|
|
431
|
-
Args:
|
|
432
|
-
pkg_dir: Package directory to clean (defaults to current directory)
|
|
433
|
-
use_backup: Whether to use backup protection (default: True for safety)
|
|
434
|
-
|
|
435
|
-
Returns:
|
|
436
|
-
PackageCleaningResult with backup protection (default), list[CleaningResult] if use_backup=False (legacy)
|
|
437
|
-
"""
|
|
438
430
|
if use_backup:
|
|
439
|
-
# Use the comprehensive backup system for maximum safety
|
|
440
431
|
package_result = self.clean_files_with_backup(pkg_dir)
|
|
441
432
|
self.logger.info(
|
|
442
433
|
f"Package cleaning with backup completed: "
|
|
@@ -445,7 +436,6 @@ class CodeCleaner(BaseModel):
|
|
|
445
436
|
)
|
|
446
437
|
return package_result
|
|
447
438
|
|
|
448
|
-
# Legacy non-backup mode (deprecated, kept for compatibility)
|
|
449
439
|
self.console.print(
|
|
450
440
|
"[yellow]⚠️ WARNING: Running without backup protection. "
|
|
451
441
|
"Consider using use_backup=True for safety.[/yellow]"
|
|
@@ -519,9 +509,10 @@ class CodeCleaner(BaseModel):
|
|
|
519
509
|
"[yellow]📦 Creating backup of all package files...[/yellow]"
|
|
520
510
|
)
|
|
521
511
|
|
|
522
|
-
|
|
512
|
+
backup_result = self.backup_service.create_package_backup(
|
|
523
513
|
validated_pkg_dir, self.base_directory
|
|
524
514
|
)
|
|
515
|
+
backup_metadata: BackupMetadata = t.cast(BackupMetadata, backup_result)
|
|
525
516
|
|
|
526
517
|
self.console.print(
|
|
527
518
|
f"[green]✅ Backup created: {backup_metadata.backup_id}[/green] "
|
|
@@ -539,23 +530,9 @@ class CodeCleaner(BaseModel):
|
|
|
539
530
|
]
|
|
540
531
|
|
|
541
532
|
def _discover_package_files(self, root_dir: Path) -> list[Path]:
|
|
542
|
-
"""Discover Python files in the main package directory using crackerjack naming convention.
|
|
543
|
-
|
|
544
|
-
Crackerjack convention:
|
|
545
|
-
- Project name with dashes → package name with underscores
|
|
546
|
-
- Single word → same name lowercase
|
|
547
|
-
- Package directory determined from pyproject.toml [project.name]
|
|
548
|
-
|
|
549
|
-
Args:
|
|
550
|
-
root_dir: Project root directory
|
|
551
|
-
|
|
552
|
-
Returns:
|
|
553
|
-
List of Python files found only in the main package directory
|
|
554
|
-
"""
|
|
555
533
|
package_dir = self._find_package_directory(root_dir)
|
|
556
534
|
|
|
557
535
|
if not package_dir or not package_dir.exists():
|
|
558
|
-
# Fallback: look for any directory with __init__.py (excluding common non-package dirs)
|
|
559
536
|
self.console.print(
|
|
560
537
|
"[yellow]⚠️ Could not determine package directory, searching for Python packages...[/yellow]"
|
|
561
538
|
)
|
|
@@ -563,10 +540,8 @@ class CodeCleaner(BaseModel):
|
|
|
563
540
|
|
|
564
541
|
self.logger.debug(f"Using package directory: {package_dir}")
|
|
565
542
|
|
|
566
|
-
|
|
567
|
-
package_files = list(package_dir.rglob("*.py"))
|
|
543
|
+
package_files = list[t.Any](package_dir.rglob("*.py"))
|
|
568
544
|
|
|
569
|
-
# Filter out any problematic subdirectories that might exist within the package
|
|
570
545
|
exclude_dirs = {
|
|
571
546
|
"__pycache__",
|
|
572
547
|
".pytest_cache",
|
|
@@ -584,15 +559,6 @@ class CodeCleaner(BaseModel):
|
|
|
584
559
|
return filtered_files
|
|
585
560
|
|
|
586
561
|
def _find_package_directory(self, root_dir: Path) -> Path | None:
|
|
587
|
-
"""Find the main package directory using crackerjack naming convention.
|
|
588
|
-
|
|
589
|
-
Args:
|
|
590
|
-
root_dir: Project root directory
|
|
591
|
-
|
|
592
|
-
Returns:
|
|
593
|
-
Path to package directory or None if not found
|
|
594
|
-
"""
|
|
595
|
-
# First, try to get project name from pyproject.toml
|
|
596
562
|
pyproject_path = root_dir / "pyproject.toml"
|
|
597
563
|
if pyproject_path.exists():
|
|
598
564
|
try:
|
|
@@ -601,9 +567,9 @@ class CodeCleaner(BaseModel):
|
|
|
601
567
|
with pyproject_path.open("rb") as f:
|
|
602
568
|
config = tomllib.load(f)
|
|
603
569
|
|
|
604
|
-
|
|
570
|
+
project_name_raw = config.get("project", {}).get("name")
|
|
571
|
+
project_name: str | None = t.cast(str | None, project_name_raw)
|
|
605
572
|
if project_name:
|
|
606
|
-
# Apply crackerjack naming convention
|
|
607
573
|
package_name = project_name.replace("-", "_").lower()
|
|
608
574
|
package_dir = root_dir / package_name
|
|
609
575
|
|
|
@@ -613,7 +579,6 @@ class CodeCleaner(BaseModel):
|
|
|
613
579
|
except Exception as e:
|
|
614
580
|
self.logger.debug(f"Could not parse pyproject.toml: {e}")
|
|
615
581
|
|
|
616
|
-
# Fallback: infer from directory name
|
|
617
582
|
package_name = root_dir.name.replace("-", "_").lower()
|
|
618
583
|
package_dir = root_dir / package_name
|
|
619
584
|
|
|
@@ -623,7 +588,6 @@ class CodeCleaner(BaseModel):
|
|
|
623
588
|
return None
|
|
624
589
|
|
|
625
590
|
def _fallback_discover_packages(self, root_dir: Path) -> list[Path]:
|
|
626
|
-
"""Fallback method to discover package files when convention-based detection fails."""
|
|
627
591
|
python_files = []
|
|
628
592
|
exclude_dirs = {
|
|
629
593
|
"__pycache__",
|
|
@@ -669,11 +633,8 @@ class CodeCleaner(BaseModel):
|
|
|
669
633
|
def _should_include_file_path(
|
|
670
634
|
self, file_path: Path, exclude_dirs: set[str]
|
|
671
635
|
) -> bool:
|
|
672
|
-
|
|
673
|
-
# Convert path parts to set for efficient lookup
|
|
674
|
-
path_parts = set(file_path.parts)
|
|
636
|
+
path_parts = set[t.Any](file_path.parts)
|
|
675
637
|
|
|
676
|
-
# If any part of the path is in exclude_dirs, exclude it
|
|
677
638
|
return not bool(path_parts.intersection(exclude_dirs))
|
|
678
639
|
|
|
679
640
|
def _handle_no_files_to_process(
|
|
@@ -864,11 +825,6 @@ class CodeCleaner(BaseModel):
|
|
|
864
825
|
return False
|
|
865
826
|
|
|
866
827
|
def restore_from_backup_metadata(self, backup_metadata: BackupMetadata) -> None:
|
|
867
|
-
"""Manually restore from backup metadata.
|
|
868
|
-
|
|
869
|
-
Args:
|
|
870
|
-
backup_metadata: Backup metadata containing restoration information
|
|
871
|
-
"""
|
|
872
828
|
self.console.print(
|
|
873
829
|
f"[yellow]🔄 Manually restoring from backup: {backup_metadata.backup_id}[/yellow]"
|
|
874
830
|
)
|
|
@@ -881,14 +837,6 @@ class CodeCleaner(BaseModel):
|
|
|
881
837
|
)
|
|
882
838
|
|
|
883
839
|
def create_emergency_backup(self, pkg_dir: Path | None = None) -> BackupMetadata:
|
|
884
|
-
"""Create an emergency backup before potentially risky operations.
|
|
885
|
-
|
|
886
|
-
Args:
|
|
887
|
-
pkg_dir: Package directory to backup (defaults to current directory)
|
|
888
|
-
|
|
889
|
-
Returns:
|
|
890
|
-
BackupMetadata for the created backup
|
|
891
|
-
"""
|
|
892
840
|
validated_pkg_dir = self._prepare_package_directory(pkg_dir)
|
|
893
841
|
|
|
894
842
|
self.console.print(
|
|
@@ -904,14 +852,6 @@ class CodeCleaner(BaseModel):
|
|
|
904
852
|
return backup_metadata
|
|
905
853
|
|
|
906
854
|
def restore_emergency_backup(self, backup_metadata: BackupMetadata) -> bool:
|
|
907
|
-
"""Restore from an emergency backup with enhanced error handling.
|
|
908
|
-
|
|
909
|
-
Args:
|
|
910
|
-
backup_metadata: Backup metadata for restoration
|
|
911
|
-
|
|
912
|
-
Returns:
|
|
913
|
-
True if restoration succeeded, False otherwise
|
|
914
|
-
"""
|
|
915
855
|
try:
|
|
916
856
|
self.console.print(
|
|
917
857
|
f"[yellow]🔄 Restoring emergency backup: {backup_metadata.backup_id}[/yellow]"
|
|
@@ -938,14 +878,6 @@ class CodeCleaner(BaseModel):
|
|
|
938
878
|
return False
|
|
939
879
|
|
|
940
880
|
def verify_backup_integrity(self, backup_metadata: BackupMetadata) -> bool:
|
|
941
|
-
"""Verify the integrity of a backup without restoring it.
|
|
942
|
-
|
|
943
|
-
Args:
|
|
944
|
-
backup_metadata: Backup metadata to verify
|
|
945
|
-
|
|
946
|
-
Returns:
|
|
947
|
-
True if backup is valid and can be restored, False otherwise
|
|
948
|
-
"""
|
|
949
881
|
try:
|
|
950
882
|
validation_result = self.backup_service._validate_backup(backup_metadata)
|
|
951
883
|
|
|
@@ -960,14 +892,12 @@ class CodeCleaner(BaseModel):
|
|
|
960
892
|
f"[red]❌ Backup verification failed: {backup_metadata.backup_id}[/red]"
|
|
961
893
|
)
|
|
962
894
|
|
|
963
|
-
for error in validation_result.validation_errors[
|
|
964
|
-
|
|
965
|
-
]: # Show first 3 errors
|
|
966
|
-
self.console.print(f"[red] • {error}[/red]")
|
|
895
|
+
for error in validation_result.validation_errors[:3]:
|
|
896
|
+
self.console.print(f"[red] • {error}[/red]")
|
|
967
897
|
|
|
968
898
|
if len(validation_result.validation_errors) > 3:
|
|
969
899
|
remaining = len(validation_result.validation_errors) - 3
|
|
970
|
-
self.console.print(f"[red]
|
|
900
|
+
self.console.print(f"[red] ... and {remaining} more errors[/red]")
|
|
971
901
|
|
|
972
902
|
return False
|
|
973
903
|
|
|
@@ -977,11 +907,6 @@ class CodeCleaner(BaseModel):
|
|
|
977
907
|
return False
|
|
978
908
|
|
|
979
909
|
def list_available_backups(self) -> list[Path]:
|
|
980
|
-
"""List all available backup directories.
|
|
981
|
-
|
|
982
|
-
Returns:
|
|
983
|
-
List of backup directory paths
|
|
984
|
-
"""
|
|
985
910
|
if (
|
|
986
911
|
not self.backup_service.backup_root
|
|
987
912
|
or not self.backup_service.backup_root.exists()
|
|
@@ -998,17 +923,17 @@ class CodeCleaner(BaseModel):
|
|
|
998
923
|
|
|
999
924
|
if backup_dirs:
|
|
1000
925
|
self.console.print(
|
|
1001
|
-
f"[cyan]📦 Found {len(backup_dirs)} available backups:[/cyan]"
|
|
926
|
+
f"[cyan]📦 Found {len(backup_dirs)} available backups: [/cyan]"
|
|
1002
927
|
)
|
|
1003
928
|
for backup_dir in sorted(backup_dirs):
|
|
1004
|
-
self.console.print(f"
|
|
929
|
+
self.console.print(f" • {backup_dir.name}")
|
|
1005
930
|
else:
|
|
1006
931
|
self.console.print("[yellow]⚠️ No backups found[/yellow]")
|
|
1007
932
|
|
|
1008
933
|
return backup_dirs
|
|
1009
934
|
|
|
1010
935
|
except Exception as e:
|
|
1011
|
-
self.logger.error(f"Failed to list backups: {e}")
|
|
936
|
+
self.logger.error(f"Failed to list[t.Any] backups: {e}")
|
|
1012
937
|
self.console.print(f"[red]💥 Error listing backups: {e}[/red]")
|
|
1013
938
|
return []
|
|
1014
939
|
|
|
@@ -1172,33 +1097,50 @@ class CodeCleaner(BaseModel):
|
|
|
1172
1097
|
return _safe_applicator.has_preserved_comment(stripped_line)
|
|
1173
1098
|
|
|
1174
1099
|
def _remove_comment_from_line(self, line: str) -> str:
|
|
1175
|
-
|
|
1100
|
+
"""Remove comment from line while preserving strings."""
|
|
1101
|
+
if not self._line_needs_comment_processing(line):
|
|
1176
1102
|
return line
|
|
1177
1103
|
|
|
1104
|
+
return self._process_line_for_comment_removal(line)
|
|
1105
|
+
|
|
1106
|
+
def _line_needs_comment_processing(self, line: str) -> bool:
|
|
1107
|
+
"""Check if line needs comment processing."""
|
|
1108
|
+
return '"' in line or "'" in line or "#" in line
|
|
1109
|
+
|
|
1110
|
+
def _process_line_for_comment_removal(self, line: str) -> str:
|
|
1111
|
+
"""Process line to remove comments while preserving strings."""
|
|
1178
1112
|
result_chars = []
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
i
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
while i < length:
|
|
1185
|
-
char = line[i]
|
|
1186
|
-
|
|
1187
|
-
if not in_string:
|
|
1188
|
-
if char == "#":
|
|
1189
|
-
break
|
|
1190
|
-
elif char in ('"', "'"):
|
|
1191
|
-
in_string = True
|
|
1192
|
-
quote_char = char
|
|
1193
|
-
elif char == quote_char and (i == 0 or line[i - 1] != "\\"):
|
|
1194
|
-
in_string = False
|
|
1195
|
-
quote_char = None
|
|
1113
|
+
string_state = {"in_string": False, "quote_char": None}
|
|
1114
|
+
|
|
1115
|
+
for i, char in enumerate(line):
|
|
1116
|
+
if self._should_break_for_comment(char, string_state):
|
|
1117
|
+
break
|
|
1196
1118
|
|
|
1119
|
+
self._update_string_state(char, i, line, string_state)
|
|
1197
1120
|
result_chars.append(char)
|
|
1198
|
-
i += 1
|
|
1199
1121
|
|
|
1200
1122
|
return "".join(result_chars).rstrip()
|
|
1201
1123
|
|
|
1124
|
+
def _should_break_for_comment(
|
|
1125
|
+
self, char: str, string_state: dict[str, t.Any]
|
|
1126
|
+
) -> bool:
|
|
1127
|
+
"""Check if we should break for a comment character."""
|
|
1128
|
+
return not string_state["in_string"] and char == "#"
|
|
1129
|
+
|
|
1130
|
+
def _update_string_state(
|
|
1131
|
+
self, char: str, index: int, line: str, string_state: dict[str, t.Any]
|
|
1132
|
+
) -> None:
|
|
1133
|
+
"""Update string parsing state."""
|
|
1134
|
+
if not string_state["in_string"]:
|
|
1135
|
+
if char in ('"', "'"):
|
|
1136
|
+
string_state["in_string"] = True
|
|
1137
|
+
string_state["quote_char"] = char
|
|
1138
|
+
elif char == string_state["quote_char"] and (
|
|
1139
|
+
index == 0 or line[index - 1] != "\\"
|
|
1140
|
+
):
|
|
1141
|
+
string_state["in_string"] = False
|
|
1142
|
+
string_state["quote_char"] = None
|
|
1143
|
+
|
|
1202
1144
|
def _create_docstring_finder_class(
|
|
1203
1145
|
self,
|
|
1204
1146
|
docstring_nodes: list[ast.AST],
|
|
@@ -1258,7 +1200,6 @@ class CodeCleaner(BaseModel):
|
|
|
1258
1200
|
)
|
|
1259
1201
|
content = cleaned_line.lstrip()
|
|
1260
1202
|
|
|
1261
|
-
# Use SAFE_PATTERNS for multiple spaces replacement
|
|
1262
1203
|
content = SAFE_PATTERNS["multiple_spaces"].apply(content)
|
|
1263
1204
|
|
|
1264
1205
|
cleaned_line = cleaned_line[:leading_whitespace] + content
|
|
@@ -8,35 +8,24 @@ from ..models.protocols import OptionsProtocol
|
|
|
8
8
|
|
|
9
9
|
@dataclass
|
|
10
10
|
class GlobalLockConfig:
|
|
11
|
-
"""Configuration for global hook locking system."""
|
|
12
|
-
|
|
13
11
|
enabled: bool = True
|
|
14
|
-
timeout_seconds: float = 600.0
|
|
15
|
-
stale_lock_hours: float = 2.0
|
|
12
|
+
timeout_seconds: float = 600.0
|
|
13
|
+
stale_lock_hours: float = 2.0
|
|
16
14
|
lock_directory: Path = field(
|
|
17
15
|
default_factory=lambda: Path.home() / ".crackerjack" / "locks"
|
|
18
16
|
)
|
|
19
|
-
session_heartbeat_interval: float = 30.0
|
|
17
|
+
session_heartbeat_interval: float = 30.0
|
|
20
18
|
max_retry_attempts: int = 3
|
|
21
19
|
retry_delay_seconds: float = 5.0
|
|
22
20
|
enable_lock_monitoring: bool = True
|
|
23
21
|
|
|
24
22
|
@classmethod
|
|
25
|
-
def from_options(
|
|
26
|
-
""
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
options: Options object containing CLI arguments
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
Configured GlobalLockConfig instance
|
|
33
|
-
"""
|
|
34
|
-
# Import here to avoid circular import
|
|
35
|
-
|
|
36
|
-
# Create config with custom values from options
|
|
23
|
+
def from_options(
|
|
24
|
+
cls: type["GlobalLockConfig"], options: "OptionsProtocol"
|
|
25
|
+
) -> "GlobalLockConfig":
|
|
37
26
|
enabled = not options.disable_global_locks
|
|
38
27
|
timeout_seconds = float(options.global_lock_timeout)
|
|
39
|
-
|
|
28
|
+
|
|
40
29
|
default_lock_dir = Path.home() / ".crackerjack" / "locks"
|
|
41
30
|
lock_directory = (
|
|
42
31
|
Path(options.global_lock_dir)
|
|
@@ -44,7 +33,6 @@ class GlobalLockConfig:
|
|
|
44
33
|
else default_lock_dir
|
|
45
34
|
)
|
|
46
35
|
|
|
47
|
-
# Create instance with all parameters so __post_init__ is called
|
|
48
36
|
config = cls(
|
|
49
37
|
enabled=enabled,
|
|
50
38
|
timeout_seconds=timeout_seconds,
|
|
@@ -54,56 +42,28 @@ class GlobalLockConfig:
|
|
|
54
42
|
return config
|
|
55
43
|
|
|
56
44
|
def __post_init__(self) -> None:
|
|
57
|
-
"""Ensure lock directory exists and has proper permissions."""
|
|
58
45
|
self.lock_directory.mkdir(parents=True, exist_ok=True)
|
|
59
|
-
|
|
46
|
+
|
|
60
47
|
self.lock_directory.chmod(0o700)
|
|
61
48
|
|
|
62
49
|
@property
|
|
63
50
|
def hostname(self) -> str:
|
|
64
|
-
"""Get the current hostname for lock identification."""
|
|
65
51
|
return socket.gethostname()
|
|
66
52
|
|
|
67
53
|
@property
|
|
68
54
|
def session_id(self) -> str:
|
|
69
|
-
"""Get a unique session identifier."""
|
|
70
55
|
return f"{self.hostname}_{os.getpid()}"
|
|
71
56
|
|
|
72
57
|
def get_lock_path(self, hook_name: str) -> Path:
|
|
73
|
-
"""Get the lock file path for a specific hook.
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
hook_name: Name of the hook
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
Path to the lock file
|
|
80
|
-
"""
|
|
81
58
|
return self.lock_directory / f"{hook_name}.lock"
|
|
82
59
|
|
|
83
60
|
def get_heartbeat_path(self, hook_name: str) -> Path:
|
|
84
|
-
"""Get the heartbeat file path for a specific hook.
|
|
85
|
-
|
|
86
|
-
Args:
|
|
87
|
-
hook_name: Name of the hook
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
Path to the heartbeat file
|
|
91
|
-
"""
|
|
92
61
|
return self.lock_directory / f"{hook_name}.heartbeat"
|
|
93
62
|
|
|
94
63
|
def is_valid_lock_file(self, lock_path: Path) -> bool:
|
|
95
|
-
"""Check if a lock file is valid (not stale).
|
|
96
|
-
|
|
97
|
-
Args:
|
|
98
|
-
lock_path: Path to the lock file
|
|
99
|
-
|
|
100
|
-
Returns:
|
|
101
|
-
True if the lock file is valid and not stale
|
|
102
|
-
"""
|
|
103
64
|
if not lock_path.exists():
|
|
104
65
|
return False
|
|
105
66
|
|
|
106
|
-
# Check if the lock file is older than stale_lock_hours
|
|
107
67
|
import time
|
|
108
68
|
|
|
109
69
|
file_age_hours = (time.time() - lock_path.stat().st_mtime) / 3600
|