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
|
@@ -202,7 +202,7 @@ class AgentRegistry:
|
|
|
202
202
|
|
|
203
203
|
def _build_agent_data(self, lines: list[str], yaml_end: int) -> dict[str, t.Any]:
|
|
204
204
|
yaml_lines = lines[1:yaml_end]
|
|
205
|
-
agent_data = {}
|
|
205
|
+
agent_data: dict[str, t.Any] = {}
|
|
206
206
|
|
|
207
207
|
for line in yaml_lines:
|
|
208
208
|
if ": " in line:
|
|
@@ -213,36 +213,46 @@ class AgentRegistry:
|
|
|
213
213
|
return agent_data
|
|
214
214
|
|
|
215
215
|
def _infer_capabilities_from_agent(self, agent: SubAgent) -> set[AgentCapability]:
|
|
216
|
-
capabilities
|
|
217
|
-
|
|
216
|
+
"""Infer agent capabilities from class name using keyword mapping."""
|
|
218
217
|
class_name = agent.__class__.__name__.lower()
|
|
218
|
+
capability_mapping = self._get_agent_capability_mapping()
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if "refactor" in class_name:
|
|
225
|
-
capabilities.add(AgentCapability.REFACTORING)
|
|
226
|
-
if "test" in class_name:
|
|
227
|
-
capabilities.add(AgentCapability.TESTING)
|
|
228
|
-
if "security" in class_name:
|
|
229
|
-
capabilities.add(AgentCapability.SECURITY)
|
|
230
|
-
if "performance" in class_name:
|
|
231
|
-
capabilities.add(AgentCapability.PERFORMANCE)
|
|
232
|
-
if "documentation" in class_name or "doc" in class_name:
|
|
233
|
-
capabilities.add(AgentCapability.DOCUMENTATION)
|
|
234
|
-
if "format" in class_name:
|
|
235
|
-
capabilities.add(AgentCapability.FORMATTING)
|
|
236
|
-
if "import" in class_name:
|
|
237
|
-
capabilities.add(AgentCapability.CODE_ANALYSIS)
|
|
238
|
-
if "dry" in class_name:
|
|
239
|
-
capabilities.add(AgentCapability.REFACTORING)
|
|
220
|
+
capabilities = set()
|
|
221
|
+
for keywords, caps in capability_mapping:
|
|
222
|
+
if self._class_name_matches_keywords(class_name, keywords):
|
|
223
|
+
capabilities.update(caps)
|
|
240
224
|
|
|
225
|
+
# Fallback to default capability if none found
|
|
241
226
|
if not capabilities:
|
|
242
227
|
capabilities.add(AgentCapability.CODE_ANALYSIS)
|
|
243
228
|
|
|
244
229
|
return capabilities
|
|
245
230
|
|
|
231
|
+
def _get_agent_capability_mapping(
|
|
232
|
+
self,
|
|
233
|
+
) -> list[tuple[list[str], set[AgentCapability]]]:
|
|
234
|
+
"""Get mapping of keywords to agent capabilities."""
|
|
235
|
+
return [
|
|
236
|
+
(
|
|
237
|
+
["architect"],
|
|
238
|
+
{AgentCapability.ARCHITECTURE, AgentCapability.CODE_ANALYSIS},
|
|
239
|
+
),
|
|
240
|
+
(["refactor"], {AgentCapability.REFACTORING}),
|
|
241
|
+
(["test"], {AgentCapability.TESTING}),
|
|
242
|
+
(["security"], {AgentCapability.SECURITY}),
|
|
243
|
+
(["performance"], {AgentCapability.PERFORMANCE}),
|
|
244
|
+
(["documentation", "doc"], {AgentCapability.DOCUMENTATION}),
|
|
245
|
+
(["format"], {AgentCapability.FORMATTING}),
|
|
246
|
+
(["import"], {AgentCapability.CODE_ANALYSIS}),
|
|
247
|
+
(["dry"], {AgentCapability.REFACTORING}),
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
def _class_name_matches_keywords(
|
|
251
|
+
self, class_name: str, keywords: list[str]
|
|
252
|
+
) -> bool:
|
|
253
|
+
"""Check if class name contains any of the specified keywords."""
|
|
254
|
+
return any(keyword in class_name for keyword in keywords)
|
|
255
|
+
|
|
246
256
|
def _infer_capabilities_from_user_agent(
|
|
247
257
|
self, agent_data: dict[str, t.Any]
|
|
248
258
|
) -> set[AgentCapability]:
|
|
@@ -334,7 +344,7 @@ class AgentRegistry:
|
|
|
334
344
|
return self._agents.get(name)
|
|
335
345
|
|
|
336
346
|
def list_all_agents(self) -> list[RegisteredAgent]:
|
|
337
|
-
agents = list(self._agents.values())
|
|
347
|
+
agents = list[t.Any](self._agents.values())
|
|
338
348
|
agents.sort(key=lambda a: a.metadata.priority, reverse=True)
|
|
339
349
|
return agents
|
|
340
350
|
|
|
@@ -143,9 +143,7 @@ class AgentSelector:
|
|
|
143
143
|
|
|
144
144
|
capabilities = set()
|
|
145
145
|
for pattern, caps in self._task_patterns.items():
|
|
146
|
-
if re.search(
|
|
147
|
-
pattern, text, re.IGNORECASE
|
|
148
|
-
): # REGEX OK: dynamic pattern matching from config
|
|
146
|
+
if re.search(pattern, text, re.IGNORECASE):
|
|
149
147
|
capabilities.update(caps)
|
|
150
148
|
|
|
151
149
|
return capabilities
|
|
@@ -182,7 +180,7 @@ class AgentSelector:
|
|
|
182
180
|
TaskContext.GENERAL: [AgentCapability.CODE_ANALYSIS],
|
|
183
181
|
}
|
|
184
182
|
|
|
185
|
-
return set(context_map.get(task.context, []))
|
|
183
|
+
return set[t.Any](context_map.get(task.context, []))
|
|
186
184
|
|
|
187
185
|
def _analyze_file_patterns(self, task: TaskDescription) -> set[AgentCapability]:
|
|
188
186
|
if not task.file_patterns:
|
|
@@ -291,8 +289,8 @@ class AgentSelector:
|
|
|
291
289
|
if not agent.metadata.description:
|
|
292
290
|
return 0.0
|
|
293
291
|
|
|
294
|
-
desc_words = set(agent.metadata.description.lower().split())
|
|
295
|
-
task_words = set(task_text.split())
|
|
292
|
+
desc_words = set[t.Any](agent.metadata.description.lower().split())
|
|
293
|
+
task_words = set[t.Any](task_text.split())
|
|
296
294
|
common_words = desc_words & task_words
|
|
297
295
|
|
|
298
296
|
if common_words:
|
|
@@ -355,7 +353,7 @@ class AgentSelector:
|
|
|
355
353
|
parts.append(source_desc.get(agent.metadata.source.value, "Unknown source"))
|
|
356
354
|
|
|
357
355
|
if agent.metadata.capabilities:
|
|
358
|
-
top_caps = list(agent.metadata.capabilities)[:2]
|
|
356
|
+
top_caps = list[t.Any](agent.metadata.capabilities)[:2]
|
|
359
357
|
cap_names = [cap.value.replace("_", " ") for cap in top_caps]
|
|
360
358
|
parts.append(f"Strengths: {', '.join(cap_names)}")
|
|
361
359
|
|
crackerjack/interactive.py
CHANGED
|
@@ -37,12 +37,23 @@ class WorkflowOptions:
|
|
|
37
37
|
@classmethod
|
|
38
38
|
def from_args(cls, args: t.Any) -> "WorkflowOptions":
|
|
39
39
|
return cls(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
cleaning=CleaningConfig(
|
|
41
|
+
clean=getattr(args, "clean", False),
|
|
42
|
+
),
|
|
43
|
+
testing=TestConfig(
|
|
44
|
+
test=getattr(args, "test", False),
|
|
45
|
+
),
|
|
46
|
+
publishing=PublishConfig(
|
|
47
|
+
publish=getattr(args, "publish", None),
|
|
48
|
+
bump=getattr(args, "bump", None),
|
|
49
|
+
),
|
|
50
|
+
git=GitConfig(
|
|
51
|
+
commit=getattr(args, "commit", False),
|
|
52
|
+
create_pr=getattr(args, "create_pr", False),
|
|
53
|
+
),
|
|
54
|
+
ai=AIConfig(
|
|
55
|
+
ai_agent=getattr(args, "ai_agent", False),
|
|
56
|
+
),
|
|
46
57
|
interactive=getattr(args, "interactive", True),
|
|
47
58
|
dry_run=getattr(args, "dry_run", False),
|
|
48
59
|
)
|
|
@@ -28,7 +28,6 @@ class AsyncHookManager:
|
|
|
28
28
|
self._config_path: Path | None = None
|
|
29
29
|
|
|
30
30
|
def set_config_path(self, config_path: Path) -> None:
|
|
31
|
-
"""Set the path to the pre-commit configuration file."""
|
|
32
31
|
self._config_path = config_path
|
|
33
32
|
|
|
34
33
|
async def run_fast_hooks_async(self) -> list[HookResult]:
|
|
@@ -6,6 +6,7 @@ from rich.console import Console
|
|
|
6
6
|
|
|
7
7
|
from crackerjack.config.hooks import HookConfigLoader
|
|
8
8
|
from crackerjack.executors.hook_executor import HookExecutor
|
|
9
|
+
from crackerjack.executors.lsp_aware_hook_executor import LSPAwareHookExecutor
|
|
9
10
|
from crackerjack.models.task import HookResult
|
|
10
11
|
|
|
11
12
|
|
|
@@ -16,12 +17,25 @@ class HookManagerImpl:
|
|
|
16
17
|
pkg_path: Path,
|
|
17
18
|
verbose: bool = False,
|
|
18
19
|
quiet: bool = False,
|
|
20
|
+
enable_lsp_optimization: bool = False,
|
|
21
|
+
enable_tool_proxy: bool = True,
|
|
19
22
|
) -> None:
|
|
20
23
|
self.console = console
|
|
21
24
|
self.pkg_path = pkg_path
|
|
22
|
-
self.executor
|
|
25
|
+
self.executor: HookExecutor
|
|
26
|
+
|
|
27
|
+
# Use LSP-aware executor if optimization is enabled
|
|
28
|
+
if enable_lsp_optimization:
|
|
29
|
+
self.executor = LSPAwareHookExecutor(
|
|
30
|
+
console, pkg_path, verbose, quiet, use_tool_proxy=enable_tool_proxy
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
self.executor = HookExecutor(console, pkg_path, verbose, quiet)
|
|
34
|
+
|
|
23
35
|
self.config_loader = HookConfigLoader()
|
|
24
36
|
self._config_path: Path | None = None
|
|
37
|
+
self.lsp_optimization_enabled = enable_lsp_optimization
|
|
38
|
+
self.tool_proxy_enabled = enable_tool_proxy
|
|
25
39
|
|
|
26
40
|
def set_config_path(self, config_path: Path) -> None:
|
|
27
41
|
self._config_path = config_path
|
|
@@ -49,6 +63,70 @@ class HookManagerImpl:
|
|
|
49
63
|
comprehensive_results = self.run_comprehensive_hooks()
|
|
50
64
|
return fast_results + comprehensive_results
|
|
51
65
|
|
|
66
|
+
def get_execution_info(self) -> dict[str, t.Any]:
|
|
67
|
+
"""Get information about current execution mode and capabilities."""
|
|
68
|
+
info = {
|
|
69
|
+
"lsp_optimization_enabled": self.lsp_optimization_enabled,
|
|
70
|
+
"tool_proxy_enabled": self.tool_proxy_enabled,
|
|
71
|
+
"executor_type": type(self.executor).__name__,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Get LSP-specific info if available
|
|
75
|
+
if hasattr(self.executor, "get_execution_mode_summary"):
|
|
76
|
+
info.update(self.executor.get_execution_mode_summary())
|
|
77
|
+
|
|
78
|
+
return info
|
|
79
|
+
|
|
80
|
+
def configure_lsp_optimization(self, enable: bool) -> None:
|
|
81
|
+
"""Enable or disable LSP optimization by switching executors."""
|
|
82
|
+
if enable == self.lsp_optimization_enabled:
|
|
83
|
+
return # Already in the correct state
|
|
84
|
+
|
|
85
|
+
# Switch executor based on the enable flag
|
|
86
|
+
if enable:
|
|
87
|
+
self.executor = LSPAwareHookExecutor(
|
|
88
|
+
self.console,
|
|
89
|
+
self.pkg_path,
|
|
90
|
+
verbose=getattr(self.executor, "verbose", False),
|
|
91
|
+
quiet=getattr(self.executor, "quiet", True),
|
|
92
|
+
use_tool_proxy=self.tool_proxy_enabled,
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
self.executor = HookExecutor(
|
|
96
|
+
self.console,
|
|
97
|
+
self.pkg_path,
|
|
98
|
+
verbose=getattr(self.executor, "verbose", False),
|
|
99
|
+
quiet=getattr(self.executor, "quiet", True),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
self.lsp_optimization_enabled = enable
|
|
103
|
+
|
|
104
|
+
# Restore config path if it was set[t.Any]
|
|
105
|
+
if self._config_path:
|
|
106
|
+
# Config path is set[t.Any] at the manager level, not executor level
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def configure_tool_proxy(self, enable: bool) -> None:
|
|
110
|
+
"""Enable or disable tool proxy resilience."""
|
|
111
|
+
if enable == self.tool_proxy_enabled:
|
|
112
|
+
return # Already in the correct state
|
|
113
|
+
|
|
114
|
+
self.tool_proxy_enabled = enable
|
|
115
|
+
|
|
116
|
+
# If using LSP-aware executor, recreate it with new tool proxy setting
|
|
117
|
+
if isinstance(self.executor, LSPAwareHookExecutor):
|
|
118
|
+
self.executor = LSPAwareHookExecutor(
|
|
119
|
+
self.console,
|
|
120
|
+
self.pkg_path,
|
|
121
|
+
verbose=getattr(self.executor, "verbose", False),
|
|
122
|
+
quiet=getattr(self.executor, "quiet", True),
|
|
123
|
+
use_tool_proxy=enable,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Restore config path if it was set[t.Any]
|
|
127
|
+
if self._config_path:
|
|
128
|
+
pass # Config path handled at manager level
|
|
129
|
+
|
|
52
130
|
def validate_hooks_config(self) -> bool:
|
|
53
131
|
try:
|
|
54
132
|
result = subprocess.run(
|
|
@@ -67,7 +67,8 @@ class PublishManagerImpl:
|
|
|
67
67
|
|
|
68
68
|
content = self.filesystem.read_file(pyproject_path)
|
|
69
69
|
data = loads(content)
|
|
70
|
-
|
|
70
|
+
version = data.get("project", {}).get("version")
|
|
71
|
+
return version if isinstance(version, str) else None
|
|
71
72
|
except Exception as e:
|
|
72
73
|
self.console.print(f"[yellow]⚠️[/ yellow] Error reading version: {e}")
|
|
73
74
|
return None
|
|
@@ -134,6 +135,8 @@ class PublishManagerImpl:
|
|
|
134
135
|
self.console.print(
|
|
135
136
|
f"[green]🚀[/ green] Bumped {version_type} version: {current_version} → {new_version}",
|
|
136
137
|
)
|
|
138
|
+
# Update changelog after successful version bump
|
|
139
|
+
self._update_changelog_for_version(current_version, new_version)
|
|
137
140
|
else:
|
|
138
141
|
msg = "Failed to update version in file"
|
|
139
142
|
raise ValueError(msg)
|
|
@@ -197,7 +200,7 @@ class PublishManagerImpl:
|
|
|
197
200
|
[
|
|
198
201
|
"keyring",
|
|
199
202
|
"get",
|
|
200
|
-
"https
|
|
203
|
+
"https: //upload.pypi.org/legacy/",
|
|
201
204
|
"__token__",
|
|
202
205
|
],
|
|
203
206
|
)
|
|
@@ -228,7 +231,7 @@ class PublishManagerImpl:
|
|
|
228
231
|
" 1. Set environment variable: export UV_PUBLISH_TOKEN=<your-pypi-token>",
|
|
229
232
|
)
|
|
230
233
|
self.console.print(
|
|
231
|
-
" 2. Use keyring: keyring set https
|
|
234
|
+
" 2. Use keyring: keyring set[t.Any] https: //upload.pypi.org/legacy/ __token__",
|
|
232
235
|
)
|
|
233
236
|
self.console.print(
|
|
234
237
|
" 3. Ensure token starts with 'pypi-' and is properly formatted",
|
|
@@ -286,7 +289,7 @@ class PublishManagerImpl:
|
|
|
286
289
|
if not dist_dir.exists():
|
|
287
290
|
return
|
|
288
291
|
|
|
289
|
-
artifacts = list(dist_dir.glob("*"))
|
|
292
|
+
artifacts = list[t.Any](dist_dir.glob("*"))
|
|
290
293
|
self.console.print(f"[cyan]📦[/ cyan] Build artifacts ({len(artifacts)}): ")
|
|
291
294
|
|
|
292
295
|
for artifact in artifacts[-5:]:
|
|
@@ -295,8 +298,8 @@ class PublishManagerImpl:
|
|
|
295
298
|
|
|
296
299
|
def _format_file_size(self, size: int) -> str:
|
|
297
300
|
if size < 1024 * 1024:
|
|
298
|
-
return f"{size / 1024
|
|
299
|
-
return f"{size / (1024 * 1024)
|
|
301
|
+
return f"{size / 1024: .1f}KB"
|
|
302
|
+
return f"{size / (1024 * 1024): .1f}MB"
|
|
300
303
|
|
|
301
304
|
def publish_package(self) -> bool:
|
|
302
305
|
if not self._validate_prerequisites():
|
|
@@ -347,7 +350,7 @@ class PublishManagerImpl:
|
|
|
347
350
|
package_name = self._get_package_name()
|
|
348
351
|
|
|
349
352
|
if package_name and current_version:
|
|
350
|
-
url = f"https
|
|
353
|
+
url = f"https: //pypi.org/project/{package_name}/{current_version}/"
|
|
351
354
|
self.console.print(f"[cyan]🔗[/ cyan] Package URL: {url}")
|
|
352
355
|
|
|
353
356
|
def _get_package_name(self) -> str | None:
|
|
@@ -358,7 +361,8 @@ class PublishManagerImpl:
|
|
|
358
361
|
|
|
359
362
|
content = self.filesystem.read_file(pyproject_path)
|
|
360
363
|
data = loads(content)
|
|
361
|
-
|
|
364
|
+
name = data.get("project", {}).get("name", "")
|
|
365
|
+
return name if isinstance(name, str) else None
|
|
362
366
|
|
|
363
367
|
return None
|
|
364
368
|
|
|
@@ -446,3 +450,35 @@ class PublishManagerImpl:
|
|
|
446
450
|
except Exception as e:
|
|
447
451
|
self.console.print(f"[yellow]⚠️[/ yellow] Error reading package info: {e}")
|
|
448
452
|
return {}
|
|
453
|
+
|
|
454
|
+
def _update_changelog_for_version(self, old_version: str, new_version: str) -> None:
|
|
455
|
+
"""Update changelog with entries from git commits since last version."""
|
|
456
|
+
try:
|
|
457
|
+
from crackerjack.services.changelog_automation import ChangelogGenerator
|
|
458
|
+
from crackerjack.services.git import GitService
|
|
459
|
+
|
|
460
|
+
# Initialize services
|
|
461
|
+
git_service = GitService(self.console, self.pkg_path)
|
|
462
|
+
changelog_generator = ChangelogGenerator(self.console, git_service)
|
|
463
|
+
|
|
464
|
+
# Look for changelog file
|
|
465
|
+
changelog_path = self.pkg_path / "CHANGELOG.md"
|
|
466
|
+
|
|
467
|
+
# Generate changelog entries since last version
|
|
468
|
+
success = changelog_generator.generate_changelog_from_commits(
|
|
469
|
+
changelog_path=changelog_path,
|
|
470
|
+
version=new_version,
|
|
471
|
+
since_version=f"v{old_version}", # Assumes git tags are prefixed with 'v'
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
if success:
|
|
475
|
+
self.console.print(
|
|
476
|
+
f"[green]📝[/green] Updated changelog for version {new_version}"
|
|
477
|
+
)
|
|
478
|
+
else:
|
|
479
|
+
self.console.print(
|
|
480
|
+
"[yellow]⚠️[/yellow] Changelog update encountered issues"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
except Exception as e:
|
|
484
|
+
self.console.print(f"[yellow]⚠️[/yellow] Failed to update changelog: {e}")
|
|
@@ -23,22 +23,8 @@ class TestCommandBuilder:
|
|
|
23
23
|
if hasattr(options, "test_workers") and options.test_workers:
|
|
24
24
|
return options.test_workers
|
|
25
25
|
|
|
26
|
-
# Temporarily disable multi-worker execution due to pytest-xdist
|
|
27
|
-
# hanging issues with async tests. See GitHub issue for details.
|
|
28
|
-
# TODO: Re-enable after fixing async test timeout issues
|
|
29
26
|
return 1
|
|
30
27
|
|
|
31
|
-
# Original multi-worker logic (commented out):
|
|
32
|
-
# import multiprocessing
|
|
33
|
-
# cpu_count = multiprocessing.cpu_count()
|
|
34
|
-
# if cpu_count <= 2:
|
|
35
|
-
# return 1
|
|
36
|
-
# elif cpu_count <= 4:
|
|
37
|
-
# return 2
|
|
38
|
-
# elif cpu_count <= 8:
|
|
39
|
-
# return 3
|
|
40
|
-
# return 4
|
|
41
|
-
|
|
42
28
|
def get_test_timeout(self, options: OptionsProtocol) -> int:
|
|
43
29
|
if hasattr(options, "test_timeout") and options.test_timeout:
|
|
44
30
|
return options.test_timeout
|
|
@@ -68,7 +54,7 @@ class TestCommandBuilder:
|
|
|
68
54
|
[
|
|
69
55
|
"--benchmark-only",
|
|
70
56
|
"--benchmark-sort=mean",
|
|
71
|
-
"--benchmark-columns=min,max,mean,stddev",
|
|
57
|
+
"--benchmark-columns=min, max, mean, stddev",
|
|
72
58
|
]
|
|
73
59
|
)
|
|
74
60
|
|
|
@@ -189,9 +189,7 @@ class TestExecutor:
|
|
|
189
189
|
|
|
190
190
|
def _handle_collection_completion(self, line: str, progress: TestProgress) -> bool:
|
|
191
191
|
if "collected" in line and ("item" in line or "test" in line):
|
|
192
|
-
match = re.search(
|
|
193
|
-
r"(\d +) (?: item | test)", line
|
|
194
|
-
) # REGEX OK: parsing pytest output format
|
|
192
|
+
match = re.search(r"(\d +) (?: item | test)", line)
|
|
195
193
|
if match:
|
|
196
194
|
progress.update(
|
|
197
195
|
total_tests=int(match.group(1)),
|
|
@@ -25,16 +25,24 @@ class TestManager:
|
|
|
25
25
|
self.command_builder = TestCommandBuilder(pkg_path)
|
|
26
26
|
|
|
27
27
|
if coverage_ratchet is None:
|
|
28
|
-
# Import here to avoid circular imports
|
|
29
28
|
from crackerjack.services.coverage_ratchet import CoverageRatchetService
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
coverage_ratchet_obj = CoverageRatchetService(pkg_path, console)
|
|
31
|
+
self.coverage_ratchet: CoverageRatchetProtocol | None = t.cast(
|
|
32
|
+
CoverageRatchetProtocol, coverage_ratchet_obj
|
|
33
|
+
)
|
|
34
|
+
else:
|
|
35
|
+
self.coverage_ratchet = coverage_ratchet
|
|
34
36
|
|
|
35
37
|
self._last_test_failures: list[str] = []
|
|
36
38
|
self._progress_callback: t.Callable[[dict[str, t.Any]], None] | None = None
|
|
37
39
|
self.coverage_ratchet_enabled = True
|
|
40
|
+
self.use_lsp_diagnostics = True
|
|
41
|
+
|
|
42
|
+
# Initialize coverage badge service
|
|
43
|
+
from crackerjack.services.coverage_badge_service import CoverageBadgeService
|
|
44
|
+
|
|
45
|
+
self._coverage_badge_service = CoverageBadgeService(console, pkg_path)
|
|
38
46
|
|
|
39
47
|
def set_progress_callback(
|
|
40
48
|
self,
|
|
@@ -156,11 +164,11 @@ class TestManager:
|
|
|
156
164
|
test_path = self.pkg_path / test_dir
|
|
157
165
|
if test_path.exists() and test_path.is_dir():
|
|
158
166
|
for test_file_pattern in test_files:
|
|
159
|
-
if list(test_path.glob(f"**/{test_file_pattern}")):
|
|
167
|
+
if list[t.Any](test_path.glob(f"**/{test_file_pattern}")):
|
|
160
168
|
return True
|
|
161
169
|
|
|
162
170
|
for test_file_pattern in test_files:
|
|
163
|
-
if list(self.pkg_path.glob(test_file_pattern)):
|
|
171
|
+
if list[t.Any](self.pkg_path.glob(test_file_pattern)):
|
|
164
172
|
return True
|
|
165
173
|
|
|
166
174
|
return False
|
|
@@ -212,8 +220,44 @@ class TestManager:
|
|
|
212
220
|
return True
|
|
213
221
|
|
|
214
222
|
ratchet_result = self.coverage_ratchet.check_and_update_coverage()
|
|
223
|
+
|
|
224
|
+
# Update coverage badge if coverage information is available
|
|
225
|
+
self._update_coverage_badge(ratchet_result)
|
|
226
|
+
|
|
215
227
|
return self._handle_ratchet_result(ratchet_result)
|
|
216
228
|
|
|
229
|
+
def _update_coverage_badge(self, ratchet_result: dict[str, t.Any]) -> None:
|
|
230
|
+
"""Update coverage badge in README.md if coverage changed."""
|
|
231
|
+
try:
|
|
232
|
+
# Get current coverage directly from coverage.json to ensure freshest data
|
|
233
|
+
import json
|
|
234
|
+
|
|
235
|
+
current_coverage = None
|
|
236
|
+
coverage_json_path = self.pkg_path / "coverage.json"
|
|
237
|
+
|
|
238
|
+
if coverage_json_path.exists():
|
|
239
|
+
with coverage_json_path.open() as f:
|
|
240
|
+
data = json.load(f)
|
|
241
|
+
current_coverage = data.get("totals", {}).get("percent_covered")
|
|
242
|
+
|
|
243
|
+
# Fallback to ratchet result if coverage.json not available
|
|
244
|
+
if current_coverage is None:
|
|
245
|
+
current_coverage = ratchet_result.get("current_coverage")
|
|
246
|
+
|
|
247
|
+
# Final fallback to coverage service
|
|
248
|
+
if current_coverage is None:
|
|
249
|
+
coverage_info = self.get_coverage()
|
|
250
|
+
current_coverage = coverage_info.get("coverage_percent")
|
|
251
|
+
|
|
252
|
+
if current_coverage is not None:
|
|
253
|
+
if self._coverage_badge_service.should_update_badge(current_coverage):
|
|
254
|
+
self._coverage_badge_service.update_readme_coverage_badge(
|
|
255
|
+
current_coverage
|
|
256
|
+
)
|
|
257
|
+
except Exception as e:
|
|
258
|
+
# Don't fail the test process if badge update fails
|
|
259
|
+
self.console.print(f"[yellow]⚠️[/yellow] Badge update failed: {e}")
|
|
260
|
+
|
|
217
261
|
def _handle_ratchet_result(self, ratchet_result: dict[str, t.Any]) -> bool:
|
|
218
262
|
if ratchet_result.get("success", False):
|
|
219
263
|
if ratchet_result.get("improved", False):
|
|
@@ -227,7 +271,7 @@ class TestManager:
|
|
|
227
271
|
previous = ratchet_result.get("previous_coverage", 0)
|
|
228
272
|
self.console.print(
|
|
229
273
|
f"[red]📉[/ red] Coverage regression: "
|
|
230
|
-
f"{current
|
|
274
|
+
f"{current: .2f}% < {previous: .2f}%"
|
|
231
275
|
)
|
|
232
276
|
return False
|
|
233
277
|
|
|
@@ -255,5 +299,52 @@ class TestManager:
|
|
|
255
299
|
def _get_timeout(self, options: OptionsProtocol) -> int:
|
|
256
300
|
return self.command_builder.get_test_timeout(options)
|
|
257
301
|
|
|
302
|
+
async def run_pre_test_lsp_diagnostics(self) -> bool:
|
|
303
|
+
"""Run LSP diagnostics before tests to catch type errors early."""
|
|
304
|
+
if not self.use_lsp_diagnostics:
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
from crackerjack.services.lsp_client import LSPClient
|
|
309
|
+
|
|
310
|
+
lsp_client = LSPClient(self.console)
|
|
311
|
+
|
|
312
|
+
# Check if LSP server is available
|
|
313
|
+
if not lsp_client.is_server_running():
|
|
314
|
+
return True # No LSP server, skip diagnostics
|
|
315
|
+
|
|
316
|
+
# Run type diagnostics on the project
|
|
317
|
+
diagnostics, summary = lsp_client.check_project_with_feedback(
|
|
318
|
+
self.pkg_path,
|
|
319
|
+
show_progress=False, # Keep quiet for test integration
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Check if there are type errors
|
|
323
|
+
has_errors = any(diags for diags in diagnostics.values())
|
|
324
|
+
|
|
325
|
+
if has_errors:
|
|
326
|
+
self.console.print(
|
|
327
|
+
"[yellow]⚠️ LSP detected type errors before running tests[/yellow]"
|
|
328
|
+
)
|
|
329
|
+
# Format and show a summary
|
|
330
|
+
error_count = sum(len(diags) for diags in diagnostics.values())
|
|
331
|
+
self.console.print(f"[yellow]Found {error_count} type issues[/yellow]")
|
|
332
|
+
|
|
333
|
+
return not has_errors # Return False if there are type errors
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
# If LSP diagnostics fail, don't block tests
|
|
337
|
+
self.console.print(f"[dim]LSP diagnostics failed: {e}[/dim]")
|
|
338
|
+
return True
|
|
339
|
+
|
|
340
|
+
def configure_lsp_diagnostics(self, enable: bool) -> None:
|
|
341
|
+
"""Enable or disable LSP diagnostics integration."""
|
|
342
|
+
self.use_lsp_diagnostics = enable
|
|
343
|
+
|
|
344
|
+
if enable:
|
|
345
|
+
self.console.print(
|
|
346
|
+
"[cyan]🔍 LSP diagnostics enabled for faster test feedback[/cyan]"
|
|
347
|
+
)
|
|
348
|
+
|
|
258
349
|
|
|
259
350
|
TestManagementImpl = TestManager
|
|
@@ -172,7 +172,7 @@ class TestManagementImpl:
|
|
|
172
172
|
self.console.print("[yellow]⚠️[/ yellow] Coverage ratchet disabled")
|
|
173
173
|
|
|
174
174
|
def get_coverage_ratchet_status(self) -> dict[str, t.Any]:
|
|
175
|
-
return self.coverage_ratchet.
|
|
175
|
+
return self.coverage_ratchet.get_ratchet_data()
|
|
176
176
|
|
|
177
177
|
def _run_test_command(
|
|
178
178
|
self,
|
|
@@ -482,9 +482,8 @@ class TestManagementImpl:
|
|
|
482
482
|
self._handle_running_test(line, progress)
|
|
483
483
|
|
|
484
484
|
def _handle_collection_completion(self, line: str, progress: TestProgress) -> bool:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
): # REGEX OK: parsing pytest collection output
|
|
485
|
+
match = re.search(r"collected (\d+) items?", line)
|
|
486
|
+
if match:
|
|
488
487
|
progress.update(
|
|
489
488
|
total_tests=int(match.group(1)),
|
|
490
489
|
is_collecting=False,
|
|
@@ -728,7 +727,7 @@ class TestManagementImpl:
|
|
|
728
727
|
import os
|
|
729
728
|
|
|
730
729
|
cpu_count = os.cpu_count() or 1
|
|
731
|
-
test_files = list(self.pkg_path.glob("tests/test_*.py"))
|
|
730
|
+
test_files = list[t.Any](self.pkg_path.glob("tests/test_*.py"))
|
|
732
731
|
if len(test_files) < 5:
|
|
733
732
|
return min(2, cpu_count)
|
|
734
733
|
|
|
@@ -737,7 +736,7 @@ class TestManagementImpl:
|
|
|
737
736
|
def _get_test_timeout(self, options: OptionsProtocol) -> int:
|
|
738
737
|
if options.test_timeout > 0:
|
|
739
738
|
return options.test_timeout
|
|
740
|
-
test_files = list(self.pkg_path.glob("tests/test_*.py"))
|
|
739
|
+
test_files = list[t.Any](self.pkg_path.glob("tests/test_*.py"))
|
|
741
740
|
base_timeout = 300
|
|
742
741
|
|
|
743
742
|
import math
|
|
@@ -1020,7 +1019,9 @@ class TestManagementImpl:
|
|
|
1020
1019
|
test_dir = self.pkg_path / "tests"
|
|
1021
1020
|
if not test_dir.exists():
|
|
1022
1021
|
issues.append("tests directory not found")
|
|
1023
|
-
test_files =
|
|
1022
|
+
test_files = (
|
|
1023
|
+
list[t.Any](test_dir.glob("test_ *.py")) if test_dir.exists() else []
|
|
1024
|
+
)
|
|
1024
1025
|
if not test_files:
|
|
1025
1026
|
issues.append("no test files found")
|
|
1026
1027
|
if issues:
|
|
@@ -1035,7 +1036,7 @@ class TestManagementImpl:
|
|
|
1035
1036
|
test_dir = self.pkg_path / "tests"
|
|
1036
1037
|
if not test_dir.exists():
|
|
1037
1038
|
return {"test_files": 0, "total_tests": 0, "test_lines": 0}
|
|
1038
|
-
test_files = list(test_dir.glob("test_ *.py"))
|
|
1039
|
+
test_files = list[t.Any](test_dir.glob("test_ *.py"))
|
|
1039
1040
|
total_lines = 0
|
|
1040
1041
|
total_tests = 0
|
|
1041
1042
|
for test_file in test_files:
|
|
@@ -1070,5 +1071,5 @@ class TestManagementImpl:
|
|
|
1070
1071
|
return None
|
|
1071
1072
|
|
|
1072
1073
|
def has_tests(self) -> bool:
|
|
1073
|
-
test_files = list(self.pkg_path.glob("tests/test_*.py"))
|
|
1074
|
+
test_files = list[t.Any](self.pkg_path.glob("tests/test_*.py"))
|
|
1074
1075
|
return len(test_files) > 0
|
crackerjack/mcp/cache.py
CHANGED
|
@@ -97,7 +97,7 @@ class ErrorCache:
|
|
|
97
97
|
]
|
|
98
98
|
|
|
99
99
|
def get_common_patterns(self, limit: int = 20) -> list[ErrorPattern]:
|
|
100
|
-
patterns = list(self.patterns.values())
|
|
100
|
+
patterns = list[t.Any](self.patterns.values())
|
|
101
101
|
patterns.sort(key=lambda p: p.frequency, reverse=True)
|
|
102
102
|
return patterns[:limit]
|
|
103
103
|
|
|
@@ -252,7 +252,7 @@ class ErrorCache:
|
|
|
252
252
|
successful_fixes = sum(1 for result in self.fix_results if result.success)
|
|
253
253
|
frequencies = [pattern.frequency for pattern in self.patterns.values()]
|
|
254
254
|
avg_frequency = sum(frequencies) / len(frequencies) if frequencies else 0
|
|
255
|
-
type_counts = {}
|
|
255
|
+
type_counts: dict[str, int] = {}
|
|
256
256
|
for pattern in self.patterns.values():
|
|
257
257
|
type_counts[pattern.error_type] = type_counts.get(pattern.error_type, 0) + 1
|
|
258
258
|
|
crackerjack/mcp/client_runner.py
CHANGED
|
@@ -22,7 +22,7 @@ def is_mcp_server_running(host: str = "localhost", port: int = 5173) -> bool:
|
|
|
22
22
|
sock.close()
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
async def ensure_mcp_server_running() -> subprocess.Popen | None:
|
|
25
|
+
async def ensure_mcp_server_running() -> subprocess.Popen[bytes] | None:
|
|
26
26
|
console = Console()
|
|
27
27
|
|
|
28
28
|
if is_mcp_server_running():
|