Skip to main content

codex_mobile_bridge/
config.rs

1use std::ffi::OsStr;
2use std::path::{Path, PathBuf};
3
4#[cfg(unix)]
5use std::os::unix::fs::PermissionsExt;
6
7use anyhow::{Context, Result, bail};
8use clap::Parser;
9
10#[derive(Debug, Clone, Parser)]
11#[command(author, version, about = "Codex Mobile App Server bridge")]
12pub struct Config {
13    #[arg(long, env = "CODEX_MOBILE_LISTEN_ADDR", default_value = "0.0.0.0:8787")]
14    pub listen_addr: String,
15
16    #[arg(long, env = "CODEX_MOBILE_TOKEN")]
17    pub token: String,
18
19    #[arg(long, env = "CODEX_MOBILE_RUNTIME_LIMIT", default_value_t = 4)]
20    pub runtime_limit: usize,
21
22    #[arg(
23        long,
24        env = "CODEX_MOBILE_DB_PATH",
25        default_value = "~/.local/state/codex-mobile/bridge.db"
26    )]
27    pub db_path: PathBuf,
28
29    #[arg(long, env = "CODEX_HOME")]
30    pub codex_home: Option<PathBuf>,
31
32    #[arg(long, env = "CODEX_BINARY", default_value = "codex")]
33    pub codex_binary: String,
34
35    #[arg(long = "directory-bookmark")]
36    pub directory_bookmarks: Vec<PathBuf>,
37}
38
39impl Config {
40    pub fn validated(mut self) -> Result<Self> {
41        if self.token.trim().is_empty() {
42            bail!("bridge token 不能为空");
43        }
44
45        self.db_path = expand_path(&self.db_path)?;
46        self.codex_home = self
47            .codex_home
48            .as_ref()
49            .map(|path| expand_path(path))
50            .transpose()?;
51        self.codex_binary = resolve_codex_binary(
52            &self.codex_binary,
53            std::env::var_os("PATH").as_deref(),
54            std::env::var_os("HOME").as_deref().map(Path::new),
55        )?;
56
57        let normalized_roots = self
58            .directory_bookmarks
59            .iter()
60            .map(|path| expand_path(path))
61            .collect::<Result<Vec<_>>>()?;
62        self.directory_bookmarks = normalized_roots;
63
64        Ok(self)
65    }
66}
67
68pub fn expand_path(path: &Path) -> Result<PathBuf> {
69    let raw = path.to_string_lossy();
70    if raw == "~" {
71        return home_dir();
72    }
73
74    if let Some(stripped) = raw.strip_prefix("~/") {
75        return Ok(home_dir()?.join(stripped));
76    }
77
78    if path.is_absolute() {
79        return Ok(path.to_path_buf());
80    }
81
82    let cwd = std::env::current_dir().context("读取当前工作目录失败")?;
83    Ok(cwd.join(path))
84}
85
86fn home_dir() -> Result<PathBuf> {
87    std::env::var_os("HOME")
88        .map(PathBuf::from)
89        .context("未找到 HOME 环境变量")
90}
91
92pub fn resolve_codex_binary(
93    raw: &str,
94    path_env: Option<&OsStr>,
95    home_env: Option<&Path>,
96) -> Result<String> {
97    let trimmed = raw.trim();
98    if trimmed.is_empty() {
99        bail!("CODEX_BINARY 不能为空");
100    }
101
102    if trimmed.contains('/') || trimmed.starts_with('~') {
103        return Ok(expand_path(Path::new(trimmed))?
104            .to_string_lossy()
105            .to_string());
106    }
107
108    if let Some(resolved) = find_in_path(trimmed, path_env) {
109        return Ok(resolved.to_string_lossy().to_string());
110    }
111
112    if let Some(resolved) = find_in_home_bins(trimmed, home_env) {
113        return Ok(resolved.to_string_lossy().to_string());
114    }
115
116    Ok(trimmed.to_string())
117}
118
119fn find_in_path(binary: &str, path_env: Option<&OsStr>) -> Option<PathBuf> {
120    let path_env = path_env?;
121    std::env::split_paths(path_env)
122        .map(|dir| dir.join(binary))
123        .find(|candidate| is_executable(candidate))
124}
125
126fn find_in_home_bins(binary: &str, home_env: Option<&Path>) -> Option<PathBuf> {
127    let home = home_env?;
128    [
129        home.join(".npm-global/bin").join(binary),
130        home.join(".local/bin").join(binary),
131        home.join("bin").join(binary),
132        home.join(".cargo/bin").join(binary),
133    ]
134    .into_iter()
135    .find(|candidate| is_executable(candidate))
136}
137
138fn is_executable(path: &Path) -> bool {
139    let Ok(metadata) = std::fs::metadata(path) else {
140        return false;
141    };
142    if !metadata.is_file() {
143        return false;
144    }
145
146    #[cfg(unix)]
147    {
148        metadata.permissions().mode() & 0o111 != 0
149    }
150
151    #[cfg(not(unix))]
152    {
153        true
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use std::env;
160    use std::ffi::OsString;
161    use std::fs;
162    #[cfg(unix)]
163    use std::os::unix::fs::PermissionsExt;
164
165    use super::resolve_codex_binary;
166
167    #[test]
168    fn resolve_codex_binary_uses_path_entry_when_available() {
169        let base_dir =
170            env::temp_dir().join(format!("codex-mobile-config-test-{}", std::process::id()));
171        fs::create_dir_all(&base_dir).expect("创建测试目录失败");
172        let binary_path = base_dir.join("codex");
173        fs::write(&binary_path, "#!/bin/sh\n").expect("写入可执行文件失败");
174        #[cfg(unix)]
175        {
176            fs::set_permissions(&binary_path, fs::Permissions::from_mode(0o755))
177                .expect("设置权限失败");
178        }
179
180        let path_env = OsString::from(&base_dir);
181        let resolved = resolve_codex_binary(
182            "codex",
183            Some(path_env.as_os_str()),
184            Some(base_dir.as_path()),
185        )
186        .expect("解析 codex 失败");
187
188        assert_eq!(resolved, binary_path.to_string_lossy());
189    }
190
191    #[test]
192    fn resolve_codex_binary_falls_back_to_common_home_bin() {
193        let home_dir =
194            env::temp_dir().join(format!("codex-mobile-home-test-{}", std::process::id()));
195        let npm_dir = home_dir.join(".npm-global/bin");
196        fs::create_dir_all(&npm_dir).expect("创建 npm bin 目录失败");
197        let binary_path = npm_dir.join("codex");
198        fs::write(&binary_path, "#!/bin/sh\n").expect("写入可执行文件失败");
199        #[cfg(unix)]
200        {
201            fs::set_permissions(&binary_path, fs::Permissions::from_mode(0o755))
202                .expect("设置权限失败");
203        }
204
205        let path_env = OsString::from("/usr/bin");
206        let resolved = resolve_codex_binary("codex", Some(path_env.as_os_str()), Some(&home_dir))
207            .expect("应回退到 HOME bin");
208
209        assert_eq!(resolved, binary_path.to_string_lossy());
210    }
211}