mi6_cli/commands/
session.rs

1use anyhow::{Context, Result};
2use serde_json::{Map, Value};
3use std::collections::HashSet;
4
5use mi6_core::{Session, Storage};
6
7/// Run the session command to display session details.
8pub fn run_session<S: Storage>(
9    storage: &S,
10    session_or_pid: String,
11    machine_id: Option<String>,
12    json: bool,
13    fields: Option<Vec<String>>,
14) -> Result<()> {
15    // Check if input is a PID (numeric) or session_id
16    let session = if let Ok(pid) = session_or_pid.parse::<i32>() {
17        // Look up session by PID using the sessions table
18        storage
19            .get_session_by_pid(pid)
20            .context("failed to query session by PID")?
21            .ok_or_else(|| anyhow::anyhow!("no session found for PID: {}", pid))?
22    } else if let Some(ref machine) = machine_id {
23        // Use composite key lookup when machine_id is provided
24        storage
25            .get_session_by_key(machine, &session_or_pid)
26            .context("failed to get session by key")?
27            .ok_or_else(|| {
28                anyhow::anyhow!(
29                    "no session found: {} on machine {}",
30                    session_or_pid,
31                    machine
32                )
33            })?
34    } else {
35        // Fall back to session_id only lookup
36        storage
37            .get_session(&session_or_pid)
38            .context("failed to get session")?
39            .ok_or_else(|| anyhow::anyhow!("no session found: {}", session_or_pid))?
40    };
41
42    // Convert session to JSON Value for field access
43    let session_json = session_to_json(&session)?;
44
45    // Filter fields if specified
46    let output = if let Some(ref field_list) = fields {
47        filter_fields(&session_json, field_list)
48    } else {
49        session_json
50    };
51
52    if json {
53        print_json(&output);
54    } else {
55        print_list(&output);
56    }
57
58    Ok(())
59}
60
61/// Convert Session to a JSON Value with all fields plus computed fields.
62fn session_to_json(session: &Session) -> Result<Value> {
63    // Serialize the session directly (uses derive(Serialize))
64    let mut map: Map<String, Value> = serde_json::from_value(serde_json::to_value(session)?)?;
65
66    // Add computed fields
67    map.insert(
68        "total_tokens".to_string(),
69        Value::Number(session.total_tokens().into()),
70    );
71    map.insert(
72        "duration_ms".to_string(),
73        Value::Number(session.duration().num_milliseconds().into()),
74    );
75    map.insert("is_active".to_string(), Value::Bool(session.is_active()));
76    map.insert(
77        "is_process_running".to_string(),
78        Value::Bool(session.is_process_running()),
79    );
80    map.insert(
81        "metrics_source".to_string(),
82        Value::String(session.metrics_source().to_string()),
83    );
84
85    Ok(Value::Object(map))
86}
87
88/// Filter a JSON object to only include specified fields.
89fn filter_fields(value: &Value, fields: &[String]) -> Value {
90    let Some(obj) = value.as_object() else {
91        return value.clone();
92    };
93
94    let field_set: HashSet<&str> = fields.iter().map(|s| s.as_str()).collect();
95
96    let filtered: Map<String, Value> = obj
97        .iter()
98        .filter(|(k, _)| field_set.contains(k.as_str()))
99        .map(|(k, v)| (k.clone(), v.clone()))
100        .collect();
101
102    Value::Object(filtered)
103}
104
105/// Print JSON output.
106fn print_json(value: &Value) {
107    if let Ok(json) = serde_json::to_string_pretty(value) {
108        println!("{}", json);
109    }
110}
111
112/// Print human-readable key-value list.
113fn print_list(value: &Value) {
114    let Some(obj) = value.as_object() else {
115        return;
116    };
117
118    // Collect and sort keys for consistent output
119    let mut keys: Vec<&String> = obj.keys().collect();
120    keys.sort();
121
122    for key in keys {
123        if let Some(val) = obj.get(key) {
124            println!("- {}: {}", key, format_value(val));
125        }
126    }
127}
128
129/// Format a JSON value for human-readable display.
130fn format_value(value: &Value) -> String {
131    match value {
132        Value::Null => "null".to_string(),
133        Value::Bool(b) => b.to_string(),
134        Value::Number(n) => n.to_string(),
135        Value::String(s) => s.clone(),
136        Value::Array(arr) => {
137            let items: Vec<String> = arr.iter().map(format_value).collect();
138            format!("[{}]", items.join(", "))
139        }
140        Value::Object(_) => "[object]".to_string(),
141    }
142}