Skip to main content

yuki_cli/cli/
init.rs

1use std::collections::BTreeMap;
2use std::io::{self, BufRead, Write};
3
4use crate::client::accounting::AccountingClient;
5use crate::config::{AdminEntry, Config};
6use crate::error::YukiError;
7
8/// Convert an administration name to a safe config key.
9///
10/// Lowercases the name and replaces any non-alphanumeric character with an underscore.
11fn safe_name(name: &str) -> String {
12    name.chars()
13        .map(|c| {
14            if c.is_alphanumeric() {
15                c.to_ascii_lowercase()
16            } else {
17                '_'
18            }
19        })
20        .collect()
21}
22
23pub async fn run(api_key: Option<&str>, default_admin: Option<&str>) -> Result<(), YukiError> {
24    let stdin = io::stdin();
25    let path = Config::default_path();
26
27    // If config exists and only --api-key is provided, update the key in place.
28    if let (Some(key), None) = (api_key, default_admin)
29        && let Ok(mut config) = Config::load_from(&path)
30    {
31        let key = key.trim().to_string();
32        if key.is_empty() {
33            return Err(YukiError::Config("API key cannot be empty".to_string()));
34        }
35
36        eprintln!("Verifying new API key...");
37        let mut client = AccountingClient::new();
38        client.authenticate(&key).await?;
39
40        config.api_key = key;
41        config.save_to(&path)?;
42        eprintln!("API key updated in {}", path.display());
43        return Ok(());
44    }
45
46    let api_key = match api_key {
47        Some(k) => k.trim().to_string(),
48        None => {
49            eprint!("Yuki API key: ");
50            io::stderr().flush().ok();
51            stdin
52                .lock()
53                .lines()
54                .next()
55                .and_then(|l| l.ok())
56                .map(|l| l.trim().to_string())
57                .unwrap_or_default()
58        }
59    };
60
61    if api_key.is_empty() {
62        return Err(YukiError::Config("API key cannot be empty".to_string()));
63    }
64
65    eprintln!("Authenticating...");
66    let mut client = AccountingClient::new();
67    client.authenticate(&api_key).await?;
68
69    eprintln!("Fetching administrations...");
70    let admins = client.administrations().await?;
71
72    if admins.is_empty() {
73        return Err(YukiError::NotFound(
74            "no administrations found for this API key".to_string(),
75        ));
76    }
77
78    eprintln!("Found {} administration(s):", admins.len());
79    for (i, a) in admins.iter().enumerate() {
80        eprintln!("  [{}] {}", i + 1, a.name);
81    }
82
83    let default_name = if let Some(name) = default_admin {
84        // Use the provided name directly — verify it exists.
85        let key = safe_name(name);
86        if !admins.iter().any(|a| safe_name(&a.name) == key) {
87            return Err(YukiError::Config(format!(
88                "administration not found: {name}"
89            )));
90        }
91        eprintln!("Using \"{name}\" as the default administration.");
92        key
93    } else if admins.len() == 1 {
94        eprintln!(
95            "Using \"{}\" as the default administration.",
96            admins[0].name
97        );
98        safe_name(&admins[0].name)
99    } else {
100        eprint!("Select default administration [1]: ");
101        io::stderr().flush().ok();
102
103        let choice = stdin
104            .lock()
105            .lines()
106            .next()
107            .and_then(|l| l.ok())
108            .map(|l| l.trim().to_string())
109            .unwrap_or_default();
110
111        let idx: usize = if choice.is_empty() {
112            1
113        } else {
114            choice
115                .parse::<usize>()
116                .map_err(|_| YukiError::Config(format!("invalid selection: {choice}")))?
117        };
118
119        if idx == 0 || idx > admins.len() {
120            return Err(YukiError::Config(format!("selection out of range: {idx}")));
121        }
122        safe_name(&admins[idx - 1].name)
123    };
124
125    // Store both domain_id and admin_id per administration
126    let administrations: BTreeMap<String, AdminEntry> = admins
127        .iter()
128        .map(|a| {
129            (
130                safe_name(&a.name),
131                AdminEntry {
132                    domain_id: a.domain_id.clone(),
133                    admin_id: a.id.clone(),
134                },
135            )
136        })
137        .collect();
138
139    // Preserve unmatched_ignore from existing config if present.
140    let unmatched_ignore = Config::load_from(&path)
141        .map(|c| c.unmatched_ignore)
142        .unwrap_or_default();
143
144    let config = Config {
145        api_key,
146        default_admin: default_name.clone(),
147        administrations,
148        unmatched_ignore,
149    };
150
151    config.save_to(&path)?;
152
153    eprintln!("Configuration saved to {}", path.display());
154    eprintln!("Default administration: {default_name}");
155
156    Ok(())
157}