Skip to main content

yuki_cli/cli/
init.rs

1use std::collections::BTreeMap;
2use std::io::{self, BufRead, Write};
3
4use owo_colors::OwoColorize;
5
6use crate::client::accounting::AccountingClient;
7use crate::config::{AdminEntry, Config};
8use crate::error::YukiError;
9use crate::output::is_tty;
10
11fn sym_ok() -> String {
12    if is_tty() {
13        "✔".green().to_string()
14    } else {
15        "✔".to_owned()
16    }
17}
18
19fn sym_fail() -> String {
20    if is_tty() {
21        "✖".red().to_string()
22    } else {
23        "✖".to_owned()
24    }
25}
26
27fn bold(s: &str) -> String {
28    if is_tty() {
29        s.bold().to_string()
30    } else {
31        s.to_owned()
32    }
33}
34
35fn dim(s: &str) -> String {
36    if is_tty() {
37        s.dimmed().to_string()
38    } else {
39        s.to_owned()
40    }
41}
42
43/// Convert an administration name to a safe config key.
44///
45/// Lowercases the name and replaces any non-alphanumeric character with an underscore.
46fn safe_name(name: &str) -> String {
47    name.chars()
48        .map(|c| {
49            if c.is_alphanumeric() {
50                c.to_ascii_lowercase()
51            } else {
52                '_'
53            }
54        })
55        .collect()
56}
57
58pub async fn run(api_key: Option<&str>, default_admin: Option<&str>) -> Result<(), YukiError> {
59    let stdin = io::stdin();
60    let path = Config::default_path();
61
62    // If config exists and only --api-key is provided, update the key in place.
63    if let (Some(key), None) = (api_key, default_admin)
64        && let Ok(mut config) = Config::load_from(&path)
65    {
66        let key = key.trim().to_string();
67        if key.is_empty() {
68            return Err(YukiError::Config("API key cannot be empty".to_string()));
69        }
70
71        eprintln!("Verifying new API key...");
72        let mut client = AccountingClient::new();
73        client.authenticate(&key).await?;
74
75        config.api_key = key;
76        config.save_to(&path)?;
77        eprintln!("{} API key updated in {}", sym_ok(), path.display());
78        return Ok(());
79    }
80
81    let api_key = match api_key {
82        Some(k) => k.trim().to_string(),
83        None => {
84            eprintln!("  {} Yuki Portal → Settings → API keys", dim("→"));
85            eprint!("Yuki API key: ");
86            io::stderr().flush().ok();
87            stdin
88                .lock()
89                .lines()
90                .next()
91                .and_then(|l| l.ok())
92                .map(|l| l.trim().to_string())
93                .unwrap_or_default()
94        }
95    };
96
97    if api_key.is_empty() {
98        return Err(YukiError::Config("API key cannot be empty".to_string()));
99    }
100
101    eprint!("Authenticating...");
102    io::stderr().flush().ok();
103    let mut client = AccountingClient::new();
104    match client.authenticate(&api_key).await {
105        Ok(_) => eprintln!(" {}", sym_ok()),
106        Err(e) => {
107            eprintln!(" {}", sym_fail());
108            return Err(e);
109        }
110    }
111
112    eprint!("Fetching administrations...");
113    io::stderr().flush().ok();
114    let admins = match client.administrations().await {
115        Ok(admins) => {
116            eprintln!(" {}", sym_ok());
117            admins
118        }
119        Err(e) => {
120            eprintln!(" {}", sym_fail());
121            return Err(e);
122        }
123    };
124
125    if admins.is_empty() {
126        return Err(YukiError::NotFound(
127            "no administrations found for this API key".to_string(),
128        ));
129    }
130
131    eprintln!("Found {} administration(s):", admins.len());
132    for (i, a) in admins.iter().enumerate() {
133        eprintln!("  [{}] {}", i + 1, a.name);
134    }
135
136    let default_name = if let Some(name) = default_admin {
137        // Use the provided name directly — verify it exists.
138        let key = safe_name(name);
139        if !admins.iter().any(|a| safe_name(&a.name) == key) {
140            return Err(YukiError::Config(format!(
141                "administration not found: {name}"
142            )));
143        }
144        eprintln!("Using \"{name}\" as the default administration.");
145        key
146    } else if admins.len() == 1 {
147        eprintln!(
148            "Using \"{}\" as the default administration.",
149            admins[0].name
150        );
151        safe_name(&admins[0].name)
152    } else {
153        eprint!("Select default administration [1]: ");
154        io::stderr().flush().ok();
155
156        let choice = stdin
157            .lock()
158            .lines()
159            .next()
160            .and_then(|l| l.ok())
161            .map(|l| l.trim().to_string())
162            .unwrap_or_default();
163
164        let idx: usize = if choice.is_empty() {
165            1
166        } else {
167            choice
168                .parse::<usize>()
169                .map_err(|_| YukiError::Config(format!("invalid selection: {choice}")))?
170        };
171
172        if idx == 0 || idx > admins.len() {
173            return Err(YukiError::Config(format!("selection out of range: {idx}")));
174        }
175        safe_name(&admins[idx - 1].name)
176    };
177
178    // Store both domain_id and admin_id per administration
179    let administrations: BTreeMap<String, AdminEntry> = admins
180        .iter()
181        .map(|a| {
182            (
183                safe_name(&a.name),
184                AdminEntry {
185                    domain_id: a.domain_id.clone(),
186                    admin_id: a.id.clone(),
187                },
188            )
189        })
190        .collect();
191
192    // Preserve unmatched_ignore from existing config if present.
193    let unmatched_ignore = Config::load_from(&path)
194        .map(|c| c.unmatched_ignore)
195        .unwrap_or_default();
196
197    let config = Config {
198        api_key,
199        default_admin: default_name.clone(),
200        administrations,
201        unmatched_ignore,
202    };
203
204    config.save_to(&path)?;
205
206    eprintln!();
207    eprintln!("{} Configuration saved to {}", sym_ok(), path.display());
208    eprintln!();
209    eprintln!("{}:", bold("Next steps"));
210    eprintln!(
211        "  yuki documents list  {}",
212        dim("# list archived documents")
213    );
214    eprintln!("  yuki contacts list   {}", dim("# list contacts"));
215    eprintln!("  yuki invoices list   {}", dim("# list invoices"));
216    eprintln!("  yuki completions zsh {}", dim("# shell completions"));
217    eprintln!();
218
219    Ok(())
220}