gemini_cli/auth/
remove.rs1use 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 unsafe { std::env::set_var("HOME", "") };
229 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 unsafe { std::env::set_var("HOME", value) };
238 } else {
239 unsafe { std::env::remove_var("HOME") };
241 }
242 if let Some(value) = old {
243 unsafe { std::env::set_var("GEMINI_SECRET_DIR", value) };
245 } else {
246 unsafe { std::env::remove_var("GEMINI_SECRET_DIR") };
248 }
249 }
250}