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.
Files changed (250) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_ENGINEER.md +286 -0
  3. claude_mpm/agents/BASE_PM.md +48 -17
  4. claude_mpm/agents/OUTPUT_STYLE.md +329 -11
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +227 -8
  6. claude_mpm/agents/agent_loader.py +17 -5
  7. claude_mpm/agents/frontmatter_validator.py +284 -253
  8. claude_mpm/agents/templates/agentic-coder-optimizer.json +9 -2
  9. claude_mpm/agents/templates/api_qa.json +7 -1
  10. claude_mpm/agents/templates/clerk-ops.json +8 -1
  11. claude_mpm/agents/templates/code_analyzer.json +4 -1
  12. claude_mpm/agents/templates/dart_engineer.json +11 -1
  13. claude_mpm/agents/templates/data_engineer.json +11 -1
  14. claude_mpm/agents/templates/documentation.json +6 -1
  15. claude_mpm/agents/templates/engineer.json +18 -1
  16. claude_mpm/agents/templates/gcp_ops_agent.json +8 -1
  17. claude_mpm/agents/templates/golang_engineer.json +11 -1
  18. claude_mpm/agents/templates/java_engineer.json +12 -2
  19. claude_mpm/agents/templates/local_ops_agent.json +1217 -6
  20. claude_mpm/agents/templates/nextjs_engineer.json +11 -1
  21. claude_mpm/agents/templates/ops.json +8 -1
  22. claude_mpm/agents/templates/php-engineer.json +11 -1
  23. claude_mpm/agents/templates/project_organizer.json +10 -3
  24. claude_mpm/agents/templates/prompt-engineer.json +5 -1
  25. claude_mpm/agents/templates/python_engineer.json +11 -1
  26. claude_mpm/agents/templates/qa.json +7 -1
  27. claude_mpm/agents/templates/react_engineer.json +11 -1
  28. claude_mpm/agents/templates/refactoring_engineer.json +8 -1
  29. claude_mpm/agents/templates/research.json +4 -1
  30. claude_mpm/agents/templates/ruby-engineer.json +11 -1
  31. claude_mpm/agents/templates/rust_engineer.json +11 -1
  32. claude_mpm/agents/templates/security.json +6 -1
  33. claude_mpm/agents/templates/svelte-engineer.json +225 -0
  34. claude_mpm/agents/templates/ticketing.json +6 -1
  35. claude_mpm/agents/templates/typescript_engineer.json +11 -1
  36. claude_mpm/agents/templates/vercel_ops_agent.json +8 -1
  37. claude_mpm/agents/templates/version_control.json +8 -1
  38. claude_mpm/agents/templates/web_qa.json +7 -1
  39. claude_mpm/agents/templates/web_ui.json +11 -1
  40. claude_mpm/cli/__init__.py +34 -706
  41. claude_mpm/cli/commands/agent_manager.py +25 -12
  42. claude_mpm/cli/commands/agent_state_manager.py +186 -0
  43. claude_mpm/cli/commands/agents.py +204 -148
  44. claude_mpm/cli/commands/aggregate.py +7 -3
  45. claude_mpm/cli/commands/analyze.py +9 -4
  46. claude_mpm/cli/commands/analyze_code.py +7 -2
  47. claude_mpm/cli/commands/auto_configure.py +7 -9
  48. claude_mpm/cli/commands/config.py +47 -13
  49. claude_mpm/cli/commands/configure.py +294 -1788
  50. claude_mpm/cli/commands/configure_agent_display.py +261 -0
  51. claude_mpm/cli/commands/configure_behavior_manager.py +204 -0
  52. claude_mpm/cli/commands/configure_hook_manager.py +225 -0
  53. claude_mpm/cli/commands/configure_models.py +18 -0
  54. claude_mpm/cli/commands/configure_navigation.py +167 -0
  55. claude_mpm/cli/commands/configure_paths.py +104 -0
  56. claude_mpm/cli/commands/configure_persistence.py +254 -0
  57. claude_mpm/cli/commands/configure_startup_manager.py +646 -0
  58. claude_mpm/cli/commands/configure_template_editor.py +497 -0
  59. claude_mpm/cli/commands/configure_validators.py +73 -0
  60. claude_mpm/cli/commands/local_deploy.py +537 -0
  61. claude_mpm/cli/commands/memory.py +54 -20
  62. claude_mpm/cli/commands/mpm_init.py +39 -25
  63. claude_mpm/cli/commands/mpm_init_handler.py +8 -3
  64. claude_mpm/cli/executor.py +202 -0
  65. claude_mpm/cli/helpers.py +105 -0
  66. claude_mpm/cli/interactive/__init__.py +3 -0
  67. claude_mpm/cli/interactive/skills_wizard.py +491 -0
  68. claude_mpm/cli/parsers/__init__.py +7 -1
  69. claude_mpm/cli/parsers/base_parser.py +98 -3
  70. claude_mpm/cli/parsers/local_deploy_parser.py +227 -0
  71. claude_mpm/cli/shared/output_formatters.py +28 -19
  72. claude_mpm/cli/startup.py +481 -0
  73. claude_mpm/cli/utils.py +52 -1
  74. claude_mpm/commands/mpm-help.md +3 -0
  75. claude_mpm/commands/mpm-version.md +113 -0
  76. claude_mpm/commands/mpm.md +1 -0
  77. claude_mpm/config/agent_config.py +2 -2
  78. claude_mpm/config/model_config.py +428 -0
  79. claude_mpm/core/base_service.py +13 -12
  80. claude_mpm/core/enums.py +452 -0
  81. claude_mpm/core/factories.py +1 -1
  82. claude_mpm/core/instruction_reinforcement_hook.py +2 -1
  83. claude_mpm/core/interactive_session.py +9 -3
  84. claude_mpm/core/logging_config.py +6 -2
  85. claude_mpm/core/oneshot_session.py +8 -4
  86. claude_mpm/core/optimized_agent_loader.py +3 -3
  87. claude_mpm/core/output_style_manager.py +12 -192
  88. claude_mpm/core/service_registry.py +5 -1
  89. claude_mpm/core/types.py +2 -9
  90. claude_mpm/core/typing_utils.py +7 -6
  91. claude_mpm/dashboard/static/js/dashboard.js +0 -14
  92. claude_mpm/dashboard/templates/index.html +3 -41
  93. claude_mpm/hooks/claude_hooks/response_tracking.py +35 -1
  94. claude_mpm/hooks/instruction_reinforcement.py +7 -2
  95. claude_mpm/models/resume_log.py +340 -0
  96. claude_mpm/services/agents/auto_config_manager.py +10 -11
  97. claude_mpm/services/agents/deployment/agent_configuration_manager.py +1 -1
  98. claude_mpm/services/agents/deployment/agent_record_service.py +1 -1
  99. claude_mpm/services/agents/deployment/agent_validator.py +17 -1
  100. claude_mpm/services/agents/deployment/async_agent_deployment.py +1 -1
  101. claude_mpm/services/agents/deployment/interface_adapter.py +3 -2
  102. claude_mpm/services/agents/deployment/local_template_deployment.py +1 -1
  103. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +7 -6
  104. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +7 -16
  105. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +4 -3
  106. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +5 -3
  107. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +6 -5
  108. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +9 -6
  109. claude_mpm/services/agents/deployment/validation/__init__.py +3 -1
  110. claude_mpm/services/agents/deployment/validation/validation_result.py +1 -9
  111. claude_mpm/services/agents/local_template_manager.py +1 -1
  112. claude_mpm/services/agents/memory/agent_memory_manager.py +5 -2
  113. claude_mpm/services/agents/registry/modification_tracker.py +5 -2
  114. claude_mpm/services/command_handler_service.py +11 -5
  115. claude_mpm/services/core/interfaces/__init__.py +74 -2
  116. claude_mpm/services/core/interfaces/health.py +172 -0
  117. claude_mpm/services/core/interfaces/model.py +281 -0
  118. claude_mpm/services/core/interfaces/process.py +372 -0
  119. claude_mpm/services/core/interfaces/restart.py +307 -0
  120. claude_mpm/services/core/interfaces/stability.py +260 -0
  121. claude_mpm/services/core/models/__init__.py +33 -0
  122. claude_mpm/services/core/models/agent_config.py +12 -28
  123. claude_mpm/services/core/models/health.py +162 -0
  124. claude_mpm/services/core/models/process.py +235 -0
  125. claude_mpm/services/core/models/restart.py +302 -0
  126. claude_mpm/services/core/models/stability.py +264 -0
  127. claude_mpm/services/core/path_resolver.py +23 -7
  128. claude_mpm/services/diagnostics/__init__.py +2 -2
  129. claude_mpm/services/diagnostics/checks/agent_check.py +25 -24
  130. claude_mpm/services/diagnostics/checks/claude_code_check.py +24 -23
  131. claude_mpm/services/diagnostics/checks/common_issues_check.py +25 -24
  132. claude_mpm/services/diagnostics/checks/configuration_check.py +24 -23
  133. claude_mpm/services/diagnostics/checks/filesystem_check.py +18 -17
  134. claude_mpm/services/diagnostics/checks/installation_check.py +30 -29
  135. claude_mpm/services/diagnostics/checks/instructions_check.py +20 -19
  136. claude_mpm/services/diagnostics/checks/mcp_check.py +50 -36
  137. claude_mpm/services/diagnostics/checks/mcp_services_check.py +36 -31
  138. claude_mpm/services/diagnostics/checks/monitor_check.py +23 -22
  139. claude_mpm/services/diagnostics/checks/startup_log_check.py +9 -8
  140. claude_mpm/services/diagnostics/diagnostic_runner.py +6 -5
  141. claude_mpm/services/diagnostics/doctor_reporter.py +28 -25
  142. claude_mpm/services/diagnostics/models.py +19 -24
  143. claude_mpm/services/infrastructure/monitoring/__init__.py +1 -1
  144. claude_mpm/services/infrastructure/monitoring/aggregator.py +12 -12
  145. claude_mpm/services/infrastructure/monitoring/base.py +5 -13
  146. claude_mpm/services/infrastructure/monitoring/network.py +7 -6
  147. claude_mpm/services/infrastructure/monitoring/process.py +13 -12
  148. claude_mpm/services/infrastructure/monitoring/resources.py +7 -6
  149. claude_mpm/services/infrastructure/monitoring/service.py +16 -15
  150. claude_mpm/services/infrastructure/resume_log_generator.py +439 -0
  151. claude_mpm/services/local_ops/__init__.py +163 -0
  152. claude_mpm/services/local_ops/crash_detector.py +257 -0
  153. claude_mpm/services/local_ops/health_checks/__init__.py +28 -0
  154. claude_mpm/services/local_ops/health_checks/http_check.py +224 -0
  155. claude_mpm/services/local_ops/health_checks/process_check.py +236 -0
  156. claude_mpm/services/local_ops/health_checks/resource_check.py +255 -0
  157. claude_mpm/services/local_ops/health_manager.py +430 -0
  158. claude_mpm/services/local_ops/log_monitor.py +396 -0
  159. claude_mpm/services/local_ops/memory_leak_detector.py +294 -0
  160. claude_mpm/services/local_ops/process_manager.py +595 -0
  161. claude_mpm/services/local_ops/resource_monitor.py +331 -0
  162. claude_mpm/services/local_ops/restart_manager.py +401 -0
  163. claude_mpm/services/local_ops/restart_policy.py +387 -0
  164. claude_mpm/services/local_ops/state_manager.py +372 -0
  165. claude_mpm/services/local_ops/unified_manager.py +600 -0
  166. claude_mpm/services/mcp_config_manager.py +9 -4
  167. claude_mpm/services/mcp_gateway/core/__init__.py +1 -2
  168. claude_mpm/services/mcp_gateway/core/base.py +18 -31
  169. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +71 -24
  170. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +30 -28
  171. claude_mpm/services/memory_hook_service.py +4 -1
  172. claude_mpm/services/model/__init__.py +147 -0
  173. claude_mpm/services/model/base_provider.py +365 -0
  174. claude_mpm/services/model/claude_provider.py +412 -0
  175. claude_mpm/services/model/model_router.py +453 -0
  176. claude_mpm/services/model/ollama_provider.py +415 -0
  177. claude_mpm/services/monitor/daemon_manager.py +3 -2
  178. claude_mpm/services/monitor/handlers/dashboard.py +2 -1
  179. claude_mpm/services/monitor/handlers/hooks.py +2 -1
  180. claude_mpm/services/monitor/management/lifecycle.py +3 -2
  181. claude_mpm/services/monitor/server.py +2 -1
  182. claude_mpm/services/session_management_service.py +3 -2
  183. claude_mpm/services/session_manager.py +205 -1
  184. claude_mpm/services/shared/async_service_base.py +16 -27
  185. claude_mpm/services/shared/lifecycle_service_base.py +1 -14
  186. claude_mpm/services/socketio/handlers/__init__.py +5 -2
  187. claude_mpm/services/socketio/handlers/hook.py +13 -2
  188. claude_mpm/services/socketio/handlers/registry.py +4 -2
  189. claude_mpm/services/socketio/server/main.py +10 -8
  190. claude_mpm/services/subprocess_launcher_service.py +14 -5
  191. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +8 -7
  192. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +6 -5
  193. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +8 -7
  194. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +7 -6
  195. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +5 -4
  196. claude_mpm/services/unified/config_strategies/validation_strategy.py +13 -9
  197. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +10 -3
  198. claude_mpm/services/unified/deployment_strategies/local.py +6 -5
  199. claude_mpm/services/unified/deployment_strategies/utils.py +6 -5
  200. claude_mpm/services/unified/deployment_strategies/vercel.py +7 -6
  201. claude_mpm/services/unified/interfaces.py +3 -1
  202. claude_mpm/services/unified/unified_analyzer.py +14 -10
  203. claude_mpm/services/unified/unified_config.py +2 -1
  204. claude_mpm/services/unified/unified_deployment.py +9 -4
  205. claude_mpm/services/version_service.py +104 -1
  206. claude_mpm/skills/__init__.py +21 -0
  207. claude_mpm/skills/bundled/__init__.py +6 -0
  208. claude_mpm/skills/bundled/api-documentation.md +393 -0
  209. claude_mpm/skills/bundled/async-testing.md +571 -0
  210. claude_mpm/skills/bundled/code-review.md +143 -0
  211. claude_mpm/skills/bundled/database-migration.md +199 -0
  212. claude_mpm/skills/bundled/docker-containerization.md +194 -0
  213. claude_mpm/skills/bundled/express-local-dev.md +1429 -0
  214. claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
  215. claude_mpm/skills/bundled/git-workflow.md +414 -0
  216. claude_mpm/skills/bundled/imagemagick.md +204 -0
  217. claude_mpm/skills/bundled/json-data-handling.md +223 -0
  218. claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
  219. claude_mpm/skills/bundled/pdf.md +141 -0
  220. claude_mpm/skills/bundled/performance-profiling.md +567 -0
  221. claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
  222. claude_mpm/skills/bundled/security-scanning.md +327 -0
  223. claude_mpm/skills/bundled/systematic-debugging.md +473 -0
  224. claude_mpm/skills/bundled/test-driven-development.md +378 -0
  225. claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
  226. claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
  227. claude_mpm/skills/bundled/xlsx.md +157 -0
  228. claude_mpm/skills/registry.py +286 -0
  229. claude_mpm/skills/skill_manager.py +310 -0
  230. claude_mpm/tools/code_tree_analyzer.py +177 -141
  231. claude_mpm/tools/code_tree_events.py +4 -2
  232. claude_mpm/utils/agent_dependency_loader.py +2 -2
  233. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/METADATA +117 -8
  234. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/RECORD +238 -174
  235. claude_mpm/dashboard/static/css/code-tree.css +0 -1639
  236. claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
  237. claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
  238. claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
  239. claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
  240. claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
  241. claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
  242. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +0 -425
  243. claude_mpm/hooks/claude_hooks/hook_handler_original.py +0 -1041
  244. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +0 -347
  245. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +0 -575
  246. claude_mpm/services/project/analyzer_refactored.py +0 -450
  247. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/WHEEL +0 -0
  248. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/entry_points.txt +0 -0
  249. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/licenses/LICENSE +0 -0
  250. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,595 @@
1
+ """
2
+ Local Process Manager for Claude MPM Framework
3
+ ==============================================
4
+
5
+ WHY: Provides reliable process lifecycle management for local deployments
6
+ with process isolation, port conflict prevention, and graceful shutdown.
7
+
8
+ DESIGN DECISION: Uses subprocess.Popen for direct process control with
9
+ process groups for clean termination. Integrates with DeploymentStateManager
10
+ for persistent tracking.
11
+
12
+ ARCHITECTURE:
13
+ - Process group isolation (start_new_session=True on Unix)
14
+ - Port conflict detection using psutil
15
+ - Linear probing for alternative ports
16
+ - Protected port range enforcement
17
+ - Graceful shutdown with timeout and force kill fallback
18
+
19
+ USAGE:
20
+ state_manager = DeploymentStateManager(state_file_path)
21
+ process_manager = LocalProcessManager(state_manager)
22
+
23
+ config = StartConfig(
24
+ command=["npm", "run", "dev"],
25
+ working_directory="/path/to/project",
26
+ port=3000
27
+ )
28
+
29
+ deployment = process_manager.start(config)
30
+ process_manager.stop(deployment.deployment_id)
31
+ """
32
+
33
+ import os
34
+ import platform
35
+ import signal
36
+ import subprocess
37
+ import time
38
+ from datetime import datetime, timezone
39
+ from hashlib import sha256
40
+ from pathlib import Path
41
+ from typing import List, Optional
42
+
43
+ import psutil
44
+
45
+ from claude_mpm.core.enums import ServiceState
46
+ from claude_mpm.services.core.base import SyncBaseService
47
+ from claude_mpm.services.core.interfaces.process import (
48
+ IDeploymentStateManager,
49
+ ILocalProcessManager,
50
+ )
51
+ from claude_mpm.services.core.models.process import (
52
+ DeploymentState,
53
+ ProcessInfo,
54
+ StartConfig,
55
+ is_port_protected,
56
+ )
57
+
58
+
59
+ class ProcessSpawnError(Exception):
60
+ """Raised when process cannot be spawned."""
61
+
62
+
63
+ class PortConflictError(Exception):
64
+ """Raised when requested port is unavailable and no alternative found."""
65
+
66
+
67
+ class LocalProcessManager(SyncBaseService, ILocalProcessManager):
68
+ """
69
+ Manages local process lifecycle with isolation and state tracking.
70
+
71
+ WHY: Provides high-level process management operations that handle
72
+ all the complexity of spawning, tracking, and terminating background
73
+ processes reliably.
74
+
75
+ Thread Safety: Operations are thread-safe through state manager locking.
76
+ """
77
+
78
+ def __init__(self, state_manager: IDeploymentStateManager):
79
+ """
80
+ Initialize process manager.
81
+
82
+ Args:
83
+ state_manager: State manager for deployment persistence
84
+ """
85
+ super().__init__("LocalProcessManager")
86
+ self.state_manager = state_manager
87
+ self.is_windows = platform.system() == "Windows"
88
+
89
+ def initialize(self) -> bool:
90
+ """
91
+ Initialize the process manager.
92
+
93
+ Returns:
94
+ True if initialization successful
95
+ """
96
+ try:
97
+ # Ensure state manager is initialized
98
+ if not self.state_manager.is_initialized:
99
+ if not self.state_manager.initialize():
100
+ self.log_error("Failed to initialize state manager")
101
+ return False
102
+
103
+ self._initialized = True
104
+ self.log_info("Process manager initialized")
105
+ return True
106
+
107
+ except Exception as e:
108
+ self.log_error(f"Failed to initialize: {e}")
109
+ return False
110
+
111
+ def shutdown(self) -> None:
112
+ """Shutdown process manager (processes continue running)."""
113
+ self._shutdown = True
114
+ self.log_info("Process manager shutdown complete")
115
+
116
+ def start(self, config: StartConfig) -> DeploymentState:
117
+ """
118
+ Start a new background process.
119
+
120
+ WHY: Combines process spawning, port allocation, and state tracking
121
+ in a single atomic operation.
122
+
123
+ Args:
124
+ config: Configuration for the process to start
125
+
126
+ Returns:
127
+ DeploymentState with process information
128
+
129
+ Raises:
130
+ ProcessSpawnError: If process cannot be spawned
131
+ PortConflictError: If port unavailable and no alternative found
132
+ ValueError: If configuration is invalid
133
+ """
134
+ # Validate working directory exists
135
+ working_dir = Path(config.working_directory)
136
+ if not working_dir.exists():
137
+ raise ValueError(f"Working directory does not exist: {working_dir}")
138
+
139
+ # Handle port allocation if needed
140
+ allocated_port = None
141
+ if config.port is not None:
142
+ allocated_port = self._allocate_port(config.port, config.auto_find_port)
143
+
144
+ # Generate deployment ID if not provided
145
+ project_name = working_dir.name
146
+ deployment_id = config.deployment_id or self.generate_deployment_id(
147
+ project_name, allocated_port
148
+ )
149
+
150
+ # Prepare environment variables
151
+ env = os.environ.copy()
152
+ env.update(config.environment)
153
+ if allocated_port is not None:
154
+ env["PORT"] = str(allocated_port)
155
+
156
+ # Spawn the process
157
+ try:
158
+ self.log_info(
159
+ f"Spawning process for {deployment_id}: {' '.join(config.command)}"
160
+ )
161
+
162
+ # Platform-specific process group creation
163
+ if self.is_windows:
164
+ # Windows: use CREATE_NEW_PROCESS_GROUP
165
+ process = subprocess.Popen(
166
+ config.command,
167
+ cwd=str(working_dir),
168
+ env=env,
169
+ stdout=subprocess.PIPE,
170
+ stderr=subprocess.PIPE,
171
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
172
+ )
173
+ else:
174
+ # Unix: use start_new_session for process group isolation
175
+ process = subprocess.Popen(
176
+ config.command,
177
+ cwd=str(working_dir),
178
+ env=env,
179
+ stdout=subprocess.PIPE,
180
+ stderr=subprocess.PIPE,
181
+ start_new_session=True,
182
+ )
183
+
184
+ # Give process a moment to start
185
+ time.sleep(0.5)
186
+
187
+ # Check if process is still running
188
+ if process.poll() is not None:
189
+ # Process died immediately
190
+ stdout, stderr = process.communicate()
191
+ error_msg = stderr.decode("utf-8", errors="replace") if stderr else ""
192
+ raise ProcessSpawnError(
193
+ f"Process died immediately. Exit code: {process.returncode}. "
194
+ f"Error: {error_msg}"
195
+ )
196
+
197
+ # Create deployment state
198
+ deployment = DeploymentState(
199
+ deployment_id=deployment_id,
200
+ process_id=process.pid,
201
+ command=config.command,
202
+ working_directory=str(working_dir),
203
+ environment=config.environment,
204
+ port=allocated_port,
205
+ started_at=datetime.now(tz=timezone.utc),
206
+ status=ServiceState.RUNNING,
207
+ metadata=config.metadata,
208
+ )
209
+
210
+ # Save to state
211
+ self.state_manager.add_deployment(deployment)
212
+
213
+ self.log_info(
214
+ f"Started process {process.pid} for {deployment_id} "
215
+ f"on port {allocated_port or 'N/A'}"
216
+ )
217
+
218
+ return deployment
219
+
220
+ except subprocess.SubprocessError as e:
221
+ raise ProcessSpawnError(f"Failed to spawn process: {e}") from e
222
+
223
+ def stop(self, deployment_id: str, timeout: int = 10, force: bool = False) -> bool:
224
+ """
225
+ Stop a running process.
226
+
227
+ WHY: Provides graceful shutdown with configurable timeout and
228
+ force kill fallback for stuck processes.
229
+
230
+ Args:
231
+ deployment_id: Unique deployment identifier
232
+ timeout: Seconds to wait for graceful shutdown
233
+ force: If True, kill immediately without waiting
234
+
235
+ Returns:
236
+ True if process stopped successfully
237
+
238
+ Raises:
239
+ ValueError: If deployment_id not found
240
+ """
241
+ deployment = self.state_manager.get_deployment(deployment_id)
242
+ if not deployment:
243
+ raise ValueError(f"Deployment not found: {deployment_id}")
244
+
245
+ try:
246
+ process = psutil.Process(deployment.process_id)
247
+ except psutil.NoSuchProcess:
248
+ # Process already dead, just update state
249
+ self.log_info(f"Process {deployment.process_id} already dead")
250
+ self.state_manager.update_deployment_status(
251
+ deployment_id, ServiceState.STOPPED
252
+ )
253
+ return True
254
+
255
+ self.log_info(f"Stopping process {deployment.process_id} for {deployment_id}")
256
+ self.state_manager.update_deployment_status(
257
+ deployment_id, ServiceState.STOPPING
258
+ )
259
+
260
+ try:
261
+ if force:
262
+ # Force kill immediately
263
+ self._kill_process_group(process)
264
+ self.state_manager.update_deployment_status(
265
+ deployment_id, ServiceState.STOPPED
266
+ )
267
+ return True
268
+
269
+ # Try graceful shutdown first
270
+ self._terminate_process_group(process)
271
+
272
+ # Wait for process to die
273
+ start_time = time.time()
274
+ while time.time() - start_time < timeout:
275
+ if not process.is_running():
276
+ self.log_info(f"Process {deployment.process_id} stopped gracefully")
277
+ self.state_manager.update_deployment_status(
278
+ deployment_id, ServiceState.STOPPED
279
+ )
280
+ return True
281
+ time.sleep(0.1)
282
+
283
+ # Timeout exceeded, force kill
284
+ self.log_warning(
285
+ f"Graceful shutdown timeout, force killing {deployment.process_id}"
286
+ )
287
+ self._kill_process_group(process)
288
+ self.state_manager.update_deployment_status(
289
+ deployment_id, ServiceState.STOPPED
290
+ )
291
+ return True
292
+
293
+ except psutil.NoSuchProcess:
294
+ # Process died during shutdown
295
+ self.state_manager.update_deployment_status(
296
+ deployment_id, ServiceState.STOPPED
297
+ )
298
+ return True
299
+
300
+ except Exception as e:
301
+ self.log_error(f"Error stopping process: {e}")
302
+ return False
303
+
304
+ def restart(self, deployment_id: str, timeout: int = 10) -> DeploymentState:
305
+ """
306
+ Restart a process (stop then start with same config).
307
+
308
+ Args:
309
+ deployment_id: Unique deployment identifier
310
+ timeout: Seconds to wait for graceful shutdown
311
+
312
+ Returns:
313
+ New DeploymentState after restart
314
+
315
+ Raises:
316
+ ValueError: If deployment_id not found
317
+ ProcessSpawnError: If restart fails
318
+ """
319
+ # Get existing deployment config
320
+ deployment = self.state_manager.get_deployment(deployment_id)
321
+ if not deployment:
322
+ raise ValueError(f"Deployment not found: {deployment_id}")
323
+
324
+ # Stop the process
325
+ self.stop(deployment_id, timeout=timeout)
326
+
327
+ # Create new start config from existing deployment
328
+ config = StartConfig(
329
+ command=deployment.command,
330
+ working_directory=deployment.working_directory,
331
+ environment=deployment.environment,
332
+ port=deployment.port,
333
+ auto_find_port=True,
334
+ metadata=deployment.metadata,
335
+ )
336
+
337
+ # Remove old deployment from state
338
+ self.state_manager.remove_deployment(deployment_id)
339
+
340
+ # Start new process
341
+ return self.start(config)
342
+
343
+ def get_status(self, deployment_id: str) -> Optional[ProcessInfo]:
344
+ """
345
+ Get current status and runtime information for a process.
346
+
347
+ Args:
348
+ deployment_id: Unique deployment identifier
349
+
350
+ Returns:
351
+ ProcessInfo with current status, or None if not found
352
+ """
353
+ deployment = self.state_manager.get_deployment(deployment_id)
354
+ if not deployment:
355
+ return None
356
+
357
+ try:
358
+ process = psutil.Process(deployment.process_id)
359
+
360
+ # Calculate uptime
361
+ create_time = process.create_time()
362
+ uptime = time.time() - create_time
363
+
364
+ # Get memory usage
365
+ memory_info = process.memory_info()
366
+ memory_mb = memory_info.rss / (1024 * 1024)
367
+
368
+ # Get CPU usage
369
+ cpu_percent = process.cpu_percent(interval=0.1)
370
+
371
+ # Determine status
372
+ if process.is_running():
373
+ status = ServiceState.RUNNING
374
+ else:
375
+ status = ServiceState.STOPPED
376
+
377
+ return ProcessInfo(
378
+ deployment_id=deployment_id,
379
+ process_id=deployment.process_id,
380
+ status=status,
381
+ port=deployment.port,
382
+ uptime_seconds=uptime,
383
+ memory_mb=memory_mb,
384
+ cpu_percent=cpu_percent,
385
+ is_responding=True, # TODO: Add actual health check
386
+ )
387
+
388
+ except psutil.NoSuchProcess:
389
+ return ProcessInfo(
390
+ deployment_id=deployment_id,
391
+ process_id=deployment.process_id,
392
+ status=ServiceState.ERROR, # CRASHED semantically maps to ERROR state
393
+ port=deployment.port,
394
+ error_message="Process no longer exists",
395
+ )
396
+
397
+ def list_processes(
398
+ self, status_filter: Optional[ServiceState] = None
399
+ ) -> List[ProcessInfo]:
400
+ """
401
+ List all managed processes.
402
+
403
+ Args:
404
+ status_filter: Optional status to filter by
405
+
406
+ Returns:
407
+ List of ProcessInfo for all matching processes
408
+ """
409
+ if status_filter:
410
+ deployments = self.state_manager.get_deployments_by_status(status_filter)
411
+ else:
412
+ deployments = self.state_manager.get_all_deployments()
413
+
414
+ process_infos = []
415
+ for deployment in deployments:
416
+ info = self.get_status(deployment.deployment_id)
417
+ if info:
418
+ process_infos.append(info)
419
+
420
+ return process_infos
421
+
422
+ def is_port_available(self, port: int) -> bool:
423
+ """
424
+ Check if a port is available for use.
425
+
426
+ WHY: Port conflict prevention is critical for reliable deployments.
427
+
428
+ Args:
429
+ port: Port number to check
430
+
431
+ Returns:
432
+ True if port is available
433
+ """
434
+ # Check if port is protected
435
+ if is_port_protected(port):
436
+ return False
437
+
438
+ # Check if port is in use
439
+ connections = psutil.net_connections()
440
+ return all(conn.laddr.port != port for conn in connections)
441
+
442
+ def find_available_port(
443
+ self, preferred_port: int, max_attempts: int = 10
444
+ ) -> Optional[int]:
445
+ """
446
+ Find an available port starting from preferred_port.
447
+
448
+ WHY: Uses linear probing to find alternative ports when preferred
449
+ port is unavailable. Respects protected port ranges.
450
+
451
+ Args:
452
+ preferred_port: Starting port number
453
+ max_attempts: Maximum number of ports to try
454
+
455
+ Returns:
456
+ Available port number, or None if none found
457
+ """
458
+ for offset in range(max_attempts):
459
+ candidate_port = preferred_port + offset
460
+
461
+ # Skip ports outside valid range
462
+ if candidate_port > 65535:
463
+ break
464
+
465
+ # Check if port is available
466
+ if self.is_port_available(candidate_port):
467
+ if offset > 0:
468
+ self.log_info(
469
+ f"Port {preferred_port} unavailable, using {candidate_port}"
470
+ )
471
+ return candidate_port
472
+
473
+ return None
474
+
475
+ def cleanup_orphans(self) -> int:
476
+ """
477
+ Clean up orphaned process state entries.
478
+
479
+ WHY: Processes may crash or be killed externally, leaving stale state.
480
+
481
+ Returns:
482
+ Number of orphaned entries cleaned up
483
+ """
484
+ return self.state_manager.cleanup_dead_pids()
485
+
486
+ def generate_deployment_id(
487
+ self, project_name: str, port: Optional[int] = None
488
+ ) -> str:
489
+ """
490
+ Generate a unique deployment ID.
491
+
492
+ WHY: Provides consistent ID generation with optional port suffix.
493
+
494
+ Args:
495
+ project_name: Name of the project
496
+ port: Optional port number to include in ID
497
+
498
+ Returns:
499
+ Unique deployment identifier
500
+ """
501
+ # Use timestamp for uniqueness
502
+ timestamp = datetime.now(tz=timezone.utc).strftime("%Y%m%d-%H%M%S")
503
+
504
+ # Generate short hash from project name for readability
505
+ name_hash = sha256(project_name.encode()).hexdigest()[:8]
506
+
507
+ if port:
508
+ return f"{project_name}_{name_hash}_{timestamp}_p{port}"
509
+ return f"{project_name}_{name_hash}_{timestamp}"
510
+
511
+ def _allocate_port(self, preferred_port: int, auto_find: bool) -> int:
512
+ """
513
+ Allocate a port for the deployment.
514
+
515
+ Args:
516
+ preferred_port: Preferred port number
517
+ auto_find: If True, find alternative if preferred unavailable
518
+
519
+ Returns:
520
+ Allocated port number
521
+
522
+ Raises:
523
+ PortConflictError: If port unavailable and auto_find is False
524
+ """
525
+ # Check if preferred port is available
526
+ if self.is_port_available(preferred_port):
527
+ return preferred_port
528
+
529
+ # If auto_find disabled, raise error
530
+ if not auto_find:
531
+ raise PortConflictError(
532
+ f"Port {preferred_port} is unavailable and auto_find_port is disabled"
533
+ )
534
+
535
+ # Find alternative port
536
+ alternative = self.find_available_port(preferred_port)
537
+ if alternative is None:
538
+ raise PortConflictError(
539
+ f"No available ports found starting from {preferred_port}"
540
+ )
541
+
542
+ return alternative
543
+
544
+ def _terminate_process_group(self, process: psutil.Process) -> None:
545
+ """
546
+ Send SIGTERM to process group for graceful shutdown.
547
+
548
+ Args:
549
+ process: Process to terminate
550
+ """
551
+ if self.is_windows:
552
+ # Windows: terminate the process tree
553
+ try:
554
+ parent = process
555
+ children = parent.children(recursive=True)
556
+ for child in children:
557
+ child.terminate()
558
+ parent.terminate()
559
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
560
+ pass
561
+ else:
562
+ # Unix: send SIGTERM to process group
563
+ try:
564
+ os.killpg(os.getpgid(process.pid), signal.SIGTERM)
565
+ except (ProcessLookupError, PermissionError):
566
+ # Fallback to single process
567
+ process.terminate()
568
+
569
+ def _kill_process_group(self, process: psutil.Process) -> None:
570
+ """
571
+ Send SIGKILL to process group for force termination.
572
+
573
+ Args:
574
+ process: Process to kill
575
+ """
576
+ if self.is_windows:
577
+ # Windows: kill the process tree
578
+ try:
579
+ parent = process
580
+ children = parent.children(recursive=True)
581
+ for child in children:
582
+ child.kill()
583
+ parent.kill()
584
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
585
+ pass
586
+ else:
587
+ # Unix: send SIGKILL to process group
588
+ try:
589
+ os.killpg(os.getpgid(process.pid), signal.SIGKILL)
590
+ except (ProcessLookupError, PermissionError):
591
+ # Fallback to single process
592
+ process.kill()
593
+
594
+
595
+ __all__ = ["LocalProcessManager", "PortConflictError", "ProcessSpawnError"]