Skip to main content

clickup_cli/
config.rs

1use crate::error::CliError;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5#[derive(Debug, Serialize, Deserialize, Default)]
6pub struct Config {
7    pub auth: AuthConfig,
8    #[serde(default)]
9    pub defaults: DefaultsConfig,
10    #[serde(default)]
11    pub git: GitConfig,
12}
13
14#[derive(Debug, Serialize, Deserialize, Default)]
15pub struct AuthConfig {
16    pub token: String,
17}
18
19#[derive(Debug, Serialize, Deserialize, Default)]
20pub struct DefaultsConfig {
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub workspace_id: Option<String>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub output: Option<String>,
25}
26
27#[derive(Debug, Serialize, Deserialize, Default)]
28pub struct GitConfig {
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub enabled: Option<bool>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub verbose: Option<bool>,
33}
34
35impl Config {
36    pub fn config_path() -> Result<PathBuf, CliError> {
37        let config_dir = dirs::config_dir()
38            .ok_or_else(|| CliError::ConfigError("Could not determine config directory".into()))?;
39        Ok(config_dir.join("clickup-cli").join("config.toml"))
40    }
41
42    /// Walk from `start` up to filesystem root, returning the nearest `.clickup.toml`.
43    pub fn find_project_config(start: &std::path::Path) -> Option<PathBuf> {
44        start.ancestors().find_map(|dir| {
45            let candidate = dir.join(".clickup.toml");
46            candidate.is_file().then_some(candidate)
47        })
48    }
49
50    /// Load config: nearest .clickup.toml walking up from CWD, then global config
51    pub fn load() -> Result<Self, CliError> {
52        if let Ok(cwd) = std::env::current_dir() {
53            if let Some(project_path) = Self::find_project_config(&cwd) {
54                let project_config = Self::load_from(&project_path)?;
55                if !project_config.auth.token.is_empty() {
56                    return Ok(project_config);
57                }
58            }
59        }
60        let path = Self::config_path()?;
61        Self::load_from(&path)
62    }
63
64    pub fn load_from(path: &std::path::Path) -> Result<Self, CliError> {
65        if !path.exists() {
66            return Err(CliError::ConfigError("Not configured".into()));
67        }
68        let contents = std::fs::read_to_string(path)?;
69        toml::from_str(&contents)
70            .map_err(|e| CliError::ConfigError(format!("Invalid config file: {}", e)))
71    }
72
73    pub fn save(&self) -> Result<(), CliError> {
74        let path = Self::config_path()?;
75        self.save_to(&path)
76    }
77
78    pub fn save_to(&self, path: &std::path::Path) -> Result<(), CliError> {
79        if let Some(parent) = path.parent() {
80            std::fs::create_dir_all(parent)?;
81        }
82        let contents = toml::to_string_pretty(self)
83            .map_err(|e| CliError::ConfigError(format!("Failed to serialize config: {}", e)))?;
84        std::fs::write(path, contents)?;
85        Ok(())
86    }
87}