claude-mpm 4.13.2__py3-none-any.whl → 4.18.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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_ENGINEER.md +286 -0
- claude_mpm/agents/BASE_PM.md +48 -17
- claude_mpm/agents/OUTPUT_STYLE.md +329 -11
- claude_mpm/agents/PM_INSTRUCTIONS.md +227 -8
- claude_mpm/agents/agent_loader.py +17 -5
- claude_mpm/agents/frontmatter_validator.py +284 -253
- claude_mpm/agents/templates/agentic-coder-optimizer.json +9 -2
- claude_mpm/agents/templates/api_qa.json +7 -1
- claude_mpm/agents/templates/clerk-ops.json +8 -1
- claude_mpm/agents/templates/code_analyzer.json +4 -1
- claude_mpm/agents/templates/dart_engineer.json +11 -1
- claude_mpm/agents/templates/data_engineer.json +11 -1
- claude_mpm/agents/templates/documentation.json +6 -1
- claude_mpm/agents/templates/engineer.json +18 -1
- claude_mpm/agents/templates/gcp_ops_agent.json +8 -1
- claude_mpm/agents/templates/golang_engineer.json +11 -1
- claude_mpm/agents/templates/java_engineer.json +12 -2
- claude_mpm/agents/templates/local_ops_agent.json +1217 -6
- claude_mpm/agents/templates/nextjs_engineer.json +11 -1
- claude_mpm/agents/templates/ops.json +8 -1
- claude_mpm/agents/templates/php-engineer.json +11 -1
- claude_mpm/agents/templates/project_organizer.json +10 -3
- claude_mpm/agents/templates/prompt-engineer.json +5 -1
- claude_mpm/agents/templates/python_engineer.json +11 -1
- claude_mpm/agents/templates/qa.json +7 -1
- claude_mpm/agents/templates/react_engineer.json +11 -1
- claude_mpm/agents/templates/refactoring_engineer.json +8 -1
- claude_mpm/agents/templates/research.json +4 -1
- claude_mpm/agents/templates/ruby-engineer.json +11 -1
- claude_mpm/agents/templates/rust_engineer.json +11 -1
- claude_mpm/agents/templates/security.json +6 -1
- claude_mpm/agents/templates/svelte-engineer.json +225 -0
- claude_mpm/agents/templates/ticketing.json +6 -1
- claude_mpm/agents/templates/typescript_engineer.json +11 -1
- claude_mpm/agents/templates/vercel_ops_agent.json +8 -1
- claude_mpm/agents/templates/version_control.json +8 -1
- claude_mpm/agents/templates/web_qa.json +7 -1
- claude_mpm/agents/templates/web_ui.json +11 -1
- claude_mpm/cli/__init__.py +34 -706
- claude_mpm/cli/commands/agent_manager.py +25 -12
- claude_mpm/cli/commands/agent_state_manager.py +186 -0
- claude_mpm/cli/commands/agents.py +204 -148
- claude_mpm/cli/commands/aggregate.py +7 -3
- claude_mpm/cli/commands/analyze.py +9 -4
- claude_mpm/cli/commands/analyze_code.py +7 -2
- claude_mpm/cli/commands/auto_configure.py +7 -9
- claude_mpm/cli/commands/config.py +47 -13
- claude_mpm/cli/commands/configure.py +294 -1788
- claude_mpm/cli/commands/configure_agent_display.py +261 -0
- claude_mpm/cli/commands/configure_behavior_manager.py +204 -0
- claude_mpm/cli/commands/configure_hook_manager.py +225 -0
- claude_mpm/cli/commands/configure_models.py +18 -0
- claude_mpm/cli/commands/configure_navigation.py +167 -0
- claude_mpm/cli/commands/configure_paths.py +104 -0
- claude_mpm/cli/commands/configure_persistence.py +254 -0
- claude_mpm/cli/commands/configure_startup_manager.py +646 -0
- claude_mpm/cli/commands/configure_template_editor.py +497 -0
- claude_mpm/cli/commands/configure_validators.py +73 -0
- claude_mpm/cli/commands/local_deploy.py +537 -0
- claude_mpm/cli/commands/memory.py +54 -20
- claude_mpm/cli/commands/mpm_init.py +39 -25
- claude_mpm/cli/commands/mpm_init_handler.py +8 -3
- claude_mpm/cli/executor.py +202 -0
- claude_mpm/cli/helpers.py +105 -0
- claude_mpm/cli/interactive/__init__.py +3 -0
- claude_mpm/cli/interactive/skills_wizard.py +491 -0
- claude_mpm/cli/parsers/__init__.py +7 -1
- claude_mpm/cli/parsers/base_parser.py +98 -3
- claude_mpm/cli/parsers/local_deploy_parser.py +227 -0
- claude_mpm/cli/shared/output_formatters.py +28 -19
- claude_mpm/cli/startup.py +481 -0
- claude_mpm/cli/utils.py +52 -1
- claude_mpm/commands/mpm-help.md +3 -0
- claude_mpm/commands/mpm-version.md +113 -0
- claude_mpm/commands/mpm.md +1 -0
- claude_mpm/config/agent_config.py +2 -2
- claude_mpm/config/model_config.py +428 -0
- claude_mpm/core/base_service.py +13 -12
- claude_mpm/core/enums.py +452 -0
- claude_mpm/core/factories.py +1 -1
- claude_mpm/core/instruction_reinforcement_hook.py +2 -1
- claude_mpm/core/interactive_session.py +9 -3
- claude_mpm/core/logging_config.py +6 -2
- claude_mpm/core/oneshot_session.py +8 -4
- claude_mpm/core/optimized_agent_loader.py +3 -3
- claude_mpm/core/output_style_manager.py +12 -192
- claude_mpm/core/service_registry.py +5 -1
- claude_mpm/core/types.py +2 -9
- claude_mpm/core/typing_utils.py +7 -6
- claude_mpm/dashboard/static/js/dashboard.js +0 -14
- claude_mpm/dashboard/templates/index.html +3 -41
- claude_mpm/hooks/claude_hooks/response_tracking.py +35 -1
- claude_mpm/hooks/instruction_reinforcement.py +7 -2
- claude_mpm/models/resume_log.py +340 -0
- claude_mpm/services/agents/auto_config_manager.py +10 -11
- claude_mpm/services/agents/deployment/agent_configuration_manager.py +1 -1
- claude_mpm/services/agents/deployment/agent_record_service.py +1 -1
- claude_mpm/services/agents/deployment/agent_validator.py +17 -1
- claude_mpm/services/agents/deployment/async_agent_deployment.py +1 -1
- claude_mpm/services/agents/deployment/interface_adapter.py +3 -2
- claude_mpm/services/agents/deployment/local_template_deployment.py +1 -1
- claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +7 -6
- claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +7 -16
- claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +4 -3
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +5 -3
- claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +6 -5
- claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +9 -6
- claude_mpm/services/agents/deployment/validation/__init__.py +3 -1
- claude_mpm/services/agents/deployment/validation/validation_result.py +1 -9
- claude_mpm/services/agents/local_template_manager.py +1 -1
- claude_mpm/services/agents/memory/agent_memory_manager.py +5 -2
- claude_mpm/services/agents/registry/modification_tracker.py +5 -2
- claude_mpm/services/command_handler_service.py +11 -5
- 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 +33 -0
- claude_mpm/services/core/models/agent_config.py +12 -28
- claude_mpm/services/core/models/health.py +162 -0
- claude_mpm/services/core/models/process.py +235 -0
- claude_mpm/services/core/models/restart.py +302 -0
- claude_mpm/services/core/models/stability.py +264 -0
- claude_mpm/services/core/path_resolver.py +23 -7
- claude_mpm/services/diagnostics/__init__.py +2 -2
- claude_mpm/services/diagnostics/checks/agent_check.py +25 -24
- claude_mpm/services/diagnostics/checks/claude_code_check.py +24 -23
- claude_mpm/services/diagnostics/checks/common_issues_check.py +25 -24
- claude_mpm/services/diagnostics/checks/configuration_check.py +24 -23
- claude_mpm/services/diagnostics/checks/filesystem_check.py +18 -17
- claude_mpm/services/diagnostics/checks/installation_check.py +30 -29
- claude_mpm/services/diagnostics/checks/instructions_check.py +20 -19
- claude_mpm/services/diagnostics/checks/mcp_check.py +50 -36
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +36 -31
- claude_mpm/services/diagnostics/checks/monitor_check.py +23 -22
- claude_mpm/services/diagnostics/checks/startup_log_check.py +9 -8
- claude_mpm/services/diagnostics/diagnostic_runner.py +6 -5
- claude_mpm/services/diagnostics/doctor_reporter.py +28 -25
- claude_mpm/services/diagnostics/models.py +19 -24
- claude_mpm/services/infrastructure/monitoring/__init__.py +1 -1
- claude_mpm/services/infrastructure/monitoring/aggregator.py +12 -12
- claude_mpm/services/infrastructure/monitoring/base.py +5 -13
- claude_mpm/services/infrastructure/monitoring/network.py +7 -6
- claude_mpm/services/infrastructure/monitoring/process.py +13 -12
- claude_mpm/services/infrastructure/monitoring/resources.py +7 -6
- claude_mpm/services/infrastructure/monitoring/service.py +16 -15
- claude_mpm/services/infrastructure/resume_log_generator.py +439 -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 +224 -0
- claude_mpm/services/local_ops/health_checks/process_check.py +236 -0
- claude_mpm/services/local_ops/health_checks/resource_check.py +255 -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 +372 -0
- claude_mpm/services/local_ops/unified_manager.py +600 -0
- claude_mpm/services/mcp_config_manager.py +9 -4
- claude_mpm/services/mcp_gateway/core/__init__.py +1 -2
- claude_mpm/services/mcp_gateway/core/base.py +18 -31
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +71 -24
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +30 -28
- claude_mpm/services/memory_hook_service.py +4 -1
- 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/services/monitor/daemon_manager.py +3 -2
- claude_mpm/services/monitor/handlers/dashboard.py +2 -1
- claude_mpm/services/monitor/handlers/hooks.py +2 -1
- claude_mpm/services/monitor/management/lifecycle.py +3 -2
- claude_mpm/services/monitor/server.py +2 -1
- claude_mpm/services/session_management_service.py +3 -2
- claude_mpm/services/session_manager.py +205 -1
- claude_mpm/services/shared/async_service_base.py +16 -27
- claude_mpm/services/shared/lifecycle_service_base.py +1 -14
- claude_mpm/services/socketio/handlers/__init__.py +5 -2
- claude_mpm/services/socketio/handlers/hook.py +13 -2
- claude_mpm/services/socketio/handlers/registry.py +4 -2
- claude_mpm/services/socketio/server/main.py +10 -8
- claude_mpm/services/subprocess_launcher_service.py +14 -5
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +8 -7
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +6 -5
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +8 -7
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +7 -6
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +5 -4
- claude_mpm/services/unified/config_strategies/validation_strategy.py +13 -9
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +10 -3
- claude_mpm/services/unified/deployment_strategies/local.py +6 -5
- claude_mpm/services/unified/deployment_strategies/utils.py +6 -5
- claude_mpm/services/unified/deployment_strategies/vercel.py +7 -6
- claude_mpm/services/unified/interfaces.py +3 -1
- claude_mpm/services/unified/unified_analyzer.py +14 -10
- claude_mpm/services/unified/unified_config.py +2 -1
- claude_mpm/services/unified/unified_deployment.py +9 -4
- claude_mpm/services/version_service.py +104 -1
- claude_mpm/skills/__init__.py +21 -0
- claude_mpm/skills/bundled/__init__.py +6 -0
- claude_mpm/skills/bundled/api-documentation.md +393 -0
- claude_mpm/skills/bundled/async-testing.md +571 -0
- claude_mpm/skills/bundled/code-review.md +143 -0
- claude_mpm/skills/bundled/database-migration.md +199 -0
- claude_mpm/skills/bundled/docker-containerization.md +194 -0
- claude_mpm/skills/bundled/express-local-dev.md +1429 -0
- claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
- claude_mpm/skills/bundled/git-workflow.md +414 -0
- claude_mpm/skills/bundled/imagemagick.md +204 -0
- claude_mpm/skills/bundled/json-data-handling.md +223 -0
- claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
- claude_mpm/skills/bundled/pdf.md +141 -0
- claude_mpm/skills/bundled/performance-profiling.md +567 -0
- claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
- claude_mpm/skills/bundled/security-scanning.md +327 -0
- claude_mpm/skills/bundled/systematic-debugging.md +473 -0
- claude_mpm/skills/bundled/test-driven-development.md +378 -0
- claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
- claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
- claude_mpm/skills/bundled/xlsx.md +157 -0
- claude_mpm/skills/registry.py +286 -0
- claude_mpm/skills/skill_manager.py +310 -0
- claude_mpm/tools/code_tree_analyzer.py +177 -141
- claude_mpm/tools/code_tree_events.py +4 -2
- claude_mpm/utils/agent_dependency_loader.py +2 -2
- {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/METADATA +117 -8
- {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/RECORD +238 -174
- claude_mpm/dashboard/static/css/code-tree.css +0 -1639
- claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
- claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
- claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
- claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
- claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
- claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +0 -425
- claude_mpm/hooks/claude_hooks/hook_handler_original.py +0 -1041
- claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +0 -347
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +0 -575
- claude_mpm/services/project/analyzer_refactored.py +0 -450
- {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/WHEEL +0 -0
- {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deployment State Manager for Claude MPM Framework
|
|
3
|
+
=================================================
|
|
4
|
+
|
|
5
|
+
WHY: Provides persistent state tracking for local deployments with atomic
|
|
6
|
+
operations, file locking, and corruption recovery. Critical for preventing
|
|
7
|
+
orphaned processes and ensuring deployment reliability.
|
|
8
|
+
|
|
9
|
+
DESIGN DECISION: Uses JSON file storage with filelock for simplicity and
|
|
10
|
+
portability. File-based storage is sufficient for local deployments and
|
|
11
|
+
doesn't require external dependencies.
|
|
12
|
+
|
|
13
|
+
ARCHITECTURE:
|
|
14
|
+
- Thread-safe operations with file locking
|
|
15
|
+
- Atomic read-modify-write cycles
|
|
16
|
+
- Automatic corruption detection and recovery
|
|
17
|
+
- Process validation using psutil
|
|
18
|
+
|
|
19
|
+
USAGE:
|
|
20
|
+
manager = DeploymentStateManager(state_file_path)
|
|
21
|
+
manager.add_deployment(deployment_state)
|
|
22
|
+
deployments = manager.get_all_deployments()
|
|
23
|
+
manager.cleanup_dead_pids()
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Dict, List, Optional
|
|
29
|
+
|
|
30
|
+
import psutil
|
|
31
|
+
from filelock import FileLock
|
|
32
|
+
|
|
33
|
+
from claude_mpm.core.enums import ServiceState
|
|
34
|
+
from claude_mpm.services.core.base import SyncBaseService
|
|
35
|
+
from claude_mpm.services.core.interfaces.process import IDeploymentStateManager
|
|
36
|
+
from claude_mpm.services.core.models.process import DeploymentState
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class StateCorruptionError(Exception):
|
|
40
|
+
"""Raised when state file is corrupted and cannot be recovered."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DeploymentStateManager(SyncBaseService, IDeploymentStateManager):
|
|
44
|
+
"""
|
|
45
|
+
Manages persistent deployment state with atomic operations.
|
|
46
|
+
|
|
47
|
+
WHY: Deployment state must survive restarts and be accessible to
|
|
48
|
+
multiple processes. This manager ensures consistency with file locking
|
|
49
|
+
and provides corruption recovery.
|
|
50
|
+
|
|
51
|
+
Thread Safety: All public methods use file locking for atomicity.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, state_file_path: str):
|
|
55
|
+
"""
|
|
56
|
+
Initialize state manager.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
state_file_path: Path to JSON state file
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If state_file_path is invalid
|
|
63
|
+
"""
|
|
64
|
+
super().__init__("DeploymentStateManager")
|
|
65
|
+
|
|
66
|
+
self.state_file = Path(state_file_path)
|
|
67
|
+
self.lock_file = Path(str(state_file_path) + ".lock")
|
|
68
|
+
|
|
69
|
+
# Create single FileLock instance for re-entrant locking
|
|
70
|
+
# WHY: Using the same lock instance allows re-entrant calls
|
|
71
|
+
# (e.g., add_deployment -> load_state) without deadlock
|
|
72
|
+
self._file_lock = FileLock(str(self.lock_file), timeout=10)
|
|
73
|
+
|
|
74
|
+
# Ensure parent directory exists
|
|
75
|
+
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
|
|
77
|
+
# Initialize empty state if file doesn't exist
|
|
78
|
+
if not self.state_file.exists():
|
|
79
|
+
self._write_state({})
|
|
80
|
+
|
|
81
|
+
self.log_info(f"Initialized state manager with file: {self.state_file}")
|
|
82
|
+
|
|
83
|
+
def initialize(self) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
Initialize the state manager.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
True if initialization successful
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
# Validate state file can be read
|
|
92
|
+
self.load_state()
|
|
93
|
+
self._initialized = True
|
|
94
|
+
return True
|
|
95
|
+
except Exception as e:
|
|
96
|
+
self.log_error(f"Failed to initialize: {e}")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def shutdown(self) -> None:
|
|
100
|
+
"""Shutdown state manager (no resources to clean up)."""
|
|
101
|
+
self._shutdown = True
|
|
102
|
+
self.log_info("State manager shutdown complete")
|
|
103
|
+
|
|
104
|
+
def load_state(self) -> Dict[str, DeploymentState]:
|
|
105
|
+
"""
|
|
106
|
+
Load all deployment states from file.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Dictionary mapping deployment_id to DeploymentState
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
StateCorruptionError: If state file is corrupted beyond recovery
|
|
113
|
+
"""
|
|
114
|
+
with self._file_lock:
|
|
115
|
+
try:
|
|
116
|
+
if not self.state_file.exists():
|
|
117
|
+
return {}
|
|
118
|
+
|
|
119
|
+
with self.state_file.open() as f:
|
|
120
|
+
data = json.load(f)
|
|
121
|
+
|
|
122
|
+
# Convert dict entries to DeploymentState objects
|
|
123
|
+
states = {}
|
|
124
|
+
for deployment_id, state_dict in data.items():
|
|
125
|
+
try:
|
|
126
|
+
states[deployment_id] = DeploymentState.from_dict(state_dict)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
self.log_warning(
|
|
129
|
+
f"Skipping corrupted state entry {deployment_id}: {e}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return states
|
|
133
|
+
|
|
134
|
+
except json.JSONDecodeError as e:
|
|
135
|
+
self.log_error(f"State file corrupted: {e}")
|
|
136
|
+
# Attempt recovery by backing up and creating fresh state
|
|
137
|
+
backup_path = self.state_file.with_suffix(".json.corrupted")
|
|
138
|
+
self.state_file.rename(backup_path)
|
|
139
|
+
self.log_warning(f"Backed up corrupted state to {backup_path}")
|
|
140
|
+
self._write_state({})
|
|
141
|
+
return {}
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
raise StateCorruptionError(f"Failed to load state: {e}") from e
|
|
145
|
+
|
|
146
|
+
def save_state(self, states: Dict[str, DeploymentState]) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Save all deployment states to file.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
states: Dictionary mapping deployment_id to DeploymentState
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
IOError: If state file cannot be written
|
|
155
|
+
"""
|
|
156
|
+
with self._file_lock:
|
|
157
|
+
self._write_state(states)
|
|
158
|
+
|
|
159
|
+
def _write_state(self, states: Dict[str, DeploymentState]) -> None:
|
|
160
|
+
"""
|
|
161
|
+
Internal method to write state without locking.
|
|
162
|
+
|
|
163
|
+
WHY: Allows caller to handle locking for atomic operations.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
states: States to write (can be dict or DeploymentState dict)
|
|
167
|
+
"""
|
|
168
|
+
# Convert DeploymentState objects to dicts
|
|
169
|
+
data = {}
|
|
170
|
+
for deployment_id, state in states.items():
|
|
171
|
+
if isinstance(state, DeploymentState):
|
|
172
|
+
data[deployment_id] = state.to_dict()
|
|
173
|
+
else:
|
|
174
|
+
data[deployment_id] = state
|
|
175
|
+
|
|
176
|
+
# Atomic write: write to temp file then rename
|
|
177
|
+
temp_file = self.state_file.with_suffix(".tmp")
|
|
178
|
+
try:
|
|
179
|
+
with temp_file.open("w") as f:
|
|
180
|
+
json.dump(data, f, indent=2)
|
|
181
|
+
temp_file.replace(self.state_file)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
if temp_file.exists():
|
|
184
|
+
temp_file.unlink()
|
|
185
|
+
raise OSError(f"Failed to write state: {e}") from e
|
|
186
|
+
|
|
187
|
+
def get_deployment(self, deployment_id: str) -> Optional[DeploymentState]:
|
|
188
|
+
"""
|
|
189
|
+
Get a specific deployment by ID.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
deployment_id: Unique deployment identifier
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
DeploymentState if found, None otherwise
|
|
196
|
+
"""
|
|
197
|
+
states = self.load_state()
|
|
198
|
+
return states.get(deployment_id)
|
|
199
|
+
|
|
200
|
+
def get_all_deployments(self) -> List[DeploymentState]:
|
|
201
|
+
"""
|
|
202
|
+
Get all tracked deployments.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of all DeploymentState objects
|
|
206
|
+
"""
|
|
207
|
+
states = self.load_state()
|
|
208
|
+
return list(states.values())
|
|
209
|
+
|
|
210
|
+
def get_deployments_by_status(self, status: ServiceState) -> List[DeploymentState]:
|
|
211
|
+
"""
|
|
212
|
+
Get all deployments with a specific status.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
status: ServiceState to filter by
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
List of matching DeploymentState objects
|
|
219
|
+
"""
|
|
220
|
+
states = self.load_state()
|
|
221
|
+
return [s for s in states.values() if s.status == status]
|
|
222
|
+
|
|
223
|
+
def get_deployment_by_port(self, port: int) -> Optional[DeploymentState]:
|
|
224
|
+
"""
|
|
225
|
+
Get deployment using a specific port.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
port: Port number to search for
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
DeploymentState if found, None otherwise
|
|
232
|
+
"""
|
|
233
|
+
states = self.load_state()
|
|
234
|
+
for state in states.values():
|
|
235
|
+
if state.port == port:
|
|
236
|
+
return state
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
def get_deployments_by_project(
|
|
240
|
+
self, working_directory: str
|
|
241
|
+
) -> List[DeploymentState]:
|
|
242
|
+
"""
|
|
243
|
+
Get all deployments for a specific project directory.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
working_directory: Project directory path
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
List of matching DeploymentState objects
|
|
250
|
+
"""
|
|
251
|
+
# Normalize path for comparison
|
|
252
|
+
normalized_dir = str(Path(working_directory).absolute())
|
|
253
|
+
states = self.load_state()
|
|
254
|
+
return [
|
|
255
|
+
s
|
|
256
|
+
for s in states.values()
|
|
257
|
+
if str(Path(s.working_directory).absolute()) == normalized_dir
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
def add_deployment(self, deployment: DeploymentState) -> None:
|
|
261
|
+
"""
|
|
262
|
+
Add or update a deployment in state.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
deployment: DeploymentState to add/update
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
IOError: If state cannot be persisted
|
|
269
|
+
"""
|
|
270
|
+
with self._file_lock:
|
|
271
|
+
states = self.load_state()
|
|
272
|
+
states[deployment.deployment_id] = deployment
|
|
273
|
+
self._write_state(states)
|
|
274
|
+
self.log_debug(f"Added deployment: {deployment.deployment_id}")
|
|
275
|
+
|
|
276
|
+
def remove_deployment(self, deployment_id: str) -> bool:
|
|
277
|
+
"""
|
|
278
|
+
Remove a deployment from state.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
deployment_id: Unique deployment identifier
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
True if deployment was removed, False if not found
|
|
285
|
+
|
|
286
|
+
Raises:
|
|
287
|
+
IOError: If state cannot be persisted
|
|
288
|
+
"""
|
|
289
|
+
with self._file_lock:
|
|
290
|
+
states = self.load_state()
|
|
291
|
+
if deployment_id in states:
|
|
292
|
+
del states[deployment_id]
|
|
293
|
+
self._write_state(states)
|
|
294
|
+
self.log_debug(f"Removed deployment: {deployment_id}")
|
|
295
|
+
return True
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
def update_deployment_status(
|
|
299
|
+
self, deployment_id: str, status: ServiceState
|
|
300
|
+
) -> bool:
|
|
301
|
+
"""
|
|
302
|
+
Update the status of a deployment.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
deployment_id: Unique deployment identifier
|
|
306
|
+
status: New ServiceState
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
True if updated, False if deployment not found
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
IOError: If state cannot be persisted
|
|
313
|
+
"""
|
|
314
|
+
with self._file_lock:
|
|
315
|
+
states = self.load_state()
|
|
316
|
+
if deployment_id in states:
|
|
317
|
+
states[deployment_id].status = status
|
|
318
|
+
self._write_state(states)
|
|
319
|
+
self.log_debug(f"Updated status for {deployment_id}: {status.value}")
|
|
320
|
+
return True
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
def cleanup_dead_pids(self) -> int:
|
|
324
|
+
"""
|
|
325
|
+
Remove deployments with dead process IDs.
|
|
326
|
+
|
|
327
|
+
WHY: Processes may crash or be killed externally. This method cleans
|
|
328
|
+
up stale state entries for processes that no longer exist.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Number of dead PIDs cleaned up
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
IOError: If state cannot be persisted
|
|
335
|
+
"""
|
|
336
|
+
with self._file_lock:
|
|
337
|
+
states = self.load_state()
|
|
338
|
+
cleaned_count = 0
|
|
339
|
+
|
|
340
|
+
for deployment_id, state in list(states.items()):
|
|
341
|
+
if not self._is_pid_alive(state.process_id):
|
|
342
|
+
self.log_info(
|
|
343
|
+
f"Cleaning dead PID {state.process_id} for {deployment_id}"
|
|
344
|
+
)
|
|
345
|
+
del states[deployment_id]
|
|
346
|
+
cleaned_count += 1
|
|
347
|
+
|
|
348
|
+
if cleaned_count > 0:
|
|
349
|
+
self._write_state(states)
|
|
350
|
+
self.log_info(f"Cleaned up {cleaned_count} dead PIDs")
|
|
351
|
+
|
|
352
|
+
return cleaned_count
|
|
353
|
+
|
|
354
|
+
def _is_pid_alive(self, pid: int) -> bool:
|
|
355
|
+
"""
|
|
356
|
+
Check if a process ID is alive.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
pid: Process ID to check
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
True if process exists and is running
|
|
363
|
+
"""
|
|
364
|
+
try:
|
|
365
|
+
process = psutil.Process(pid)
|
|
366
|
+
# Check if process still exists and is not a zombie
|
|
367
|
+
return process.is_running() and process.status() != psutil.STATUS_ZOMBIE
|
|
368
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
369
|
+
return False
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
__all__ = ["DeploymentStateManager", "StateCorruptionError"]
|