Skip to main content

mabi_cli/commands/
list.rs

1//! List command implementation.
2//!
3//! Lists devices, protocols, and other resources.
4
5use crate::context::CliContext;
6use crate::error::{CliError, CliResult};
7use crate::output::{DeviceSummary, OutputFormat, StatusType, TableBuilder};
8use crate::runner::{Command, CommandOutput};
9use async_trait::async_trait;
10use mabi_core::prelude::*;
11use serde::Serialize;
12
13/// Resource type to list.
14#[derive(Debug, Clone, Copy, Default)]
15pub enum ListResource {
16    #[default]
17    Devices,
18    Protocols,
19    Points,
20    Scenarios,
21}
22
23impl ListResource {
24    pub fn from_str(s: &str) -> Option<Self> {
25        match s.to_lowercase().as_str() {
26            "devices" | "device" | "d" => Some(Self::Devices),
27            "protocols" | "protocol" | "p" => Some(Self::Protocols),
28            "points" | "point" => Some(Self::Points),
29            "scenarios" | "scenario" | "s" => Some(Self::Scenarios),
30            _ => None,
31        }
32    }
33}
34
35/// List command for displaying resources.
36pub struct ListCommand {
37    /// Resource type to list.
38    resource: ListResource,
39    /// Filter by protocol.
40    protocol: Option<Protocol>,
41    /// Filter by device ID pattern.
42    device_filter: Option<String>,
43    /// Maximum number of items to show.
44    limit: Option<usize>,
45}
46
47impl ListCommand {
48    /// Create a new list command.
49    pub fn new(resource: ListResource) -> Self {
50        Self {
51            resource,
52            protocol: None,
53            device_filter: None,
54            limit: None,
55        }
56    }
57
58    /// Filter by protocol.
59    pub fn with_protocol(mut self, protocol: Protocol) -> Self {
60        self.protocol = Some(protocol);
61        self
62    }
63
64    /// Filter by device ID pattern.
65    pub fn with_device_filter(mut self, pattern: impl Into<String>) -> Self {
66        self.device_filter = Some(pattern.into());
67        self
68    }
69
70    /// Set the maximum number of items.
71    pub fn with_limit(mut self, limit: usize) -> Self {
72        self.limit = Some(limit);
73        self
74    }
75
76    /// List devices.
77    async fn list_devices(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
78        let output = ctx.output();
79
80        // Get devices from engine if available
81        let devices: Vec<DeviceSummary> = {
82            let engine_guard = ctx.engine_ref().read().await;
83            if let Some(engine) = engine_guard.as_ref() {
84                engine
85                    .list_devices()
86                    .into_iter()
87                    .filter(|info| {
88                        // Protocol filter
89                        if let Some(ref proto) = self.protocol {
90                            if info.protocol != *proto {
91                                return false;
92                            }
93                        }
94                        // Device ID filter
95                        if let Some(ref pattern) = self.device_filter {
96                            if !info.id.contains(pattern) {
97                                return false;
98                            }
99                        }
100                        true
101                    })
102                    .take(self.limit.unwrap_or(usize::MAX))
103                    .map(|info| {
104                        DeviceSummary {
105                            id: info.id.clone(),
106                            name: info.name.clone(),
107                            protocol: format!("{:?}", info.protocol),
108                            status: format!("{:?}", info.state),
109                            points: info.point_count,
110                            last_update: info.updated_at.to_rfc3339(),
111                        }
112                    })
113                    .collect()
114            } else {
115                Vec::new()
116            }
117        };
118
119        if matches!(output.format(), OutputFormat::Json | OutputFormat::Yaml) {
120            output.write(&devices)?;
121            return Ok(CommandOutput::quiet_success());
122        }
123
124        output.header("Devices");
125
126        if devices.is_empty() {
127            output.info("No devices found");
128            if self.protocol.is_some() || self.device_filter.is_some() {
129                output.kv("Tip", "Try removing filters to see all devices");
130            }
131        } else {
132            let mut table = TableBuilder::new(output.colors_enabled())
133                .header(["ID", "Name", "Protocol", "Points", "Status"]);
134
135            for device in &devices {
136                let status_type = match device.status.as_str() {
137                    "Online" => StatusType::Success,
138                    "Offline" | "Error" => StatusType::Error,
139                    _ => StatusType::Neutral,
140                };
141                table = table.status_row(
142                    [
143                        &device.id,
144                        &device.name,
145                        &device.protocol,
146                        &device.points.to_string(),
147                        &device.status,
148                    ],
149                    status_type,
150                );
151            }
152            table.print();
153            output.kv("Total", devices.len());
154        }
155
156        Ok(CommandOutput::quiet_success())
157    }
158
159    /// List supported protocols.
160    async fn list_protocols(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
161        let output = ctx.output();
162
163        let protocols = vec![
164            ProtocolInfo {
165                name: "Modbus TCP".into(),
166                protocol: "modbus_tcp".into(),
167                default_port: 502,
168                description: "Modbus TCP/IP protocol".into(),
169                features: vec!["Read/Write Coils", "Read/Write Registers", "Multi-unit support"],
170            },
171            ProtocolInfo {
172                name: "Modbus RTU".into(),
173                protocol: "modbus_rtu".into(),
174                default_port: 0,
175                description: "Modbus RTU serial protocol".into(),
176                features: vec!["Serial communication", "CRC validation", "Multi-device bus"],
177            },
178            ProtocolInfo {
179                name: "OPC UA".into(),
180                protocol: "opcua".into(),
181                default_port: 4840,
182                description: "OPC Unified Architecture".into(),
183                features: vec!["Subscriptions", "History", "Security", "Address space"],
184            },
185            ProtocolInfo {
186                name: "BACnet/IP".into(),
187                protocol: "bacnet".into(),
188                default_port: 47808,
189                description: "BACnet over IP".into(),
190                features: vec!["Read/Write Properties", "COV Subscriptions", "BBMD", "Device discovery"],
191            },
192            ProtocolInfo {
193                name: "KNXnet/IP".into(),
194                protocol: "knx".into(),
195                default_port: 3671,
196                description: "KNX over IP".into(),
197                features: vec!["Tunneling", "Group addressing", "DPT support"],
198            },
199        ];
200
201        if matches!(output.format(), OutputFormat::Json | OutputFormat::Yaml) {
202            output.write(&protocols)?;
203            return Ok(CommandOutput::quiet_success());
204        }
205
206        output.header("Supported Protocols");
207
208        let mut table = TableBuilder::new(output.colors_enabled())
209            .header(["Protocol", "Name", "Port", "Features"]);
210
211        for proto in &protocols {
212            table = table.row([
213                &proto.protocol,
214                &proto.name,
215                &proto.default_port.to_string(),
216                &proto.features.join(", "),
217            ]);
218        }
219        table.print();
220
221        Ok(CommandOutput::quiet_success())
222    }
223
224    /// List data points.
225    async fn list_points(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
226        let output = ctx.output();
227
228        // Get points from devices
229        // Note: This is a simplified implementation that lists device info
230        // Full point listing would require device handles with point_definitions()
231        let points: Vec<PointInfo> = {
232            let engine_guard = ctx.engine_ref().read().await;
233            if let Some(engine) = engine_guard.as_ref() {
234                engine
235                    .list_devices()
236                    .into_iter()
237                    .filter(|info| {
238                        if let Some(ref pattern) = self.device_filter {
239                            info.id.contains(pattern)
240                        } else {
241                            true
242                        }
243                    })
244                    .flat_map(|info| {
245                        // For now, create placeholder points based on point_count
246                        // In real implementation, we would use device handles to get actual point definitions
247                        (0..info.point_count).map(move |i| PointInfo {
248                            device_id: info.id.clone(),
249                            point_id: format!("point_{}", i),
250                            data_type: "Unknown".into(),
251                            access: "ReadWrite".into(),
252                            description: String::new(),
253                        })
254                    })
255                    .take(self.limit.unwrap_or(usize::MAX))
256                    .collect()
257            } else {
258                Vec::new()
259            }
260        };
261
262        if matches!(output.format(), OutputFormat::Json | OutputFormat::Yaml) {
263            output.write(&points)?;
264            return Ok(CommandOutput::quiet_success());
265        }
266
267        output.header("Data Points");
268
269        if points.is_empty() {
270            output.info("No data points found");
271        } else {
272            let mut table = TableBuilder::new(output.colors_enabled())
273                .header(["Device", "Point ID", "Type", "Access", "Description"]);
274
275            for point in &points {
276                table = table.row([
277                    &point.device_id,
278                    &point.point_id,
279                    &point.data_type,
280                    &point.access,
281                    &point.description,
282                ]);
283            }
284            table.print();
285            output.kv("Total", points.len());
286        }
287
288        Ok(CommandOutput::quiet_success())
289    }
290
291    /// List scenarios.
292    async fn list_scenarios(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
293        let output = ctx.output();
294        let working_dir = ctx.working_dir().clone();
295
296        // Search for scenario files
297        let scenarios_dir = working_dir.join("scenarios");
298        let mut scenarios = Vec::new();
299
300        if scenarios_dir.exists() {
301            let mut entries = tokio::fs::read_dir(&scenarios_dir).await?;
302            while let Some(entry) = entries.next_entry().await? {
303                let path = entry.path();
304                if path.extension().map_or(false, |e| e == "yaml" || e == "yml") {
305                    if let Ok(content) = tokio::fs::read_to_string(&path).await {
306                        if let Ok(scenario) = serde_yaml::from_str::<super::run::ScenarioConfig>(&content) {
307                            scenarios.push(ScenarioInfo {
308                                name: scenario.name,
309                                file: path.file_name().unwrap_or_default().to_string_lossy().into(),
310                                devices: scenario.devices.len(),
311                                events: scenario.events.len(),
312                                description: scenario.description,
313                            });
314                        }
315                    }
316                }
317
318                if let Some(limit) = self.limit {
319                    if scenarios.len() >= limit {
320                        break;
321                    }
322                }
323            }
324        }
325
326        if matches!(output.format(), OutputFormat::Json | OutputFormat::Yaml) {
327            output.write(&scenarios)?;
328            return Ok(CommandOutput::quiet_success());
329        }
330
331        output.header("Scenarios");
332
333        if scenarios.is_empty() {
334            output.info("No scenarios found");
335            output.kv("Search path", scenarios_dir.display());
336        } else {
337            let mut table = TableBuilder::new(output.colors_enabled())
338                .header(["Name", "File", "Devices", "Events"]);
339
340            for scenario in &scenarios {
341                table = table.row([
342                    &scenario.name,
343                    &scenario.file,
344                    &scenario.devices.to_string(),
345                    &scenario.events.to_string(),
346                ]);
347            }
348            table.print();
349            output.kv("Total", scenarios.len());
350        }
351
352        Ok(CommandOutput::quiet_success())
353    }
354}
355
356#[async_trait]
357impl Command for ListCommand {
358    fn name(&self) -> &str {
359        "list"
360    }
361
362    fn description(&self) -> &str {
363        "List devices, protocols, points, or scenarios"
364    }
365
366    async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput> {
367        match self.resource {
368            ListResource::Devices => self.list_devices(ctx).await,
369            ListResource::Protocols => self.list_protocols(ctx).await,
370            ListResource::Points => self.list_points(ctx).await,
371            ListResource::Scenarios => self.list_scenarios(ctx).await,
372        }
373    }
374}
375
376#[derive(Debug, Clone, Serialize)]
377struct ProtocolInfo {
378    name: String,
379    protocol: String,
380    default_port: u16,
381    description: String,
382    features: Vec<&'static str>,
383}
384
385#[derive(Debug, Clone, Serialize)]
386struct PointInfo {
387    device_id: String,
388    point_id: String,
389    data_type: String,
390    access: String,
391    description: String,
392}
393
394#[derive(Debug, Clone, Serialize)]
395struct ScenarioInfo {
396    name: String,
397    file: String,
398    devices: usize,
399    events: usize,
400    description: String,
401}