1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::{ColabError, Result};
6
7const APP_NAME: &str = "colab-cli";
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ColabEnvironment {
11 Production,
12 Sandbox,
13 Local,
14}
15
16impl ColabEnvironment {
17 fn colab_domain(&self) -> &'static str {
18 match self {
19 ColabEnvironment::Production => "https://colab.research.google.com",
20 ColabEnvironment::Sandbox => "https://colab.sandbox.google.com",
21 ColabEnvironment::Local => "https://localhost:8888",
22 }
23 }
24}
25
26impl std::str::FromStr for ColabEnvironment {
27 type Err = String;
28 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
29 match s {
30 "production" => Ok(ColabEnvironment::Production),
31 "sandbox" => Ok(ColabEnvironment::Sandbox),
32 "local" => Ok(ColabEnvironment::Local),
33 other => Err(format!(
34 "unknown environment '{other}' \u{2014} expected production, sandbox, or local"
35 )),
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
41pub struct ColabConfig {
42 pub colab_domain: String,
43 pub client_id: String,
44 pub client_secret: String,
45 pub environment: ColabEnvironment,
46 pub data_dir: PathBuf,
47}
48
49#[derive(Debug, Deserialize, Serialize, Default)]
50struct ConfigFile {
51 environment: Option<String>,
52 client_id: Option<String>,
53 client_secret: Option<String>,
54 colab_domain: Option<String>,
55 colab_gapi_domain: Option<String>,
56}
57
58impl ColabConfig {
59 pub fn load(_quiet: bool) -> Result<Self> {
60 let config_dir = config_dir()?;
61 let data_dir = data_dir()?;
62 let file = load_config_file(&config_dir);
63
64 let env_str = std::env::var("COLAB_EXTENSION_ENVIRONMENT")
65 .ok()
66 .or(file.environment)
67 .unwrap_or_else(|| "production".to_string());
68
69 let environment: ColabEnvironment =
70 env_str.parse().map_err(|e: String| ColabError::config(e))?;
71
72 let colab_domain = std::env::var("COLAB_DOMAIN")
73 .ok()
74 .or(file.colab_domain)
75 .unwrap_or_else(|| environment.colab_domain().to_string());
76
77 let client_id = std::env::var("COLAB_EXTENSION_CLIENT_ID")
78 .ok()
79 .filter(|s| !s.is_empty())
80 .or_else(|| file.client_id.filter(|s| !s.is_empty()))
81 .unwrap_or_else(crate::embedded::embedded_client_id);
82
83 if client_id.is_empty() {
84 return Err(ColabError::config(
85 "COLAB_EXTENSION_CLIENT_ID is not set \u{2014} add it to your .env file or ~/.config/colab-cli/config.toml",
86 ));
87 }
88
89 let client_secret = std::env::var("COLAB_EXTENSION_CLIENT_NOT_SO_SECRET")
90 .ok()
91 .filter(|s| !s.is_empty())
92 .or_else(|| file.client_secret.filter(|s| !s.is_empty()))
93 .unwrap_or_else(crate::embedded::embedded_client_secret);
94
95 if client_secret.is_empty() {
96 return Err(ColabError::config(
97 "COLAB_EXTENSION_CLIENT_NOT_SO_SECRET is not set \u{2014} add it to your .env file or ~/.config/colab-cli/config.toml",
98 ));
99 }
100
101 Ok(Self {
102 colab_domain,
103 client_id,
104 client_secret,
105 environment,
106 data_dir,
107 })
108 }
109
110 pub fn servers_file(&self) -> PathBuf {
111 self.data_dir.join("servers.json")
112 }
113
114 pub fn is_local(&self) -> bool {
115 self.environment == ColabEnvironment::Local
116 }
117}
118
119fn load_config_file(config_dir: &Path) -> ConfigFile {
120 let path = config_dir.join("config.toml");
121 let Ok(contents) = std::fs::read_to_string(&path) else {
122 return ConfigFile::default();
123 };
124 toml::from_str(&contents).unwrap_or_default()
125}
126
127fn config_dir() -> Result<PathBuf> {
128 let base = dirs::config_dir()
129 .ok_or_else(|| ColabError::config("could not determine config directory"))?;
130 let dir = base.join(APP_NAME);
131 std::fs::create_dir_all(&dir)?;
132 Ok(dir)
133}
134
135fn data_dir() -> Result<PathBuf> {
136 let base = dirs::data_local_dir()
137 .ok_or_else(|| ColabError::config("could not determine data directory"))?;
138 let dir = base.join(APP_NAME);
139 std::fs::create_dir_all(&dir)?;
140 Ok(dir)
141}