mecha10_cli/dev/lifecycle_adapter.rs
1//! Lifecycle adapter for CLI process management
2//!
3//! This module adapts the runtime lifecycle system for the CLI's process-based
4//! node management. While the runtime manages nodes in-process, the CLI spawns
5//! nodes as separate processes via node-runner.
6//!
7//! This adapter:
8//! - Reads lifecycle config from mecha10.json
9//! - Determines which nodes to spawn based on current mode
10//! - Provides mode change logic without runtime dependency
11
12use crate::types::project::{LifecycleConfig, ModeConfig, ProjectConfig};
13use anyhow::Result;
14use std::collections::{HashMap, HashSet};
15
16/// Lifecycle mode manager for CLI
17///
18/// Manages mode transitions and determines which nodes should run in each mode.
19/// This is a lightweight adapter that doesn't depend on the full runtime.
20pub struct CliLifecycleManager {
21 /// Current operational mode
22 current_mode: String,
23
24 /// Set of currently running node names
25 running_nodes: HashSet<String>,
26
27 /// Mode configurations from mecha10.json
28 mode_config: HashMap<String, ModeConfig>,
29}
30
31/// Result of calculating mode transition
32#[derive(Debug)]
33#[allow(dead_code)] // Will be used for mode change commands
34pub struct ModeTransitionDiff {
35 /// Nodes that need to be started
36 pub start: Vec<String>,
37
38 /// Nodes that need to be stopped
39 pub stop: Vec<String>,
40}
41
42impl CliLifecycleManager {
43 /// Create a new CLI lifecycle manager from project config
44 ///
45 /// Returns None if project doesn't have lifecycle configuration
46 pub fn from_project_config(config: &ProjectConfig) -> Option<Self> {
47 let lifecycle = config.lifecycle.as_ref()?;
48
49 Some(Self {
50 current_mode: lifecycle.default_mode.clone(),
51 running_nodes: HashSet::new(),
52 mode_config: lifecycle.modes.clone(),
53 })
54 }
55
56 /// Get the current mode
57 pub fn current_mode(&self) -> &str {
58 &self.current_mode
59 }
60
61 /// Check if a mode exists in the configuration
62 pub fn has_mode(&self, mode: &str) -> bool {
63 self.mode_config.contains_key(mode)
64 }
65
66 /// Get list of nodes that should run in the current mode
67 pub fn nodes_for_current_mode(&self) -> Vec<String> {
68 self.mode_config
69 .get(&self.current_mode)
70 .map(|config| config.nodes.clone())
71 .unwrap_or_default()
72 }
73
74 /// Calculate what changes are needed to transition to target mode
75 ///
76 /// Returns the diff (nodes to start/stop) without changing state.
77 #[allow(dead_code)] // Will be used for mode change commands
78 pub fn calculate_mode_diff(&self, target_mode: &str) -> Result<ModeTransitionDiff> {
79 // Validate mode exists
80 let target_config = self
81 .mode_config
82 .get(target_mode)
83 .ok_or_else(|| anyhow::anyhow!("Mode '{}' not found", target_mode))?;
84
85 // Nodes that should run in target mode
86 let target_nodes: HashSet<_> = target_config.nodes.iter().map(|s| s.as_str()).collect();
87
88 // Nodes to start: in target but not currently running
89 let start: Vec<_> = target_nodes
90 .iter()
91 .filter(|n| !self.running_nodes.contains(**n))
92 .map(|s| s.to_string())
93 .collect();
94
95 // Nodes to stop: running but not in target
96 let stop: Vec<_> = self
97 .running_nodes
98 .iter()
99 .filter(|n| !target_nodes.contains(n.as_str()))
100 .cloned()
101 .collect();
102
103 Ok(ModeTransitionDiff { start, stop })
104 }
105
106 /// Mark nodes as running (after spawning them)
107 pub fn mark_nodes_running(&mut self, nodes: &[String]) {
108 for node in nodes {
109 self.running_nodes.insert(node.clone());
110 }
111 }
112
113 /// Mark nodes as stopped (after killing them)
114 #[allow(dead_code)] // Will be used for mode change commands
115 pub fn mark_nodes_stopped(&mut self, nodes: &[String]) {
116 for node in nodes {
117 self.running_nodes.remove(node);
118 }
119 }
120
121 /// Change to a new mode (updates internal state only)
122 ///
123 /// Returns the diff of what needs to change. Caller is responsible
124 /// for actually spawning/killing processes.
125 #[allow(dead_code)] // Will be used for mode change commands
126 pub fn change_mode(&mut self, target_mode: &str) -> Result<ModeTransitionDiff> {
127 let diff = self.calculate_mode_diff(target_mode)?;
128 self.current_mode = target_mode.to_string();
129 Ok(diff)
130 }
131
132 /// Get available modes
133 #[allow(dead_code)] // Will be used for mode change commands
134 pub fn available_modes(&self) -> Vec<&str> {
135 self.mode_config.keys().map(|s| s.as_str()).collect()
136 }
137
138 /// Validate lifecycle configuration
139 ///
140 /// Checks that:
141 /// - All node references exist in project config
142 /// - Default mode exists
143 #[allow(dead_code)] // Will be used for validation on project init
144 pub fn validate(lifecycle: &LifecycleConfig, available_nodes: &[String]) -> Result<()> {
145 // Check default mode exists
146 if !lifecycle.modes.contains_key(&lifecycle.default_mode) {
147 return Err(anyhow::anyhow!(
148 "Default mode '{}' not found in modes",
149 lifecycle.default_mode
150 ));
151 }
152
153 // Check all node references are valid
154 let node_set: HashSet<_> = available_nodes.iter().map(|s| s.as_str()).collect();
155
156 for (mode_name, mode_config) in &lifecycle.modes {
157 for node in &mode_config.nodes {
158 if !node_set.contains(node.as_str()) {
159 return Err(anyhow::anyhow!(
160 "Mode '{}' references unknown node '{}'",
161 mode_name,
162 node
163 ));
164 }
165 }
166 }
167
168 Ok(())
169 }
170}