1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone)]
5pub struct DirResolverEnv {
6 pub platform: String,
7 pub env: HashMap<String, String>,
8 pub home: String,
9}
10
11impl Default for DirResolverEnv {
12 fn default() -> Self {
13 Self {
14 platform: std::env::consts::OS.into(),
15 env: std::env::vars().collect(),
16 home: std::env::var("HOME")
17 .or_else(|_| std::env::var("USERPROFILE"))
18 .unwrap_or_else(|_| "/".to_string()),
19 }
20 }
21}
22
23pub fn resolve_config_dir(deps: &DirResolverEnv) -> PathBuf {
24 if let Some(override_dir) = deps.env.get("CCP_CONFIG_DIR") {
25 return Path::new(override_dir).to_path_buf();
26 }
27
28 if deps.platform == "win32" {
29 let appdata = deps
30 .env
31 .get("APPDATA")
32 .cloned()
33 .unwrap_or_else(|| format!("{}\\AppData\\Roaming", deps.home));
34 return join_with_sep(&appdata, &["claude-code-proxy"], true);
35 }
36
37 if deps.platform == "darwin" {
38 return join_with_sep(&deps.home, &[".config", "claude-code-proxy"], false);
39 }
40
41 let base = deps.env.get("XDG_CONFIG_HOME").cloned().unwrap_or_else(|| {
42 join_with_sep(&deps.home, &[".config"], false)
43 .to_string_lossy()
44 .into_owned()
45 });
46 join_with_sep(&base, &["claude-code-proxy"], false)
47}
48
49pub fn resolve_state_dir(deps: &DirResolverEnv) -> PathBuf {
50 if deps.platform == "win32" {
51 let local = deps
52 .env
53 .get("LOCALAPPDATA")
54 .cloned()
55 .unwrap_or_else(|| format!("{}\\AppData\\Local", deps.home));
56 return join_with_sep(&local, &["claude-code-proxy"], true);
57 }
58
59 let base = deps.env.get("XDG_STATE_HOME").cloned().unwrap_or_else(|| {
60 join_with_sep(&deps.home, &[".local", "state"], false)
61 .to_string_lossy()
62 .into_owned()
63 });
64 join_with_sep(&base, &["claude-code-proxy"], false)
65}
66
67pub fn legacy_config_dir(deps: &DirResolverEnv) -> PathBuf {
68 join_with_sep(&deps.home, &[".config", "claude-code-proxy"], false)
69}
70
71pub fn config_dir() -> PathBuf {
72 resolve_config_dir(&DirResolverEnv::default())
73}
74
75pub fn state_dir() -> PathBuf {
76 resolve_state_dir(&DirResolverEnv::default())
77}
78
79pub fn codex_auth_file(deps: &DirResolverEnv) -> PathBuf {
80 resolve_config_dir(deps).join("codex").join("auth.json")
81}
82
83pub fn kimi_auth_file(deps: &DirResolverEnv) -> PathBuf {
84 resolve_config_dir(deps).join("kimi").join("auth.json")
85}
86
87pub fn cursor_auth_file(deps: &DirResolverEnv) -> PathBuf {
88 resolve_config_dir(deps).join("cursor").join("auth.json")
89}
90
91pub fn kimi_device_id_file(deps: &DirResolverEnv) -> PathBuf {
92 resolve_config_dir(deps).join("kimi").join("device_id")
93}
94
95pub fn log_file() -> PathBuf {
96 resolve_state_dir(&DirResolverEnv::default()).join("proxy.log")
97}
98
99pub fn provider_auth_file(provider: &str) -> PathBuf {
100 let deps = DirResolverEnv::default();
101 resolve_config_dir(&deps).join(provider).join("auth.json")
102}
103
104pub fn provider_legacy_auth_file(provider: &str) -> PathBuf {
105 let deps = DirResolverEnv::default();
106 legacy_config_dir(&deps).join(provider).join("auth.json")
107}
108
109fn join_with_sep(base: &str, parts: &[&str], win32: bool) -> PathBuf {
110 let sep = '/';
111 let _ = win32;
112 let mut out = String::new();
113 for part in std::iter::once(base).chain(parts.iter().copied()) {
114 if !out.is_empty() && !out.ends_with(sep) {
115 out.push(sep);
116 }
117 out.push_str(part);
118 }
119 Path::new(&out).to_path_buf()
120}
121
122pub fn resolve_config_dir_for_env(
123 platform: &str,
124 home: &str,
125 env: &HashMap<String, String>,
126) -> PathBuf {
127 resolve_config_dir(&DirResolverEnv {
128 platform: platform.to_string(),
129 env: env.clone(),
130 home: home.to_string(),
131 })
132}
133
134pub fn resolve_state_dir_for_env(
135 platform: &str,
136 home: &str,
137 env: &HashMap<String, String>,
138) -> PathBuf {
139 resolve_state_dir(&DirResolverEnv {
140 platform: platform.to_string(),
141 env: env.clone(),
142 home: home.to_string(),
143 })
144}