1use std::path::{Path, PathBuf};
2
3use crate::auth;
4use crate::auth::output;
5
6pub fn run() -> i32 {
7 run_with_json(false)
8}
9
10pub fn run_with_json(output_json: bool) -> i32 {
11 let auth_file = match crate::paths::resolve_auth_file() {
12 Some(path) => path,
13 None => {
14 if output_json {
15 let _ = output::emit_result(
16 "auth sync",
17 output::obj(vec![
18 ("auth_file", output::s("")),
19 ("synced", output::n(0)),
20 ("skipped", output::n(0)),
21 ("failed", output::n(0)),
22 ("updated_files", output::arr(Vec::new())),
23 ]),
24 );
25 }
26 return 0;
27 }
28 };
29
30 if !auth_file.is_file() {
31 if output_json {
32 let _ = output::emit_result(
33 "auth sync",
34 output::obj(vec![
35 ("auth_file", output::s(auth_file.display().to_string())),
36 ("synced", output::n(0)),
37 ("skipped", output::n(1)),
38 ("failed", output::n(0)),
39 ("updated_files", output::arr(Vec::new())),
40 ]),
41 );
42 }
43 return 0;
44 }
45
46 let auth_key = match auth::identity_key_from_auth_file(&auth_file) {
47 Ok(Some(key)) => key,
48 _ => {
49 if output_json {
50 let _ = output::emit_result(
51 "auth sync",
52 output::obj(vec![
53 ("auth_file", output::s(auth_file.display().to_string())),
54 ("synced", output::n(0)),
55 ("skipped", output::n(1)),
56 ("failed", output::n(0)),
57 ("updated_files", output::arr(Vec::new())),
58 ]),
59 );
60 }
61 return 0;
62 }
63 };
64
65 let auth_last_refresh = auth::last_refresh_from_auth_file(&auth_file).ok().flatten();
66 let auth_contents = match std::fs::read(&auth_file) {
67 Ok(contents) => contents,
68 Err(_) => {
69 if output_json {
70 let _ = output::emit_error(
71 "auth sync",
72 "auth-read-failed",
73 format!("failed to read {}", auth_file.display()),
74 Some(output::obj(vec![(
75 "path",
76 output::s(auth_file.display().to_string()),
77 )])),
78 );
79 } else {
80 eprintln!("gemini: failed to read {}", auth_file.display());
81 }
82 return 1;
83 }
84 };
85
86 let mut synced = 0usize;
87 let mut skipped = 0usize;
88 let failed = 0usize;
89 let mut updated_files: Vec<String> = Vec::new();
90
91 if let Some(secret_dir) = crate::paths::resolve_secret_dir()
92 && let Ok(entries) = std::fs::read_dir(&secret_dir)
93 {
94 for entry in entries.flatten() {
95 let path = entry.path();
96 if path.extension().and_then(|s| s.to_str()) != Some("json") {
97 continue;
98 }
99
100 let candidate_key = match auth::identity_key_from_auth_file(&path) {
101 Ok(Some(key)) => key,
102 _ => {
103 skipped += 1;
104 continue;
105 }
106 };
107
108 if candidate_key != auth_key {
109 skipped += 1;
110 continue;
111 }
112
113 let secret_contents = match std::fs::read(&path) {
114 Ok(contents) => contents,
115 Err(_) => {
116 if output_json {
117 let _ = output::emit_error(
118 "auth sync",
119 "secret-read-failed",
120 format!("failed to read {}", path.display()),
121 Some(output::obj(vec![(
122 "path",
123 output::s(path.display().to_string()),
124 )])),
125 );
126 } else {
127 eprintln!("gemini: failed to read {}", path.display());
128 }
129 return 1;
130 }
131 };
132
133 if secret_contents == auth_contents {
134 skipped += 1;
135 continue;
136 }
137
138 if auth::write_atomic(&path, &auth_contents, auth::SECRET_FILE_MODE).is_err() {
139 if output_json {
140 let _ = output::emit_error(
141 "auth sync",
142 "sync-write-failed",
143 format!("failed to write {}", path.display()),
144 Some(output::obj(vec![(
145 "path",
146 output::s(path.display().to_string()),
147 )])),
148 );
149 } else {
150 eprintln!("gemini: failed to write {}", path.display());
151 }
152 return 1;
153 }
154
155 if let Some(timestamp_path) = secret_timestamp_path(&path) {
156 let _ = auth::write_timestamp(×tamp_path, auth_last_refresh.as_deref());
157 }
158 synced += 1;
159 updated_files.push(path.display().to_string());
160 }
161 }
162
163 if let Some(auth_timestamp) = secret_timestamp_path(&auth_file) {
164 let _ = auth::write_timestamp(&auth_timestamp, auth_last_refresh.as_deref());
165 }
166
167 if output_json {
168 let _ = output::emit_result(
169 "auth sync",
170 output::obj(vec![
171 ("auth_file", output::s(auth_file.display().to_string())),
172 ("synced", output::n(synced as i64)),
173 ("skipped", output::n(skipped as i64)),
174 ("failed", output::n(failed as i64)),
175 (
176 "updated_files",
177 output::arr(updated_files.into_iter().map(output::s).collect()),
178 ),
179 ]),
180 );
181 }
182
183 0
184}
185
186fn secret_timestamp_path(target_file: &Path) -> Option<PathBuf> {
187 let cache_dir = crate::paths::resolve_secret_cache_dir()?;
188 let name = target_file
189 .file_name()
190 .and_then(|name| name.to_str())
191 .unwrap_or("auth.json");
192 Some(cache_dir.join(format!("{name}.timestamp")))
193}