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}