Skip to main content

virtuoso_cli/commands/
session.rs

1use crate::config::Config;
2use crate::error::{Result, VirtuosoError};
3use crate::models::SessionInfo;
4use crate::output::OutputFormat;
5use crate::transport::tunnel::SSHClient;
6use serde_json::{json, Value};
7
8pub fn list(format: OutputFormat) -> Result<Value> {
9    // In remote mode, sync session files from remote host first.
10    // Best effort: failures are silent so local cache still works.
11    if let Ok(cfg) = Config::from_env() {
12        if cfg.is_remote() {
13            if let Ok(client) = SSHClient::from_env(cfg.keep_remote_files) {
14                let _ = SessionInfo::sync_from_remote(&client.runner);
15            }
16        }
17    }
18
19    let mut sessions = SessionInfo::list()
20        .map_err(|e| VirtuosoError::Execution(format!("failed to read sessions: {e}")))?;
21
22    let sessions_dir = SessionInfo::sessions_dir();
23    sessions.retain(|s| {
24        if s.is_alive() {
25            true
26        } else {
27            let _ = std::fs::remove_file(sessions_dir.join(format!("{}.json", s.id)));
28            false
29        }
30    });
31
32    if format == OutputFormat::Json {
33        return Ok(json!({
34            "status": "success",
35            "count": sessions.len(),
36            "sessions": sessions.iter().map(|s| json!({
37                "id": s.id,
38                "port": s.port,
39                "pid": s.pid,
40                "host": s.host,
41                "user": s.user,
42                "created": s.created,
43            })).collect::<Vec<_>>(),
44        }));
45    }
46
47    if sessions.is_empty() {
48        println!("No active Virtuoso sessions found.");
49        println!("Start Virtuoso and run RBStart() in CIW to register a session.");
50        return Ok(json!({"status": "success", "count": 0}));
51    }
52
53    println!(
54        "{:<20} {:>6}  {:>7}  {:<12}  CREATED",
55        "SESSION ID", "PORT", "PID", "HOST"
56    );
57    println!("{}", "-".repeat(72));
58    for s in &sessions {
59        println!(
60            "{:<20} {:>6}  {:>7}  {:<12}  {}",
61            s.id, s.port, s.pid, s.host, s.created
62        );
63    }
64
65    Ok(json!({"status": "success", "count": sessions.len()}))
66}
67
68pub fn current() -> Result<Value> {
69    let live = SessionInfo::list_alive();
70    match live.len() {
71        0 => Ok(json!({"status": "success", "session": null, "note": "no live sessions; VB_PORT will be used"})),
72        1 => Ok(json!({
73            "status": "success",
74            "session": live[0].id,
75            "port": live[0].port,
76            "auto_selected": true,
77        })),
78        _ => {
79            let ids: Vec<&str> = live.iter().map(|s| s.id.as_str()).collect();
80            Ok(json!({
81                "status": "ambiguous",
82                "sessions": ids,
83                "note": "use --session <id> to select one",
84            }))
85        }
86    }
87}
88
89pub fn cleanup() -> Result<Value> {
90    let all = SessionInfo::list().unwrap_or_default();
91    let dir = SessionInfo::sessions_dir();
92    let mut removed = Vec::new();
93    for s in &all {
94        if !s.is_alive() {
95            let path = dir.join(format!("{}.json", s.id));
96            if std::fs::remove_file(&path).is_ok() {
97                removed.push(s.id.clone());
98            }
99        }
100    }
101    Ok(json!({
102        "status": "success",
103        "removed": removed.len(),
104        "sessions": removed,
105    }))
106}
107
108pub fn history(id: &str, only_skill: bool, only_cmd: bool, limit: usize) -> Result<Value> {
109    let show_skill = !only_cmd;
110    let show_cmd = !only_skill;
111
112    let skill_entries: Vec<Value> = if show_skill {
113        crate::history::load_skill(id, limit)
114            .into_iter()
115            .map(|e| serde_json::json!({"type":"skill","ts":e.ts,"ok":e.ok,"skill":e.skill,"output":e.output}))
116            .collect()
117    } else {
118        vec![]
119    };
120
121    let cmd_entries: Vec<Value> = if show_cmd {
122        crate::history::load_cmd(Some(id), limit)
123            .into_iter()
124            .map(|e| serde_json::json!({"type":"cmd","ts":e.ts,"cmd":e.cmd,"exit_code":e.exit_code}))
125            .collect()
126    } else {
127        vec![]
128    };
129
130    Ok(json!({
131        "status": "success",
132        "session": id,
133        "skill_count": skill_entries.len(),
134        "cmd_count": cmd_entries.len(),
135        "skill": skill_entries,
136        "cmd": cmd_entries,
137    }))
138}
139
140pub fn show(id: &str, _format: OutputFormat) -> Result<Value> {
141    let s = SessionInfo::load(id)
142        .map_err(|e| VirtuosoError::NotFound(format!("session '{id}' not found: {e}")))?;
143
144    Ok(json!({
145        "status": "success",
146        "session": {
147            "id": s.id,
148            "port": s.port,
149            "pid": s.pid,
150            "host": s.host,
151            "user": s.user,
152            "created": s.created,
153            "alive": s.is_alive(),
154        }
155    }))
156}