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