Skip to main content

virtuoso_cli/
config.rs

1use crate::error::{Result, VirtuosoError};
2use dotenvy::dotenv;
3use std::env;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone)]
7pub struct Config {
8    pub remote_host: Option<String>,
9    pub remote_user: Option<String>,
10    pub port: u16,
11    pub jump_host: Option<String>,
12    pub jump_user: Option<String>,
13    pub timeout: u64,
14    pub keep_remote_files: bool,
15    pub spectre_cmd: String,
16    pub spectre_args: Vec<String>,
17}
18
19impl Config {
20    pub fn from_env() -> Result<Self> {
21        if let Err(e) = dotenv() {
22            if !e.not_found() {
23                tracing::warn!("failed to load .env: {e}");
24            }
25        }
26
27        let remote_host = env::var("VB_REMOTE_HOST").ok().filter(|s| !s.is_empty());
28
29        let port: u16 = env::var("VB_PORT")
30            .ok()
31            .and_then(|v| v.parse().ok())
32            .unwrap_or(65432);
33
34        if port == 0 {
35            return Err(VirtuosoError::Config(
36                "VB_PORT must be between 1 and 65535".into(),
37            ));
38        }
39
40        Ok(Self {
41            remote_host,
42            remote_user: env::var("VB_REMOTE_USER").ok(),
43            port,
44            jump_host: env::var("VB_JUMP_HOST").ok(),
45            jump_user: env::var("VB_JUMP_USER").ok(),
46            timeout: env::var("VB_TIMEOUT")
47                .ok()
48                .and_then(|v| v.parse().ok())
49                .unwrap_or(30),
50            keep_remote_files: env::var("VB_KEEP_REMOTE_FILES")
51                .ok()
52                .map(|v| v == "1" || v.to_lowercase() == "true")
53                .unwrap_or(false),
54            spectre_cmd: env::var("VB_SPECTRE_CMD")
55                .ok()
56                .unwrap_or_else(|| "spectre".into()),
57            spectre_args: env::var("VB_SPECTRE_ARGS")
58                .ok()
59                .map(|v| shlex::split(&v).unwrap_or_default())
60                .unwrap_or_default(),
61        })
62    }
63
64    pub fn is_remote(&self) -> bool {
65        self.remote_host.is_some()
66    }
67
68    pub fn ssh_target(&self) -> String {
69        let host = self.remote_host.as_deref().unwrap_or("");
70        match &self.remote_user {
71            Some(user) => format!("{user}@{host}"),
72            None => host.to_string(),
73        }
74    }
75
76    pub fn ssh_jump(&self) -> Option<String> {
77        match (&self.jump_host, &self.jump_user) {
78            (Some(host), Some(user)) => Some(format!("{user}@{host}")),
79            (Some(host), None) => Some(host.clone()),
80            _ => None,
81        }
82    }
83}
84
85pub fn find_project_root() -> Option<PathBuf> {
86    let mut current = std::env::current_dir().ok()?;
87    loop {
88        if current.join(".env").exists() {
89            return Some(current);
90        }
91        if current.join("pyproject.toml").exists() {
92            let content = std::fs::read_to_string(current.join("pyproject.toml")).ok()?;
93            if content.contains("virtuoso-bridge") || content.contains("virtuoso-cli") {
94                return Some(current);
95            }
96        }
97        if !current.pop() {
98            break;
99        }
100    }
101    None
102}