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 let binary_path = self.binary_path(project_name);
56
57 if !binary_path.starts_with("target/") {
58 vec![]
59 } else {
60 vec!["node".to_string(), self.name.clone()]
61 }
62 } else {
63 vec![]
64 }
65 }
66}
67
68/// Get list of nodes to run based on configuration
69///
70/// # Arguments
71///
72/// * `requested_nodes` - Specific nodes requested by user (empty = all nodes)
73/// * `project_config` - Project configuration
74///
75/// # Returns
76///
77/// List of nodes to run
78pub fn get_nodes_to_run(requested_nodes: &[String], project_config: &ProjectConfig) -> Vec<NodeToRun> {
79 let all_specs = project_config.nodes.get_node_specs();
80
81 let specs_to_run: Vec<NodeSpec> = if requested_nodes.is_empty() {
82 all_specs
83 } else {
84 all_specs
85 .into_iter()
86 .filter(|s| requested_nodes.contains(&s.name))
87 .collect()
88 };
89
90 specs_to_run.iter().map(NodeToRun::from_spec).collect()
91}
92
93/// Get list of all available node names
94pub fn get_available_node_names(project_config: &ProjectConfig) -> Vec<String> {
95 project_config.nodes.get_node_names()
96}
97
98/// Get list of enabled node names
99///
100/// Note: In the new format, all nodes in the list are considered "enabled".
101/// Use lifecycle modes to control which nodes run in different contexts.
102#[allow(dead_code)]
103pub fn get_enabled_node_names(project_config: &ProjectConfig) -> Vec<String> {
104 project_config.nodes.get_node_names()
105}
106
107/// Check if a node is a framework node by name
108#[allow(dead_code)]
109pub fn is_framework_node(project_config: &ProjectConfig, name: &str) -> bool {
110 project_config
111 .nodes
112 .find_by_name(name)
113 .map(|s| s.is_framework())
114 .unwrap_or(false)
115}