romm_cli/commands/
init.rs1use anyhow::{anyhow, Context, Result};
7use clap::Args;
8use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select};
9use std::fs;
10
11use crate::config::{normalize_romm_origin, persist_user_config, user_config_env_path, AuthConfig};
12
13#[derive(Args, Debug, Clone)]
14pub struct InitCommand {
15 #[arg(long)]
17 pub force: bool,
18
19 #[arg(long)]
21 pub print_path: bool,
22}
23
24enum AuthChoice {
25 None,
26 Basic,
27 Bearer,
28 ApiKeyHeader,
29}
30
31pub fn handle(cmd: InitCommand) -> Result<()> {
32 let Some(path) = user_config_env_path() else {
33 return Err(anyhow!(
34 "Could not determine config directory (no HOME / APPDATA?)."
35 ));
36 };
37
38 if cmd.print_path {
39 println!("{}", path.display());
40 return Ok(());
41 }
42
43 let dir = path
44 .parent()
45 .ok_or_else(|| anyhow!("invalid config path"))?;
46
47 if path.exists() && !cmd.force {
48 let cont = Confirm::with_theme(&ColorfulTheme::default())
49 .with_prompt(format!("Overwrite existing config at {}?", path.display()))
50 .default(false)
51 .interact()?;
52 if !cont {
53 println!("Aborted.");
54 return Ok(());
55 }
56 }
57
58 fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
59
60 let base_input: String = Input::with_theme(&ColorfulTheme::default())
61 .with_prompt("RomM web URL (same as in your browser; do not add /api)")
62 .with_initial_text("https://")
63 .interact_text()?;
64
65 let base_input = base_input.trim();
66 if base_input.is_empty() {
67 return Err(anyhow!("Base URL cannot be empty"));
68 }
69
70 let had_api_path = base_input.trim_end_matches('/').ends_with("/api");
71 let base_url = normalize_romm_origin(base_input);
72 if had_api_path {
73 println!(
74 "Using `{base_url}` — `/api` was removed. Requests use `/api/...` under that origin automatically."
75 );
76 }
77
78 let default_dl_dir = dirs::download_dir()
80 .unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join("Downloads"))
81 .join("romm-cli");
82
83 let download_dir: String = Input::with_theme(&ColorfulTheme::default())
84 .with_prompt("Download directory for ROMs")
85 .default(default_dl_dir.display().to_string())
86 .interact_text()?;
87
88 let download_dir = download_dir.trim().to_string();
89
90 let items = vec![
92 "No authentication",
93 "Basic (username + password)",
94 "Bearer token",
95 "API key in custom header",
96 ];
97 let idx = Select::with_theme(&ColorfulTheme::default())
98 .with_prompt("Authentication")
99 .items(&items)
100 .default(0)
101 .interact()?;
102
103 let choice = match idx {
104 0 => AuthChoice::None,
105 1 => AuthChoice::Basic,
106 2 => AuthChoice::Bearer,
107 3 => AuthChoice::ApiKeyHeader,
108 _ => AuthChoice::None,
109 };
110
111 let auth: Option<AuthConfig> = match choice {
112 AuthChoice::None => None,
113 AuthChoice::Basic => {
114 let username: String = Input::with_theme(&ColorfulTheme::default())
115 .with_prompt("Username")
116 .interact_text()?;
117 let password = Password::with_theme(&ColorfulTheme::default())
118 .with_prompt("Password")
119 .interact()?;
120 Some(AuthConfig::Basic {
121 username: username.trim().to_string(),
122 password,
123 })
124 }
125 AuthChoice::Bearer => {
126 let token = Password::with_theme(&ColorfulTheme::default())
127 .with_prompt("Bearer token")
128 .interact()?;
129 Some(AuthConfig::Bearer { token })
130 }
131 AuthChoice::ApiKeyHeader => {
132 let header: String = Input::with_theme(&ColorfulTheme::default())
133 .with_prompt("Header name (e.g. X-API-Key)")
134 .interact_text()?;
135 let key = Password::with_theme(&ColorfulTheme::default())
136 .with_prompt("API key value")
137 .interact()?;
138 Some(AuthConfig::ApiKey {
139 header: header.trim().to_string(),
140 key,
141 })
142 }
143 };
144
145 let use_https = Confirm::with_theme(&ColorfulTheme::default())
146 .with_prompt("Connect over HTTPS?")
147 .default(true)
148 .interact()?;
149
150 persist_user_config(&base_url, &download_dir, use_https, auth)?;
151
152 println!("Wrote {}", path.display());
153 println!("Secrets are stored in the OS keyring when available (see file comments if plaintext fallback was used).");
154 println!("You can run `romm-cli tui` or `romm-tui` to start the TUI.");
155 Ok(())
156}