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.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +227 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +170 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +657 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +409 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +618 -928
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +585 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +826 -0
- crackerjack/dynamic_config.py +94 -103
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +433 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +443 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +114 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +621 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +372 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +217 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +565 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/coverage_improvement.py +223 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +358 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +356 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +421 -0
- crackerjack/services/git.py +176 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +873 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.7.dist-info/METADATA +742 -0
- crackerjack-0.31.7.dist-info/RECORD +149 -0
- crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/WHEEL +0 -0
- {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
|