Skip to main content

gemini_cli/auth/
remove.rs

1use std::io::{self, IsTerminal, Write};
2use std::path::{Path, PathBuf};
3
4use crate::auth::output;
5
6pub fn run(target: &str, yes: bool) -> i32 {
7    run_with_json(target, yes, false)
8}
9
10pub fn run_with_json(target: &str, yes: bool, output_json: bool) -> i32 {
11    if target.is_empty() {
12        return usage_error(
13            output_json,
14            "gemini-remove: usage: gemini-remove [--yes] <secret.json>",
15        );
16    }
17
18    if is_invalid_target(target) {
19        if output_json {
20            let _ = output::emit_error(
21                "auth remove",
22                "invalid-secret-file-name",
23                format!("gemini-remove: invalid secret file name: {target}"),
24                Some(output::obj(vec![("target", output::s(target))])),
25            );
26        } else {
27            eprintln!("gemini-remove: invalid secret file name: {target}");
28        }
29        return 64;
30    }
31
32    let secret_dir = match resolve_secret_dir() {
33        Some(path) => path,
34        None => {
35            if output_json {
36                let _ = output::emit_error(
37                    "auth remove",
38                    "secret-dir-not-configured",
39                    "gemini-remove: secret directory is not configured",
40                    None,
41                );
42            } else {
43                eprintln!("gemini-remove: secret directory is not configured");
44            }
45            return 1;
46        }
47    };
48
49    if !secret_dir.is_dir() {
50        if output_json {
51            let _ = output::emit_error(
52                "auth remove",
53                "secret-dir-not-found",
54                format!(
55                    "gemini-remove: secret directory not found: {}",
56                    secret_dir.display()
57                ),
58                Some(output::obj(vec![(
59                    "secret_dir",
60                    output::s(secret_dir.display().to_string()),
61                )])),
62            );
63        } else {
64            eprintln!(
65                "gemini-remove: secret directory not found: {}",
66                secret_dir.display()
67            );
68        }
69        return 1;
70    }
71
72    let target_file = secret_dir.join(target);
73    if !target_file.is_file() {
74        if output_json {
75            let _ = output::emit_error(
76                "auth remove",
77                "target-not-found",
78                format!(
79                    "gemini-remove: secret file not found: {}",
80                    target_file.display()
81                ),
82                Some(output::obj(vec![(
83                    "target_file",
84                    output::s(target_file.display().to_string()),
85                )])),
86            );
87        } else {
88            eprintln!(
89                "gemini-remove: secret file not found: {}",
90                target_file.display()
91            );
92        }
93        return 1;
94    }
95
96    if !yes {
97        if output_json {
98            let _ = output::emit_error(
99                "auth remove",
100                "remove-confirmation-required",
101                format!(
102                    "gemini-remove: {} exists; rerun with --yes to remove",
103                    target_file.display()
104                ),
105                Some(output::obj(vec![
106                    ("target_file", output::s(target_file.display().to_string())),
107                    ("removed", output::b(false)),
108                ])),
109            );
110            return 1;
111        }
112
113        if !interactive_io_available() {
114            eprintln!(
115                "gemini-remove: {} exists; rerun with --yes to remove",
116                target_file.display()
117            );
118            return 1;
119        }
120
121        match confirm_remove(&target_file) {
122            Ok(true) => {}
123            Ok(false) => {
124                eprintln!(
125                    "gemini-remove: removal declined for {}",
126                    target_file.display()
127                );
128                return 1;
129            }
130            Err(_) => return 1,
131        }
132    }
133
134    if let Err(err) = std::fs::remove_file(&target_file) {
135        if output_json {
136            let _ = output::emit_error(
137                "auth remove",
138                "remove-failed",
139                format!("gemini-remove: failed to remove {}", target_file.display()),
140                Some(output::obj(vec![
141                    ("target_file", output::s(target_file.display().to_string())),
142                    ("error", output::s(err.to_string())),
143                ])),
144            );
145        } else {
146            eprintln!("gemini-remove: failed to remove {}", target_file.display());
147        }
148        return 1;
149    }
150
151    remove_target_timestamp(&target_file);
152
153    if output_json {
154        let _ = output::emit_result(
155            "auth remove",
156            output::obj(vec![
157                ("target_file", output::s(target_file.display().to_string())),
158                ("removed", output::b(true)),
159            ]),
160        );
161    } else {
162        println!("gemini: removed {}", target_file.display());
163    }
164    0
165}
166
167fn usage_error(output_json: bool, message: &str) -> i32 {
168    if output_json {
169        let _ = output::emit_error("auth remove", "invalid-usage", message, None);
170    } else {
171        eprintln!("{message}");
172    }
173    64
174}
175
176fn resolve_secret_dir() -> Option<PathBuf> {
177    crate::paths::resolve_secret_dir()
178}
179
180fn is_invalid_target(target: &str) -> bool {
181    target.contains('/') || target.contains('\\') || target.contains("..")
182}
183
184fn interactive_io_available() -> bool {
185    io::stdin().is_terminal() && io::stdout().is_terminal()
186}
187
188fn confirm_remove(target: &Path) -> io::Result<bool> {
189    eprint!("gemini-remove: remove {}? [y/N]: ", target.display());
190    io::stderr().flush()?;
191
192    let mut line = String::new();
193    io::stdin().read_line(&mut line)?;
194    let normalized = line.trim().to_ascii_lowercase();
195    Ok(matches!(normalized.as_str(), "y" | "yes"))
196}
197
198fn remove_target_timestamp(target_file: &Path) {
199    let Some(cache_dir) = crate::paths::resolve_secret_cache_dir() else {
200        return;
201    };
202    let file_name = target_file
203        .file_name()
204        .and_then(|v| v.to_str())
205        .unwrap_or("auth.json");
206    let timestamp_file = cache_dir.join(format!("{file_name}.timestamp"));
207    let _ = std::fs::remove_file(timestamp_file);
208}
209
210#[cfg(test)]
211mod tests {
212    use super::{is_invalid_target, resolve_secret_dir};
213
214    #[test]
215    fn invalid_target_rejects_paths_and_traversal() {
216        assert!(is_invalid_target("../a.json"));
217        assert!(is_invalid_target("a/b.json"));
218        assert!(is_invalid_target(r"a\b.json"));
219        assert!(!is_invalid_target("alpha.json"));
220    }
221
222    #[test]
223    fn resolve_secret_dir_uses_gemini_secret_dir_env_override() {
224        let _lock = crate::auth::test_env_lock();
225        let old_home = std::env::var_os("HOME");
226        let old = std::env::var_os("GEMINI_SECRET_DIR");
227        // SAFETY: test-scoped env mutation.
228        unsafe { std::env::set_var("HOME", "") };
229        // SAFETY: test-scoped env mutation.
230        unsafe { std::env::set_var("GEMINI_SECRET_DIR", "/tmp/secrets") };
231        assert_eq!(
232            resolve_secret_dir().expect("secret dir"),
233            std::path::PathBuf::from("/tmp/secrets")
234        );
235        if let Some(value) = old_home {
236            // SAFETY: test-scoped env restore.
237            unsafe { std::env::set_var("HOME", value) };
238        } else {
239            // SAFETY: test-scoped env restore.
240            unsafe { std::env::remove_var("HOME") };
241        }
242        if let Some(value) = old {
243            // SAFETY: test-scoped env restore.
244            unsafe { std::env::set_var("GEMINI_SECRET_DIR", value) };
245        } else {
246            // SAFETY: test-scoped env restore.
247            unsafe { std::env::remove_var("GEMINI_SECRET_DIR") };
248        }
249    }
250}