claude-mpm 4.13.1__py3-none-any.whl → 4.14.0__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 claude-mpm might be problematic. Click here for more details.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +68 -0
- claude_mpm/cli/__init__.py +10 -0
- claude_mpm/cli/commands/local_deploy.py +536 -0
- claude_mpm/cli/parsers/base_parser.py +7 -0
- claude_mpm/cli/parsers/local_deploy_parser.py +227 -0
- claude_mpm/commands/mpm-agents-detect.md +168 -0
- claude_mpm/commands/mpm-agents-recommend.md +214 -0
- claude_mpm/commands/mpm-agents.md +75 -1
- claude_mpm/commands/mpm-auto-configure.md +217 -0
- claude_mpm/commands/mpm-help.md +160 -0
- claude_mpm/config/model_config.py +428 -0
- claude_mpm/core/interactive_session.py +3 -0
- claude_mpm/services/core/interfaces/__init__.py +74 -2
- claude_mpm/services/core/interfaces/health.py +172 -0
- claude_mpm/services/core/interfaces/model.py +281 -0
- claude_mpm/services/core/interfaces/process.py +372 -0
- claude_mpm/services/core/interfaces/restart.py +307 -0
- claude_mpm/services/core/interfaces/stability.py +260 -0
- claude_mpm/services/core/models/__init__.py +35 -0
- claude_mpm/services/core/models/health.py +189 -0
- claude_mpm/services/core/models/process.py +258 -0
- claude_mpm/services/core/models/restart.py +302 -0
- claude_mpm/services/core/models/stability.py +264 -0
- claude_mpm/services/local_ops/__init__.py +163 -0
- claude_mpm/services/local_ops/crash_detector.py +257 -0
- claude_mpm/services/local_ops/health_checks/__init__.py +28 -0
- claude_mpm/services/local_ops/health_checks/http_check.py +223 -0
- claude_mpm/services/local_ops/health_checks/process_check.py +235 -0
- claude_mpm/services/local_ops/health_checks/resource_check.py +254 -0
- claude_mpm/services/local_ops/health_manager.py +430 -0
- claude_mpm/services/local_ops/log_monitor.py +396 -0
- claude_mpm/services/local_ops/memory_leak_detector.py +294 -0
- claude_mpm/services/local_ops/process_manager.py +595 -0
- claude_mpm/services/local_ops/resource_monitor.py +331 -0
- claude_mpm/services/local_ops/restart_manager.py +401 -0
- claude_mpm/services/local_ops/restart_policy.py +387 -0
- claude_mpm/services/local_ops/state_manager.py +371 -0
- claude_mpm/services/local_ops/unified_manager.py +600 -0
- claude_mpm/services/model/__init__.py +147 -0
- claude_mpm/services/model/base_provider.py +365 -0
- claude_mpm/services/model/claude_provider.py +412 -0
- claude_mpm/services/model/model_router.py +453 -0
- claude_mpm/services/model/ollama_provider.py +415 -0
- {claude_mpm-4.13.1.dist-info → claude_mpm-4.14.0.dist-info}/METADATA +1 -1
- {claude_mpm-4.13.1.dist-info → claude_mpm-4.14.0.dist-info}/RECORD +50 -15
- {claude_mpm-4.13.1.dist-info → claude_mpm-4.14.0.dist-info}/WHEEL +0 -0
- {claude_mpm-4.13.1.dist-info → claude_mpm-4.14.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.13.1.dist-info → claude_mpm-4.14.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.13.1.dist-info → claude_mpm-4.14.0.dist-info}/top_level.txt +0 -0
claude_mpm/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.14.0
|
|
@@ -113,6 +113,73 @@ Read: /mpm-doctor # WRONG - not a file to read
|
|
|
113
113
|
- MPM commands are system operations, NOT files or scripts
|
|
114
114
|
- Always use SlashCommand tool for these operations
|
|
115
115
|
|
|
116
|
+
## 🤖 AUTO-CONFIGURATION FEATURE (NEW!)
|
|
117
|
+
|
|
118
|
+
**IMPORTANT**: Claude MPM now includes intelligent auto-configuration that can detect project stacks and recommend the right agents automatically.
|
|
119
|
+
|
|
120
|
+
### When to Suggest Auto-Configuration
|
|
121
|
+
|
|
122
|
+
PM SHOULD proactively suggest auto-configuration when:
|
|
123
|
+
1. **New user/session**: First interaction in a project without deployed agents
|
|
124
|
+
2. **Few agents deployed**: < 3 agents deployed but project seems to need more
|
|
125
|
+
3. **User asks about agents**: "What agents should I use?" or "Which agents do I need?"
|
|
126
|
+
4. **Stack changes detected**: User mentions adding new frameworks or tools
|
|
127
|
+
5. **User struggles**: User manually deploying multiple agents one-by-one
|
|
128
|
+
|
|
129
|
+
### Auto-Configuration Commands
|
|
130
|
+
|
|
131
|
+
**Three new MPM commands available**:
|
|
132
|
+
- `/mpm-auto-configure [--preview|--yes]` - Full auto-configuration workflow
|
|
133
|
+
- `/mpm-agents-detect` - Just show detected toolchain
|
|
134
|
+
- `/mpm-agents-recommend` - Show agent recommendations without deploying
|
|
135
|
+
|
|
136
|
+
### Suggestion Patterns
|
|
137
|
+
|
|
138
|
+
**Example 1: First-time user**
|
|
139
|
+
```
|
|
140
|
+
User: "I need help with my FastAPI project"
|
|
141
|
+
PM: "I notice this is a FastAPI project. Would you like me to run auto-configuration
|
|
142
|
+
to set up the right agents automatically? Run '/mpm-auto-configure --preview'
|
|
143
|
+
to see what would be configured."
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Example 2: User manually deploying agents**
|
|
147
|
+
```
|
|
148
|
+
User: "Deploy fastapi-engineer"
|
|
149
|
+
PM: "Deploying fastapi-engineer... By the way, you can use '/mpm-auto-configure'
|
|
150
|
+
to automatically detect your stack and deploy all recommended agents at once.
|
|
151
|
+
Would you like to try that instead?"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Example 3: User asks about agents**
|
|
155
|
+
```
|
|
156
|
+
User: "What agents should I use for Next.js?"
|
|
157
|
+
PM: "Let me run auto-detection to give you personalized recommendations.
|
|
158
|
+
I'll use '/mpm-agents-detect' to scan your project, then
|
|
159
|
+
'/mpm-agents-recommend' to show exactly which agents fit your stack."
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Proactive Suggestion Template
|
|
163
|
+
|
|
164
|
+
When appropriate, include a helpful suggestion like:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
💡 Tip: Try the new auto-configuration feature!
|
|
168
|
+
Run '/mpm-auto-configure --preview' to see which agents
|
|
169
|
+
are recommended for your project based on detected toolchain.
|
|
170
|
+
|
|
171
|
+
Supported: Python, Node.js, Rust, Go, and popular frameworks
|
|
172
|
+
like FastAPI, Next.js, React, Express, and more.
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Important Notes
|
|
176
|
+
|
|
177
|
+
- **Don't over-suggest**: Only mention once per session
|
|
178
|
+
- **User choice**: Always respect if user prefers manual configuration
|
|
179
|
+
- **Preview first**: Recommend --preview flag for first-time users
|
|
180
|
+
- **Not mandatory**: Auto-config is a convenience, not a requirement
|
|
181
|
+
- **Fallback available**: Manual agent deployment always works
|
|
182
|
+
|
|
116
183
|
## NO ASSERTION WITHOUT VERIFICATION RULE
|
|
117
184
|
|
|
118
185
|
**CRITICAL**: PM MUST NEVER make claims without evidence from agents.
|
|
@@ -194,6 +261,7 @@ See [Validation Templates](templates/validation_templates.md#required-evidence-f
|
|
|
194
261
|
| "error", "bug", "issue" | "I'll have QA reproduce this" | QA |
|
|
195
262
|
| "slow", "performance" | "I'll have QA benchmark this" | QA |
|
|
196
263
|
| "/mpm-doctor", "/mpm-status", etc | "I'll run the MPM command" | Use SlashCommand tool (NOT bash) |
|
|
264
|
+
| "/mpm-auto-configure", "/mpm-agents-detect" | "I'll run the auto-config command" | Use SlashCommand tool (NEW!) |
|
|
197
265
|
| ANY question about code | "I'll have Research examine this" | Research |
|
|
198
266
|
|
|
199
267
|
### 🔴 CIRCUIT BREAKER - IMPLEMENTATION DETECTION 🔴
|
claude_mpm/cli/__init__.py
CHANGED
|
@@ -730,6 +730,16 @@ def _execute_command(command: str, args) -> int:
|
|
|
730
730
|
# Convert CommandResult to exit code
|
|
731
731
|
return result.exit_code if result else 0
|
|
732
732
|
|
|
733
|
+
# Handle local-deploy command with lazy import
|
|
734
|
+
if command == "local-deploy":
|
|
735
|
+
# Lazy import to avoid loading unless needed
|
|
736
|
+
from .commands.local_deploy import LocalDeployCommand
|
|
737
|
+
|
|
738
|
+
cmd = LocalDeployCommand()
|
|
739
|
+
result = cmd.run(args)
|
|
740
|
+
# Convert CommandResult to exit code
|
|
741
|
+
return result.exit_code if result else 0
|
|
742
|
+
|
|
733
743
|
# Map stable commands to their implementations
|
|
734
744
|
command_map = {
|
|
735
745
|
CLICommands.RUN.value: run_session,
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local Deploy command implementation for claude-mpm.
|
|
3
|
+
|
|
4
|
+
WHY: This module provides CLI commands for managing local development deployments
|
|
5
|
+
using the UnifiedLocalOpsManager. Supports starting, stopping, monitoring, and
|
|
6
|
+
managing local processes with full health monitoring and auto-restart capabilities.
|
|
7
|
+
|
|
8
|
+
DESIGN DECISIONS:
|
|
9
|
+
- Use UnifiedLocalOpsManager as single entry point
|
|
10
|
+
- Rich terminal output for better user experience
|
|
11
|
+
- Subcommands: start, stop, restart, status, health, list, monitor, history
|
|
12
|
+
- Support both interactive and script-friendly output modes
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.live import Live
|
|
22
|
+
from rich.panel import Panel
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
from rich.text import Text
|
|
25
|
+
|
|
26
|
+
from ...services.local_ops import (
|
|
27
|
+
ProcessStatus,
|
|
28
|
+
StartConfig,
|
|
29
|
+
UnifiedLocalOpsManager,
|
|
30
|
+
)
|
|
31
|
+
from ..shared import BaseCommand, CommandResult
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LocalDeployCommand(BaseCommand):
|
|
35
|
+
"""Local Deploy command for managing local development deployments."""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
super().__init__("local-deploy")
|
|
39
|
+
self.console = Console()
|
|
40
|
+
self.manager: Optional[UnifiedLocalOpsManager] = None
|
|
41
|
+
|
|
42
|
+
def validate_args(self, args) -> Optional[str]:
|
|
43
|
+
"""Validate command arguments."""
|
|
44
|
+
if not hasattr(args, "local_deploy_command") or not args.local_deploy_command:
|
|
45
|
+
return "No subcommand specified. Use: start, stop, restart, status, list, monitor, history"
|
|
46
|
+
|
|
47
|
+
valid_commands = [
|
|
48
|
+
"start",
|
|
49
|
+
"stop",
|
|
50
|
+
"restart",
|
|
51
|
+
"status",
|
|
52
|
+
"health",
|
|
53
|
+
"list",
|
|
54
|
+
"monitor",
|
|
55
|
+
"history",
|
|
56
|
+
"enable-auto-restart",
|
|
57
|
+
"disable-auto-restart",
|
|
58
|
+
]
|
|
59
|
+
if args.local_deploy_command not in valid_commands:
|
|
60
|
+
return f"Unknown subcommand: {args.local_deploy_command}. Valid commands: {', '.join(valid_commands)}"
|
|
61
|
+
|
|
62
|
+
# Validate command-specific arguments
|
|
63
|
+
if args.local_deploy_command == "start":
|
|
64
|
+
if not hasattr(args, "command") or not args.command:
|
|
65
|
+
return "Missing required argument: --command"
|
|
66
|
+
|
|
67
|
+
elif args.local_deploy_command in [
|
|
68
|
+
"stop",
|
|
69
|
+
"restart",
|
|
70
|
+
"status",
|
|
71
|
+
"health",
|
|
72
|
+
"history",
|
|
73
|
+
"enable-auto-restart",
|
|
74
|
+
"disable-auto-restart",
|
|
75
|
+
]:
|
|
76
|
+
if not hasattr(args, "deployment_id") or not args.deployment_id:
|
|
77
|
+
return "Missing required argument: --deployment-id"
|
|
78
|
+
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def run(self, args) -> CommandResult:
|
|
82
|
+
"""Execute the local-deploy command."""
|
|
83
|
+
try:
|
|
84
|
+
self.logger.info(f"Local deploy command: {args.local_deploy_command}")
|
|
85
|
+
|
|
86
|
+
# Initialize manager
|
|
87
|
+
project_root = getattr(args, "project_dir", None) or self.working_dir
|
|
88
|
+
self.manager = UnifiedLocalOpsManager(project_root=Path(project_root))
|
|
89
|
+
|
|
90
|
+
if not self.manager.initialize():
|
|
91
|
+
return CommandResult.error_result(
|
|
92
|
+
"Failed to initialize local ops manager"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Route to specific command
|
|
96
|
+
command = args.local_deploy_command
|
|
97
|
+
if command == "start":
|
|
98
|
+
return self._start_command(args)
|
|
99
|
+
if command == "stop":
|
|
100
|
+
return self._stop_command(args)
|
|
101
|
+
if command == "restart":
|
|
102
|
+
return self._restart_command(args)
|
|
103
|
+
if command == "status":
|
|
104
|
+
return self._status_command(args)
|
|
105
|
+
if command == "health":
|
|
106
|
+
return self._health_command(args)
|
|
107
|
+
if command == "list":
|
|
108
|
+
return self._list_command(args)
|
|
109
|
+
if command == "monitor":
|
|
110
|
+
return self._monitor_command(args)
|
|
111
|
+
if command == "history":
|
|
112
|
+
return self._history_command(args)
|
|
113
|
+
if command == "enable-auto-restart":
|
|
114
|
+
return self._enable_auto_restart_command(args)
|
|
115
|
+
if command == "disable-auto-restart":
|
|
116
|
+
return self._disable_auto_restart_command(args)
|
|
117
|
+
return CommandResult.error_result(f"Unknown command: {command}")
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
self.logger.error(
|
|
121
|
+
f"Error executing local-deploy command: {e}", exc_info=True
|
|
122
|
+
)
|
|
123
|
+
return CommandResult.error_result(f"Error: {e}")
|
|
124
|
+
finally:
|
|
125
|
+
if self.manager:
|
|
126
|
+
self.manager.shutdown()
|
|
127
|
+
|
|
128
|
+
def _start_command(self, args) -> CommandResult:
|
|
129
|
+
"""Start a new deployment."""
|
|
130
|
+
try:
|
|
131
|
+
# Parse command
|
|
132
|
+
command = (
|
|
133
|
+
args.command.split() if isinstance(args.command, str) else args.command
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Create start configuration
|
|
137
|
+
config = StartConfig(
|
|
138
|
+
command=command,
|
|
139
|
+
working_directory=str(args.working_directory or self.working_dir),
|
|
140
|
+
port=getattr(args, "port", None),
|
|
141
|
+
auto_find_port=getattr(args, "auto_find_port", True),
|
|
142
|
+
environment=getattr(args, "env", {}) or {},
|
|
143
|
+
metadata={"log_file": getattr(args, "log_file", None)},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Start deployment
|
|
147
|
+
auto_restart = getattr(args, "auto_restart", False)
|
|
148
|
+
deployment = self.manager.start_deployment(
|
|
149
|
+
config, auto_restart=auto_restart
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Output result
|
|
153
|
+
self.console.print(
|
|
154
|
+
Panel(
|
|
155
|
+
f"[green]✓[/green] Deployment started successfully\n\n"
|
|
156
|
+
f"[bold]Deployment ID:[/bold] {deployment.deployment_id}\n"
|
|
157
|
+
f"[bold]Process ID:[/bold] {deployment.process_id}\n"
|
|
158
|
+
f"[bold]Port:[/bold] {deployment.port or 'N/A'}\n"
|
|
159
|
+
f"[bold]Auto-restart:[/bold] {'Enabled' if auto_restart else 'Disabled'}\n"
|
|
160
|
+
f"[bold]Command:[/bold] {' '.join(deployment.command)}",
|
|
161
|
+
title="Deployment Started",
|
|
162
|
+
border_style="green",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return CommandResult.success_result(
|
|
167
|
+
f"Started deployment {deployment.deployment_id}",
|
|
168
|
+
data={
|
|
169
|
+
"deployment_id": deployment.deployment_id,
|
|
170
|
+
"process_id": deployment.process_id,
|
|
171
|
+
"port": deployment.port,
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
self.logger.error(f"Failed to start deployment: {e}", exc_info=True)
|
|
177
|
+
self.console.print(f"[red]✗ Failed to start deployment: {e}[/red]")
|
|
178
|
+
return CommandResult.error_result(str(e))
|
|
179
|
+
|
|
180
|
+
def _stop_command(self, args) -> CommandResult:
|
|
181
|
+
"""Stop a deployment."""
|
|
182
|
+
try:
|
|
183
|
+
deployment_id = args.deployment_id
|
|
184
|
+
force = getattr(args, "force", False)
|
|
185
|
+
timeout = getattr(args, "timeout", 10)
|
|
186
|
+
|
|
187
|
+
success = self.manager.stop_deployment(
|
|
188
|
+
deployment_id, timeout=timeout, force=force
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if success:
|
|
192
|
+
self.console.print(
|
|
193
|
+
f"[green]✓ Deployment {deployment_id} stopped successfully[/green]"
|
|
194
|
+
)
|
|
195
|
+
return CommandResult.success_result(
|
|
196
|
+
f"Stopped deployment {deployment_id}"
|
|
197
|
+
)
|
|
198
|
+
self.console.print(
|
|
199
|
+
f"[red]✗ Failed to stop deployment {deployment_id}[/red]"
|
|
200
|
+
)
|
|
201
|
+
return CommandResult.error_result("Failed to stop deployment")
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
self.logger.error(f"Failed to stop deployment: {e}", exc_info=True)
|
|
205
|
+
self.console.print(f"[red]✗ Error: {e}[/red]")
|
|
206
|
+
return CommandResult.error_result(str(e))
|
|
207
|
+
|
|
208
|
+
def _restart_command(self, args) -> CommandResult:
|
|
209
|
+
"""Restart a deployment."""
|
|
210
|
+
try:
|
|
211
|
+
deployment_id = args.deployment_id
|
|
212
|
+
timeout = getattr(args, "timeout", 10)
|
|
213
|
+
|
|
214
|
+
deployment = self.manager.restart_deployment(deployment_id, timeout=timeout)
|
|
215
|
+
|
|
216
|
+
self.console.print(
|
|
217
|
+
Panel(
|
|
218
|
+
f"[green]✓[/green] Deployment restarted successfully\n\n"
|
|
219
|
+
f"[bold]Deployment ID:[/bold] {deployment.deployment_id}\n"
|
|
220
|
+
f"[bold]New Process ID:[/bold] {deployment.process_id}\n"
|
|
221
|
+
f"[bold]Port:[/bold] {deployment.port or 'N/A'}",
|
|
222
|
+
title="Deployment Restarted",
|
|
223
|
+
border_style="green",
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return CommandResult.success_result(f"Restarted deployment {deployment_id}")
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
self.logger.error(f"Failed to restart deployment: {e}", exc_info=True)
|
|
231
|
+
self.console.print(f"[red]✗ Error: {e}[/red]")
|
|
232
|
+
return CommandResult.error_result(str(e))
|
|
233
|
+
|
|
234
|
+
def _status_command(self, args) -> CommandResult:
|
|
235
|
+
"""Show deployment status."""
|
|
236
|
+
try:
|
|
237
|
+
deployment_id = args.deployment_id
|
|
238
|
+
json_output = getattr(args, "json", False)
|
|
239
|
+
|
|
240
|
+
status = self.manager.get_full_status(deployment_id)
|
|
241
|
+
|
|
242
|
+
if json_output:
|
|
243
|
+
print(json.dumps(status, indent=2, default=str))
|
|
244
|
+
return CommandResult.success_result("Status retrieved")
|
|
245
|
+
|
|
246
|
+
# Rich formatted output
|
|
247
|
+
self._render_status_panel(status)
|
|
248
|
+
|
|
249
|
+
return CommandResult.success_result("Status retrieved", data=status)
|
|
250
|
+
|
|
251
|
+
except Exception as e:
|
|
252
|
+
self.logger.error(f"Failed to get status: {e}", exc_info=True)
|
|
253
|
+
self.console.print(f"[red]✗ Error: {e}[/red]")
|
|
254
|
+
return CommandResult.error_result(str(e))
|
|
255
|
+
|
|
256
|
+
def _health_command(self, args) -> CommandResult:
|
|
257
|
+
"""Show health status."""
|
|
258
|
+
try:
|
|
259
|
+
deployment_id = args.deployment_id
|
|
260
|
+
health = self.manager.get_health_status(deployment_id)
|
|
261
|
+
|
|
262
|
+
if not health:
|
|
263
|
+
self.console.print(
|
|
264
|
+
f"[yellow]No health data available for {deployment_id}[/yellow]"
|
|
265
|
+
)
|
|
266
|
+
return CommandResult.error_result("No health data available")
|
|
267
|
+
|
|
268
|
+
# Render health status
|
|
269
|
+
status_color = {
|
|
270
|
+
"healthy": "green",
|
|
271
|
+
"degraded": "yellow",
|
|
272
|
+
"unhealthy": "red",
|
|
273
|
+
"unknown": "dim",
|
|
274
|
+
}.get(health.overall_status.value, "dim")
|
|
275
|
+
|
|
276
|
+
self.console.print(
|
|
277
|
+
Panel(
|
|
278
|
+
f"[{status_color}]Status:[/{status_color}] {health.overall_status.value.upper()}\n\n"
|
|
279
|
+
f"[bold]HTTP Check:[/bold] {'✓' if health.http_healthy else '✗'}\n"
|
|
280
|
+
f"[bold]Process Check:[/bold] {'✓' if health.process_healthy else '✗'}\n"
|
|
281
|
+
f"[bold]Resource Check:[/bold] {'✓' if health.resource_healthy else '✗'}\n"
|
|
282
|
+
f"[bold]Last Check:[/bold] {health.last_check or 'Never'}\n"
|
|
283
|
+
f"{f'[bold]Failure Reason:[/bold] {health.failure_reason}' if health.failure_reason else ''}",
|
|
284
|
+
title=f"Health Status: {deployment_id}",
|
|
285
|
+
border_style=status_color,
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return CommandResult.success_result("Health status retrieved")
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
self.logger.error(f"Failed to get health status: {e}", exc_info=True)
|
|
293
|
+
self.console.print(f"[red]✗ Error: {e}[/red]")
|
|
294
|
+
return CommandResult.error_result(str(e))
|
|
295
|
+
|
|
296
|
+
def _list_command(self, args) -> CommandResult:
|
|
297
|
+
"""List all deployments."""
|
|
298
|
+
try:
|
|
299
|
+
status_filter_str = getattr(args, "status", None)
|
|
300
|
+
status_filter = (
|
|
301
|
+
ProcessStatus(status_filter_str) if status_filter_str else None
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
deployments = self.manager.list_deployments(status_filter=status_filter)
|
|
305
|
+
|
|
306
|
+
if not deployments:
|
|
307
|
+
self.console.print("[yellow]No deployments found[/yellow]")
|
|
308
|
+
return CommandResult.success_result("No deployments found")
|
|
309
|
+
|
|
310
|
+
# Create table
|
|
311
|
+
table = Table(title="Local Deployments", show_header=True)
|
|
312
|
+
table.add_column("Deployment ID", style="cyan")
|
|
313
|
+
table.add_column("PID", style="magenta")
|
|
314
|
+
table.add_column("Port", style="green")
|
|
315
|
+
table.add_column("Status", style="yellow")
|
|
316
|
+
table.add_column("Started At", style="dim")
|
|
317
|
+
|
|
318
|
+
for deployment in deployments:
|
|
319
|
+
table.add_row(
|
|
320
|
+
deployment.deployment_id,
|
|
321
|
+
str(deployment.process_id),
|
|
322
|
+
str(deployment.port) if deployment.port else "N/A",
|
|
323
|
+
deployment.status.value,
|
|
324
|
+
deployment.started_at.strftime("%Y-%m-%d %H:%M:%S"),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
self.console.print(table)
|
|
328
|
+
|
|
329
|
+
return CommandResult.success_result(
|
|
330
|
+
f"Found {len(deployments)} deployment(s)",
|
|
331
|
+
data={"count": len(deployments)},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
self.logger.error(f"Failed to list deployments: {e}", exc_info=True)
|
|
336
|
+
self.console.print(f"[red]✗ Error: {e}[/red]")
|
|
337
|
+
return CommandResult.error_result(str(e))
|
|
338
|
+
|
|
339
|
+
def _monitor_command(self, args) -> CommandResult:
|
|
340
|
+
"""Live monitoring dashboard."""
|
|
341
|
+
try:
|
|
342
|
+
deployment_id = args.deployment_id
|
|
343
|
+
refresh_interval = getattr(args, "refresh", 2)
|
|
344
|
+
|
|
345
|
+
self.console.print(
|
|
346
|
+
f"[cyan]Monitoring {deployment_id}... (Press Ctrl+C to stop)[/cyan]\n"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
with Live(
|
|
350
|
+
console=self.console, refresh_per_second=1 / refresh_interval
|
|
351
|
+
) as live:
|
|
352
|
+
while True:
|
|
353
|
+
try:
|
|
354
|
+
status = self.manager.get_full_status(deployment_id)
|
|
355
|
+
live.update(self._render_live_status(status))
|
|
356
|
+
time.sleep(refresh_interval)
|
|
357
|
+
except KeyboardInterrupt:
|
|
358
|
+
break
|
|
359
|
+
|
|
360
|
+
return CommandResult.success_result("Monitoring stopped")
|
|
361
|
+
|
|
362
|
+
except Exception as e:
|
|
363
|
+
self.logger.error(f"Failed to monitor deployment: {e}", exc_info=True)
|
|
364
|
+
self.console.print(f"[red]✗ Error: {e}[/red]")
|
|
365
|
+
return CommandResult.error_result(str(e))
|
|
366
|
+
|
|
367
|
+
def _history_command(self, args) -> CommandResult:
|
|
368
|
+
"""Show restart history."""
|
|
369
|
+
try:
|
|
370
|
+
deployment_id = args.deployment_id
|
|
371
|
+
history = self.manager.get_restart_history(deployment_id)
|
|
372
|
+
|
|
373
|
+
if not history:
|
|
374
|
+
self.console.print(
|
|
375
|
+
f"[yellow]No restart history for {deployment_id}[/yellow]"
|
|
376
|
+
)
|
|
377
|
+
return CommandResult.success_result("No restart history")
|
|
378
|
+
|
|
379
|
+
self.console.print(
|
|
380
|
+
Panel(
|
|
381
|
+
f"[bold]Total Restarts:[/bold] {history.total_restarts}\n"
|
|
382
|
+
f"[bold]Successful:[/bold] {history.successful_restarts}\n"
|
|
383
|
+
f"[bold]Failed:[/bold] {history.failed_restarts}\n"
|
|
384
|
+
f"[bold]Circuit Breaker:[/bold] {history.circuit_breaker_state.value}\n"
|
|
385
|
+
f"[bold]Auto-restart:[/bold] {'Enabled' if history.auto_restart_enabled else 'Disabled'}",
|
|
386
|
+
title=f"Restart History: {deployment_id}",
|
|
387
|
+
border_style="cyan",
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Show recent attempts
|
|
392
|
+
if history.recent_attempts:
|
|
393
|
+
table = Table(title="Recent Restart Attempts", show_header=True)
|
|
394
|
+
table.add_column("Timestamp", style="dim")
|
|
395
|
+
table.add_column("Success", style="green")
|
|
396
|
+
table.add_column("Reason", style="yellow")
|
|
397
|
+
|
|
398
|
+
for attempt in history.recent_attempts[-10:]: # Last 10
|
|
399
|
+
table.add_row(
|
|
400
|
+
attempt.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
|
|
401
|
+
"✓" if attempt.success else "✗",
|
|
402
|
+
attempt.reason or "Unknown",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
self.console.print("\n")
|
|
406
|
+
self.console.print(table)
|
|
407
|
+
|
|
408
|
+
return CommandResult.success_result("Restart history retrieved")
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
self.logger.error(f"Failed to get restart history: {e}", exc_info=True)
|
|
412
|
+
self.console.print(f"[red]✗ Error: {e}[/red]")
|
|
413
|
+
return CommandResult.error_result(str(e))
|
|
414
|
+
|
|
415
|
+
def _enable_auto_restart_command(self, args) -> CommandResult:
|
|
416
|
+
"""Enable auto-restart for a deployment."""
|
|
417
|
+
try:
|
|
418
|
+
deployment_id = args.deployment_id
|
|
419
|
+
success = self.manager.enable_auto_restart(deployment_id)
|
|
420
|
+
|
|
421
|
+
if success:
|
|
422
|
+
self.console.print(
|
|
423
|
+
f"[green]✓ Auto-restart enabled for {deployment_id}[/green]"
|
|
424
|
+
)
|
|
425
|
+
return CommandResult.success_result(
|
|
426
|
+
f"Auto-restart enabled for {deployment_id}"
|
|
427
|
+
)
|
|
428
|
+
self.console.print(
|
|
429
|
+
f"[red]✗ Failed to enable auto-restart for {deployment_id}[/red]"
|
|
430
|
+
)
|
|
431
|
+
return CommandResult.error_result("Failed to enable auto-restart")
|
|
432
|
+
|
|
433
|
+
except Exception as e:
|
|
434
|
+
self.logger.error(f"Failed to enable auto-restart: {e}", exc_info=True)
|
|
435
|
+
self.console.print(f"[red]✗ Error: {e}[/red]")
|
|
436
|
+
return CommandResult.error_result(str(e))
|
|
437
|
+
|
|
438
|
+
def _disable_auto_restart_command(self, args) -> CommandResult:
|
|
439
|
+
"""Disable auto-restart for a deployment."""
|
|
440
|
+
try:
|
|
441
|
+
deployment_id = args.deployment_id
|
|
442
|
+
success = self.manager.disable_auto_restart(deployment_id)
|
|
443
|
+
|
|
444
|
+
if success:
|
|
445
|
+
self.console.print(
|
|
446
|
+
f"[green]✓ Auto-restart disabled for {deployment_id}[/green]"
|
|
447
|
+
)
|
|
448
|
+
return CommandResult.success_result(
|
|
449
|
+
f"Auto-restart disabled for {deployment_id}"
|
|
450
|
+
)
|
|
451
|
+
self.console.print(
|
|
452
|
+
f"[red]✗ Failed to disable auto-restart for {deployment_id}[/red]"
|
|
453
|
+
)
|
|
454
|
+
return CommandResult.error_result("Failed to disable auto-restart")
|
|
455
|
+
|
|
456
|
+
except Exception as e:
|
|
457
|
+
self.logger.error(f"Failed to disable auto-restart: {e}", exc_info=True)
|
|
458
|
+
self.console.print(f"[red]✗ Error: {e}[/red]")
|
|
459
|
+
return CommandResult.error_result(str(e))
|
|
460
|
+
|
|
461
|
+
def _render_status_panel(self, status: dict) -> None:
|
|
462
|
+
"""Render full status as a rich panel."""
|
|
463
|
+
process = status.get("process", {})
|
|
464
|
+
health = status.get("health", {})
|
|
465
|
+
restart = status.get("restart_history", {})
|
|
466
|
+
|
|
467
|
+
content = "[bold cyan]Process Information[/bold cyan]\n"
|
|
468
|
+
content += f" Status: {process.get('status', 'unknown')}\n"
|
|
469
|
+
content += f" PID: {process.get('pid', 'N/A')}\n"
|
|
470
|
+
content += f" Port: {process.get('port', 'N/A')}\n"
|
|
471
|
+
content += f" Uptime: {process.get('uptime_seconds', 0):.1f}s\n"
|
|
472
|
+
content += f" Memory: {process.get('memory_mb', 0):.1f} MB\n"
|
|
473
|
+
content += f" CPU: {process.get('cpu_percent', 0):.1f}%\n\n"
|
|
474
|
+
|
|
475
|
+
if health:
|
|
476
|
+
content += "[bold green]Health Status[/bold green]\n"
|
|
477
|
+
content += f" Overall: {health.get('status', 'unknown')}\n"
|
|
478
|
+
content += f" HTTP: {'✓' if health.get('http_healthy') else '✗'}\n"
|
|
479
|
+
content += f" Process: {'✓' if health.get('process_healthy') else '✗'}\n"
|
|
480
|
+
content += (
|
|
481
|
+
f" Resources: {'✓' if health.get('resource_healthy') else '✗'}\n\n"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
if restart:
|
|
485
|
+
content += "[bold yellow]Restart Statistics[/bold yellow]\n"
|
|
486
|
+
content += f" Total Restarts: {restart.get('total_restarts', 0)}\n"
|
|
487
|
+
content += f" Successful: {restart.get('successful_restarts', 0)}\n"
|
|
488
|
+
content += f" Failed: {restart.get('failed_restarts', 0)}\n"
|
|
489
|
+
content += f" Auto-restart: {'Enabled' if restart.get('auto_restart_enabled') else 'Disabled'}"
|
|
490
|
+
|
|
491
|
+
self.console.print(
|
|
492
|
+
Panel(
|
|
493
|
+
content,
|
|
494
|
+
title=f"Status: {status.get('deployment_id', 'Unknown')}",
|
|
495
|
+
border_style="cyan",
|
|
496
|
+
)
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
def _render_live_status(self, status: dict) -> Panel:
|
|
500
|
+
"""Render status for live monitoring."""
|
|
501
|
+
process = status.get("process", {})
|
|
502
|
+
health = status.get("health", {})
|
|
503
|
+
|
|
504
|
+
content = Text()
|
|
505
|
+
content.append("Process Status\n", style="bold cyan")
|
|
506
|
+
content.append(f" PID: {process.get('pid', 'N/A')}\n")
|
|
507
|
+
content.append(f" Status: {process.get('status', 'unknown')}\n")
|
|
508
|
+
content.append(f" Uptime: {process.get('uptime_seconds', 0):.1f}s\n")
|
|
509
|
+
content.append(f" Memory: {process.get('memory_mb', 0):.1f} MB\n")
|
|
510
|
+
content.append(f" CPU: {process.get('cpu_percent', 0):.1f}%\n\n")
|
|
511
|
+
|
|
512
|
+
if health:
|
|
513
|
+
health_status = health.get("status", "unknown")
|
|
514
|
+
health_color = {
|
|
515
|
+
"healthy": "green",
|
|
516
|
+
"degraded": "yellow",
|
|
517
|
+
"unhealthy": "red",
|
|
518
|
+
}.get(health_status, "white")
|
|
519
|
+
|
|
520
|
+
content.append("Health Status\n", style="bold green")
|
|
521
|
+
content.append(" Overall: ", style="white")
|
|
522
|
+
content.append(f"{health_status.upper()}\n", style=health_color)
|
|
523
|
+
content.append(
|
|
524
|
+
f" Checks: HTTP={'✓' if health.get('http_healthy') else '✗'} "
|
|
525
|
+
f"Process={'✓' if health.get('process_healthy') else '✗'} "
|
|
526
|
+
f"Resources={'✓' if health.get('resource_healthy') else '✗'}\n"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
return Panel(
|
|
530
|
+
content,
|
|
531
|
+
title=f"Monitoring: {status.get('deployment_id', 'Unknown')}",
|
|
532
|
+
border_style="cyan",
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
__all__ = ["LocalDeployCommand"]
|
|
@@ -336,6 +336,13 @@ def create_parser(
|
|
|
336
336
|
except ImportError:
|
|
337
337
|
pass
|
|
338
338
|
|
|
339
|
+
try:
|
|
340
|
+
from .local_deploy_parser import add_local_deploy_arguments
|
|
341
|
+
|
|
342
|
+
add_local_deploy_arguments(subparsers)
|
|
343
|
+
except ImportError:
|
|
344
|
+
pass
|
|
345
|
+
|
|
339
346
|
try:
|
|
340
347
|
from .mcp_parser import add_mcp_subparser
|
|
341
348
|
|