crackerjack 0.30.3__py3-none-any.whl → 0.31.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of crackerjack might be problematic. Click here for more details.

Files changed (156) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +227 -299
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +170 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +657 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +409 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +618 -928
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +585 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +826 -0
  40. crackerjack/dynamic_config.py +94 -103
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +433 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +443 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +114 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +621 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +372 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +217 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +565 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/coverage_improvement.py +223 -0
  107. crackerjack/orchestration/execution_strategies.py +341 -0
  108. crackerjack/orchestration/test_progress_streamer.py +636 -0
  109. crackerjack/plugins/__init__.py +15 -0
  110. crackerjack/plugins/base.py +200 -0
  111. crackerjack/plugins/hooks.py +246 -0
  112. crackerjack/plugins/loader.py +335 -0
  113. crackerjack/plugins/managers.py +259 -0
  114. crackerjack/py313.py +8 -3
  115. crackerjack/services/__init__.py +22 -0
  116. crackerjack/services/cache.py +314 -0
  117. crackerjack/services/config.py +358 -0
  118. crackerjack/services/config_integrity.py +99 -0
  119. crackerjack/services/contextual_ai_assistant.py +516 -0
  120. crackerjack/services/coverage_ratchet.py +356 -0
  121. crackerjack/services/debug.py +736 -0
  122. crackerjack/services/dependency_monitor.py +617 -0
  123. crackerjack/services/enhanced_filesystem.py +439 -0
  124. crackerjack/services/file_hasher.py +151 -0
  125. crackerjack/services/filesystem.py +421 -0
  126. crackerjack/services/git.py +176 -0
  127. crackerjack/services/health_metrics.py +611 -0
  128. crackerjack/services/initialization.py +873 -0
  129. crackerjack/services/log_manager.py +286 -0
  130. crackerjack/services/logging.py +174 -0
  131. crackerjack/services/metrics.py +578 -0
  132. crackerjack/services/pattern_cache.py +362 -0
  133. crackerjack/services/pattern_detector.py +515 -0
  134. crackerjack/services/performance_benchmarks.py +653 -0
  135. crackerjack/services/security.py +163 -0
  136. crackerjack/services/server_manager.py +234 -0
  137. crackerjack/services/smart_scheduling.py +144 -0
  138. crackerjack/services/tool_version_service.py +61 -0
  139. crackerjack/services/unified_config.py +437 -0
  140. crackerjack/services/version_checker.py +248 -0
  141. crackerjack/slash_commands/__init__.py +14 -0
  142. crackerjack/slash_commands/init.md +122 -0
  143. crackerjack/slash_commands/run.md +163 -0
  144. crackerjack/slash_commands/status.md +127 -0
  145. crackerjack-0.31.7.dist-info/METADATA +742 -0
  146. crackerjack-0.31.7.dist-info/RECORD +149 -0
  147. crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
  148. crackerjack/.gitignore +0 -34
  149. crackerjack/.libcst.codemod.yaml +0 -18
  150. crackerjack/.pdm.toml +0 -1
  151. crackerjack/crackerjack.py +0 -3805
  152. crackerjack/pyproject.toml +0 -286
  153. crackerjack-0.30.3.dist-info/METADATA +0 -1290
  154. crackerjack-0.30.3.dist-info/RECORD +0 -16
  155. {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/WHEEL +0 -0
  156. {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,163 @@
1
+ import os
2
+ import re
3
+ import tempfile
4
+ from contextlib import suppress
5
+ from pathlib import Path
6
+
7
+ from crackerjack.errors import FileError, SecurityError
8
+
9
+
10
+ class SecurityService:
11
+ TOKEN_PATTERNS = [
12
+ (r"pypi-[a-zA-Z0-9_-]{12,}", "pypi-****"),
13
+ (r"ghp_[a-zA-Z0-9]{20,}", "ghp_****"),
14
+ (r"[a-zA-Z0-9_-]{20,}", "****"),
15
+ (r"(token[=:]\s*)['\"][^'\"]+['\"]", r"\1'****'"),
16
+ (r"(password[=:]\s*)['\"][^'\"]+['\"]", r"\1'****'"),
17
+ ]
18
+
19
+ SENSITIVE_ENV_VARS = {
20
+ "UV_PUBLISH_TOKEN",
21
+ "PYPI_TOKEN",
22
+ "GITHUB_TOKEN",
23
+ "AUTH_TOKEN",
24
+ "API_KEY",
25
+ "SECRET_KEY",
26
+ "PASSWORD",
27
+ }
28
+
29
+ def mask_tokens(self, text: str) -> str:
30
+ if not text:
31
+ return text
32
+ masked_text = text
33
+ for pattern, replacement in self.TOKEN_PATTERNS:
34
+ masked_text = re.sub(pattern, replacement, masked_text, flags=re.IGNORECASE)
35
+ for env_var in self.SENSITIVE_ENV_VARS:
36
+ value = os.getenv(env_var)
37
+ if value and len(value) > 8:
38
+ masked_value = (
39
+ f"{value[:4]}...{value[-4:]}" if len(value) > 12 else "****"
40
+ )
41
+ masked_text = masked_text.replace(value, masked_value)
42
+
43
+ return masked_text
44
+
45
+ def mask_command_output(self, stdout: str, stderr: str) -> tuple[str, str]:
46
+ return self.mask_tokens(stdout), self.mask_tokens(stderr)
47
+
48
+ def create_secure_token_file(
49
+ self,
50
+ token: str,
51
+ prefix: str = "crackerjack_token",
52
+ ) -> Path:
53
+ if not token:
54
+ raise SecurityError(
55
+ message="Invalid token provided",
56
+ details="Token must be a non-empty string",
57
+ recovery="Provide a valid authentication token",
58
+ )
59
+ if len(token) < 8:
60
+ raise SecurityError(
61
+ message="Token appears too short to be valid",
62
+ details=f"Token length: {len(token)} characters",
63
+ recovery="Ensure you're using a full authentication token",
64
+ )
65
+ try:
66
+ fd, temp_path = tempfile.mkstemp(
67
+ prefix=f"{prefix}_",
68
+ suffix=".token",
69
+ text=True,
70
+ )
71
+ temp_file = Path(temp_path)
72
+ try:
73
+ temp_file.chmod(0o600)
74
+ except OSError as e:
75
+ with suppress(OSError):
76
+ temp_file.unlink()
77
+ raise FileError(
78
+ message="Failed to set secure file permissions",
79
+ details=str(e),
80
+ recovery="Check file system permissions and try again",
81
+ ) from e
82
+ try:
83
+ with os.fdopen(fd, "w") as f:
84
+ f.write(token)
85
+ except OSError as e:
86
+ with suppress(OSError):
87
+ temp_file.unlink()
88
+ raise FileError(
89
+ message="Failed to write token to secure file",
90
+ details=str(e),
91
+ recovery="Check disk space and file system integrity",
92
+ ) from e
93
+
94
+ return temp_file
95
+ except OSError as e:
96
+ raise FileError(
97
+ message="Failed to create secure token file",
98
+ details=str(e),
99
+ recovery="Check temporary directory permissions and disk space",
100
+ ) from e
101
+
102
+ def cleanup_token_file(self, token_file: Path) -> None:
103
+ if not token_file or not token_file.exists():
104
+ return
105
+ with suppress(OSError):
106
+ if token_file.is_file():
107
+ with token_file.open("w") as f:
108
+ f.write("0" * max(1024, token_file.stat().st_size))
109
+ f.flush()
110
+ os.fsync(f.fileno())
111
+ token_file.unlink()
112
+
113
+ def get_masked_env_summary(self) -> dict[str, str]:
114
+ env_summary = {}
115
+ for key, value in os.environ.items():
116
+ if any(sensitive in key.upper() for sensitive in self.SENSITIVE_ENV_VARS):
117
+ if value:
118
+ env_summary[key] = (
119
+ "****" if len(value) <= 8 else f"{value[:2]}...{value[-2:]}"
120
+ )
121
+ else:
122
+ env_summary[key] = "(empty)"
123
+ elif key.startswith(("PATH", "HOME", "USER", "SHELL", "TERM")):
124
+ env_summary[key] = value
125
+
126
+ return env_summary
127
+
128
+ def validate_token_format(self, token: str, token_type: str | None = None) -> bool:
129
+ if not token:
130
+ return False
131
+ if len(token) < 8:
132
+ return False
133
+ if token_type and token_type.lower() == "pypi":
134
+ return token.startswith("pypi-") and len(token) >= 16
135
+ if token_type and token_type.lower() == "github":
136
+ return token.startswith("ghp_") and len(token) == 40
137
+ return len(token) >= 16 and not token.isspace()
138
+
139
+ def create_secure_command_env(
140
+ self,
141
+ base_env: dict[str, str] | None = None,
142
+ additional_vars: dict[str, str] | None = None,
143
+ ) -> dict[str, str]:
144
+ if base_env is None:
145
+ base_env = os.environ.copy()
146
+
147
+ secure_env = base_env.copy()
148
+
149
+ if additional_vars:
150
+ secure_env.update(additional_vars)
151
+
152
+ dangerous_vars = [
153
+ "LD_PRELOAD",
154
+ "LD_LIBRARY_PATH",
155
+ "DYLD_INSERT_LIBRARIES",
156
+ "DYLD_LIBRARY_PATH",
157
+ "PYTHONPATH",
158
+ ]
159
+
160
+ for var in dangerous_vars:
161
+ secure_env.pop(var, None)
162
+
163
+ return secure_env
@@ -0,0 +1,234 @@
1
+ import os
2
+ import signal
3
+ import subprocess
4
+ import sys
5
+ import time
6
+ import typing as t
7
+ from pathlib import Path
8
+
9
+ from rich.console import Console
10
+
11
+
12
+ def find_mcp_server_processes() -> list[dict[str, t.Any]]:
13
+ """Find all running MCP server processes for this project."""
14
+ try:
15
+ result = subprocess.run(
16
+ ["ps", "aux"],
17
+ capture_output=True,
18
+ text=True,
19
+ check=True,
20
+ )
21
+
22
+ processes: list[dict[str, t.Any]] = []
23
+ str(Path.cwd())
24
+
25
+ for line in result.stdout.splitlines():
26
+ if "crackerjack" in line and "--start-mcp-server" in line:
27
+ parts = line.split()
28
+ if len(parts) >= 11:
29
+ try:
30
+ pid = int(parts[1])
31
+ processes.append(
32
+ {
33
+ "pid": pid,
34
+ "command": " ".join(parts[10:]),
35
+ "user": parts[0],
36
+ "cpu": parts[2],
37
+ "mem": parts[3],
38
+ },
39
+ )
40
+ except (ValueError, IndexError):
41
+ continue
42
+
43
+ return processes
44
+
45
+ except (subprocess.CalledProcessError, FileNotFoundError):
46
+ return []
47
+
48
+
49
+ def find_websocket_server_processes() -> list[dict[str, t.Any]]:
50
+ """Find all running WebSocket server processes for this project."""
51
+ try:
52
+ result = subprocess.run(
53
+ ["ps", "aux"],
54
+ capture_output=True,
55
+ text=True,
56
+ check=True,
57
+ )
58
+
59
+ processes: list[dict[str, t.Any]] = []
60
+
61
+ for line in result.stdout.splitlines():
62
+ if "crackerjack" in line and "--start-websocket-server" in line:
63
+ parts = line.split()
64
+ if len(parts) >= 11:
65
+ try:
66
+ pid = int(parts[1])
67
+ processes.append(
68
+ {
69
+ "pid": pid,
70
+ "command": " ".join(parts[10:]),
71
+ "user": parts[0],
72
+ "cpu": parts[2],
73
+ "mem": parts[3],
74
+ },
75
+ )
76
+ except (ValueError, IndexError):
77
+ continue
78
+
79
+ return processes
80
+
81
+ except (subprocess.CalledProcessError, FileNotFoundError):
82
+ return []
83
+
84
+
85
+ def stop_process(pid: int, force: bool = False) -> bool:
86
+ """Stop a process by PID."""
87
+ try:
88
+ if force:
89
+ os.kill(pid, signal.SIGKILL)
90
+ else:
91
+ os.kill(pid, signal.SIGTERM)
92
+
93
+ for _ in range(10):
94
+ try:
95
+ os.kill(pid, 0)
96
+ time.sleep(0.5)
97
+ except OSError:
98
+ return True
99
+
100
+ if not force:
101
+ os.kill(pid, signal.SIGKILL)
102
+ time.sleep(1)
103
+
104
+ return True
105
+
106
+ except (OSError, ProcessLookupError):
107
+ return True
108
+
109
+
110
+ def stop_mcp_server(console: Console | None = None) -> bool:
111
+ """Stop all MCP server processes."""
112
+ if console is None:
113
+ console = Console()
114
+
115
+ processes = find_mcp_server_processes()
116
+
117
+ if not processes:
118
+ console.print("[yellow]⚠️ No MCP server processes found[/yellow]")
119
+ return True
120
+
121
+ success = True
122
+ for proc in processes:
123
+ console.print(f"🛑 Stopping MCP server process {proc['pid']}")
124
+ if stop_process(proc["pid"]):
125
+ console.print(f"✅ Stopped process {proc['pid']}")
126
+ else:
127
+ console.print(f"❌ Failed to stop process {proc['pid']}")
128
+ success = False
129
+
130
+ return success
131
+
132
+
133
+ def stop_websocket_server(console: Console | None = None) -> bool:
134
+ """Stop all WebSocket server processes."""
135
+ if console is None:
136
+ console = Console()
137
+
138
+ processes = find_websocket_server_processes()
139
+
140
+ if not processes:
141
+ console.print("[yellow]⚠️ No WebSocket server processes found[/yellow]")
142
+ return True
143
+
144
+ success = True
145
+ for proc in processes:
146
+ console.print(f"🛑 Stopping WebSocket server process {proc['pid']}")
147
+ if stop_process(proc["pid"]):
148
+ console.print(f"✅ Stopped process {proc['pid']}")
149
+ else:
150
+ console.print(f"❌ Failed to stop process {proc['pid']}")
151
+ success = False
152
+
153
+ return success
154
+
155
+
156
+ def stop_all_servers(console: Console | None = None) -> bool:
157
+ """Stop all crackerjack server processes."""
158
+ if console is None:
159
+ console = Console()
160
+
161
+ mcp_success = stop_mcp_server(console)
162
+ websocket_success = stop_websocket_server(console)
163
+
164
+ return mcp_success and websocket_success
165
+
166
+
167
+ def restart_mcp_server(
168
+ websocket_port: int | None = None,
169
+ console: Console | None = None,
170
+ ) -> bool:
171
+ """Restart the MCP server."""
172
+ if console is None:
173
+ console = Console()
174
+
175
+ console.print("[bold cyan]🔄 Restarting MCP server...[/bold cyan]")
176
+
177
+ stop_mcp_server(console)
178
+
179
+ console.print("⏳ Waiting for cleanup...")
180
+ time.sleep(2)
181
+
182
+ console.print("🚀 Starting new MCP server...")
183
+ try:
184
+ cmd = [sys.executable, "-m", "crackerjack", "--start-mcp-server"]
185
+ if websocket_port:
186
+ cmd.extend(["--websocket-port", str(websocket_port)])
187
+
188
+ subprocess.Popen(
189
+ cmd,
190
+ stdout=subprocess.DEVNULL,
191
+ stderr=subprocess.DEVNULL,
192
+ start_new_session=True,
193
+ )
194
+
195
+ console.print("✅ MCP server restart initiated")
196
+ return True
197
+
198
+ except Exception as e:
199
+ console.print(f"❌ Failed to restart MCP server: {e}")
200
+ return False
201
+
202
+
203
+ def list_server_status(console: Console | None = None) -> None:
204
+ """List status of all crackerjack servers."""
205
+ if console is None:
206
+ console = Console()
207
+
208
+ console.print("[bold cyan]📊 Crackerjack Server Status[/bold cyan]")
209
+
210
+ mcp_processes = find_mcp_server_processes()
211
+ websocket_processes = find_websocket_server_processes()
212
+
213
+ if mcp_processes:
214
+ console.print("\n[bold green]MCP Servers:[/bold green]")
215
+ for proc in mcp_processes:
216
+ console.print(
217
+ f" • PID {proc['pid']} - CPU: {proc['cpu']}% - Memory: {proc['mem']}%",
218
+ )
219
+ console.print(f" Command: {proc['command']}")
220
+ else:
221
+ console.print("\n[yellow]MCP Servers: None running[/yellow]")
222
+
223
+ if websocket_processes:
224
+ console.print("\n[bold green]WebSocket Servers:[/bold green]")
225
+ for proc in websocket_processes:
226
+ console.print(
227
+ f" • PID {proc['pid']} - CPU: {proc['cpu']}% - Memory: {proc['mem']}%",
228
+ )
229
+ console.print(f" Command: {proc['command']}")
230
+ else:
231
+ console.print("\n[yellow]WebSocket Servers: None running[/yellow]")
232
+
233
+ if not mcp_processes and not websocket_processes:
234
+ console.print("\n[dim]No crackerjack servers currently running[/dim]")
@@ -0,0 +1,144 @@
1
+ """Smart scheduling service for automated initialization.
2
+
3
+ This module handles intelligent scheduling of crackerjack initialization based on
4
+ various triggers like time, commits, or activity. Split from tool_version_service.py.
5
+ """
6
+
7
+ import os
8
+ import subprocess
9
+ from datetime import datetime, timedelta
10
+ from pathlib import Path
11
+
12
+ from rich.console import Console
13
+
14
+
15
+ class SmartSchedulingService:
16
+ """Service for intelligent scheduling of crackerjack operations."""
17
+
18
+ def __init__(self, console: Console, project_path: Path) -> None:
19
+ self.console = console
20
+ self.project_path = project_path
21
+ self.cache_dir = Path.home() / ".cache" / "crackerjack"
22
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
23
+
24
+ def should_scheduled_init(self) -> bool:
25
+ """Check if initialization should run based on configured schedule."""
26
+ init_schedule = os.environ.get("CRACKERJACK_INIT_SCHEDULE", "weekly")
27
+
28
+ if init_schedule == "disabled":
29
+ return False
30
+
31
+ if init_schedule == "weekly":
32
+ return self._check_weekly_schedule()
33
+ if init_schedule == "commit-based":
34
+ return self._check_commit_based_schedule()
35
+ if init_schedule == "activity-based":
36
+ return self._check_activity_based_schedule()
37
+
38
+ return self._check_weekly_schedule()
39
+
40
+ def record_init_timestamp(self) -> None:
41
+ """Record the current timestamp as the last initialization time."""
42
+ timestamp_file = self.cache_dir / f"{self.project_path.name}.init_timestamp"
43
+ try:
44
+ timestamp_file.write_text(datetime.now().isoformat())
45
+ except OSError as e:
46
+ self.console.print(
47
+ f"[yellow]⚠️ Could not record init timestamp: {e}[/yellow]",
48
+ )
49
+
50
+ def _check_weekly_schedule(self) -> bool:
51
+ """Check if weekly initialization is due."""
52
+ init_day = os.environ.get("CRACKERJACK_INIT_DAY", "monday")
53
+ today = datetime.now().strftime("%A").lower()
54
+
55
+ if today == init_day.lower():
56
+ last_init = self._get_last_init_timestamp()
57
+ if datetime.now() - last_init > timedelta(days=6):
58
+ self.console.print(
59
+ f"[blue]📅 Weekly initialization scheduled for {init_day}[/blue]",
60
+ )
61
+ return True
62
+
63
+ return False
64
+
65
+ def _check_commit_based_schedule(self) -> bool:
66
+ """Check if initialization is due based on commit count."""
67
+ commits_since_init = self._count_commits_since_init()
68
+ threshold = int(os.environ.get("CRACKERJACK_INIT_COMMITS", "50"))
69
+
70
+ if commits_since_init >= threshold:
71
+ self.console.print(
72
+ f"[blue]📊 {commits_since_init} commits since last init "
73
+ f"(threshold: {threshold})[/blue]",
74
+ )
75
+ return True
76
+
77
+ return False
78
+
79
+ def _check_activity_based_schedule(self) -> bool:
80
+ """Check if initialization is due based on recent activity."""
81
+ if self._has_recent_activity() and self._days_since_init() >= 7:
82
+ self.console.print(
83
+ "[blue]⚡ Recent activity detected, initialization recommended[/blue]",
84
+ )
85
+ return True
86
+
87
+ return False
88
+
89
+ def _get_last_init_timestamp(self) -> datetime:
90
+ """Get the timestamp of the last initialization."""
91
+ timestamp_file = self.cache_dir / f"{self.project_path.name}.init_timestamp"
92
+
93
+ if timestamp_file.exists():
94
+ from contextlib import suppress
95
+
96
+ with suppress(OSError, ValueError):
97
+ timestamp_str = timestamp_file.read_text().strip()
98
+ return datetime.fromisoformat(timestamp_str)
99
+
100
+ return datetime.now() - timedelta(days=30)
101
+
102
+ def _count_commits_since_init(self) -> int:
103
+ """Count git commits since last initialization."""
104
+ since_date = self._get_last_init_timestamp().strftime("%Y-%m-%d")
105
+
106
+ try:
107
+ result = subprocess.run(
108
+ ["git", "log", f"--since={since_date}", "--oneline"],
109
+ cwd=self.project_path,
110
+ capture_output=True,
111
+ text=True,
112
+ timeout=10,
113
+ check=False,
114
+ )
115
+
116
+ if result.returncode == 0:
117
+ return len([line for line in result.stdout.strip().split("\n") if line])
118
+
119
+ except (FileNotFoundError, subprocess.TimeoutExpired):
120
+ pass
121
+
122
+ return 0
123
+
124
+ def _has_recent_activity(self) -> bool:
125
+ """Check if there has been recent git activity."""
126
+ try:
127
+ result = subprocess.run(
128
+ ["git", "log", "-1", "--since=24.hours", "--oneline"],
129
+ cwd=self.project_path,
130
+ capture_output=True,
131
+ text=True,
132
+ timeout=10,
133
+ check=False,
134
+ )
135
+
136
+ return result.returncode == 0 and bool(result.stdout.strip())
137
+
138
+ except (FileNotFoundError, subprocess.TimeoutExpired):
139
+ return False
140
+
141
+ def _days_since_init(self) -> int:
142
+ """Calculate days since last initialization."""
143
+ last_init = self._get_last_init_timestamp()
144
+ return (datetime.now() - last_init).days
@@ -0,0 +1,61 @@
1
+ """Tool version service - unified facade for version and configuration management.
2
+
3
+ This module provides a unified interface to various tool and configuration services.
4
+ The implementation has been split into focused modules following single responsibility principle.
5
+
6
+ REFACTORING NOTE: This file was reduced from 1353 lines to ~50 lines by splitting into:
7
+ - version_checker.py: Core version checking and comparison
8
+ - config_integrity.py: Configuration file integrity checking
9
+ - smart_scheduling.py: Intelligent scheduling for automated initialization
10
+ - (Additional services extracted into separate files)
11
+ """
12
+
13
+ from pathlib import Path
14
+
15
+ from rich.console import Console
16
+
17
+ from .config_integrity import ConfigIntegrityService
18
+ from .smart_scheduling import SmartSchedulingService
19
+ from .version_checker import VersionChecker, VersionInfo
20
+
21
+ # Re-export for backward compatibility
22
+ __all__ = [
23
+ "VersionInfo",
24
+ "ToolVersionService",
25
+ "ConfigIntegrityService",
26
+ "SmartSchedulingService",
27
+ ]
28
+
29
+
30
+ class ToolVersionService:
31
+ """Facade for tool version management services."""
32
+
33
+ def __init__(self, console: Console, project_path: Path | None = None) -> None:
34
+ self.console = console
35
+ self.project_path = project_path or Path.cwd()
36
+
37
+ # Initialize component services
38
+ self._version_checker = VersionChecker(console)
39
+ self._config_integrity = ConfigIntegrityService(console, self.project_path)
40
+ self._scheduling = SmartSchedulingService(console, self.project_path)
41
+
42
+ async def check_tool_updates(self) -> dict[str, VersionInfo]:
43
+ """Check for tool updates using the version checker service."""
44
+ return await self._version_checker.check_tool_updates()
45
+
46
+ def check_config_integrity(self) -> bool:
47
+ """Check configuration integrity using the config integrity service."""
48
+ return self._config_integrity.check_config_integrity()
49
+
50
+ def should_scheduled_init(self) -> bool:
51
+ """Check if scheduled initialization should run."""
52
+ return self._scheduling.should_scheduled_init()
53
+
54
+ def record_init_timestamp(self) -> None:
55
+ """Record initialization timestamp."""
56
+ self._scheduling.record_init_timestamp()
57
+
58
+
59
+ # For backward compatibility, maintain the other services here if needed
60
+ # They are primarily accessed through the facade now
61
+ ToolManager = ToolVersionService # Alias for compatibility