mecha10_cli/dev/node_selection.rs
1//! Node selection logic for development mode
2
3use crate::services::ProcessService;
4use crate::types::{NodeSpec, ProjectConfig};
5
6/// Information about a node to run
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct NodeToRun {
9 /// Node name (short name, e.g., "speaker")
10 pub name: String,
11 /// Full node identifier (e.g., "@mecha10/speaker")
12 pub identifier: String,
13 /// Path to the node package (e.g., "mecha10-nodes-motor-fake" or "nodes/custom")
14 pub path: String,
15 /// Whether this is a framework node (@mecha10/*)
16 pub is_framework_node: bool,
17}
18
19impl NodeToRun {
20 /// Create a new NodeToRun from NodeSpec
21 pub fn from_spec(spec: &NodeSpec) -> Self {
22 Self {
23 name: spec.name.clone(),
24 identifier: spec.identifier.clone(),
25 path: spec.package_path(),
26 is_framework_node: spec.is_framework(),
27 }
28 }
29
30 /// Get the binary path for this node
31 ///
32 /// Uses smart resolution to find the best binary location:
33 /// - Framework dev mode: local build in target/
34 /// - Production mode: globally installed binary (if available)
35 /// - Fallback: local build
36 ///
37 /// Note: This method is no longer used by CLI (replaced by node-runner in Phase 2).
38 /// Kept for testing purposes.
39 #[allow(dead_code)]
40 pub fn binary_path(&self, project_name: &str) -> String {
41 ProcessService::resolve_node_binary(&self.name, self.is_framework_node, project_name)
42 }
43
44 /// Get command args for running this node
45 ///
46 /// Returns args based on whether we're using:
47 /// - Global binary: no args (binary IS the node)
48 /// - Local project build: ["node", "node_name"] subcommand
49 ///
50 /// Note: This method is no longer used by CLI (replaced by node-runner in Phase 2).
51 /// Kept for testing purposes.
52 #[allow(dead_code)]
53 pub fn args(&self, project_name: &str) -> Vec<String> {
54 if self.is_framework_node {
55 // Check if we're using a global binary
56 let binary_path = self.binary_path(project_name);
57
58 // If the binary path doesn't start with "target/", it's a global binary
59 // Global binaries don't need the "node" subcommand
60 if !binary_path.starts_with("target/") {
61 vec![]
62 } else {
63 // Local build uses "node" subcommand
64 vec!["node".to_string(), self.name.clone()]
65 }
66 } else {
67 vec![]
68 }
69 }
70}
71
72/// Get list of nodes to run based on configuration
73///
74/// # Arguments
75///
76/// * `requested_nodes` - Specific nodes requested by user (empty = all nodes)
77/// * `project_config` - Project configuration
78///
79/// # Returns
80///
81/// List of nodes to run
82pub fn get_nodes_to_run(requested_nodes: &[String], project_config: &ProjectConfig) -> Vec<NodeToRun> {
83 // Get all node specs from config (works with both old and new formats)
84 let all_specs = project_config.nodes.get_node_specs();
85
86 // If specific nodes requested, filter to those
87 let specs_to_run: Vec<NodeSpec> = if requested_nodes.is_empty() {
88 // Use all nodes
89 all_specs
90 } else {
91 // Use only requested nodes
92 all_specs
93 .into_iter()
94 .filter(|s| requested_nodes.contains(&s.name))
95 .collect()
96 };
97
98 // Convert to NodeToRun
99 specs_to_run.iter().map(NodeToRun::from_spec).collect()
100}
101
102/// Get list of all available node names
103pub fn get_available_node_names(project_config: &ProjectConfig) -> Vec<String> {
104 project_config.nodes.get_node_names()
105}
106
107/// Get list of enabled node names
108///
109/// Note: In the new format, all nodes in the list are considered "enabled".
110/// Use lifecycle modes to control which nodes run in different contexts.
111#[allow(dead_code)] // Tested, planned for future use
112pub fn get_enabled_node_names(project_config: &ProjectConfig) -> Vec<String> {
113 // In the new format, all nodes are enabled by default
114 // Lifecycle modes control which nodes run in which context
115 project_config.nodes.get_node_names()
116}
117
118/// Find a node spec by name
119#[allow(dead_code)] // API for future use
120pub fn find_node_by_name(project_config: &ProjectConfig, name: &str) -> Option<NodeSpec> {
121 project_config.nodes.find_by_name(name)
122}
123
124/// Check if a node is a framework node by name
125#[allow(dead_code)] // API for future use
126pub fn is_framework_node(project_config: &ProjectConfig, name: &str) -> bool {
127 project_config
128 .nodes
129 .find_by_name(name)
130 .map(|s| s.is_framework())
131 .unwrap_or(false)
132}
133
134/// Check if a node is a project node by name
135#[allow(dead_code)] // API for future use
136pub fn is_project_node(project_config: &ProjectConfig, name: &str) -> bool {
137 project_config
138 .nodes
139 .find_by_name(name)
140 .map(|s| s.is_project())
141 .unwrap_or(false)
142}