codex_mobile_bridge/manage/
mod.rs1use std::collections::HashMap;
2use std::env;
3use std::fs;
4#[cfg(unix)]
5use std::os::unix::fs::{PermissionsExt, symlink};
6use std::path::{Path, PathBuf};
7use std::process::Command;
8
9use anyhow::{Context, Result, bail};
10use clap::{Args, Parser, Subcommand, ValueEnum};
11use serde::{Deserialize, Serialize};
12use serde_json::json;
13use sha2::{Digest, Sha256};
14
15use crate::{BRIDGE_BUILD_HASH, BRIDGE_PROTOCOL_VERSION, BRIDGE_VERSION};
16
17mod commands;
18mod environment;
19mod records;
20mod release;
21#[cfg(test)]
22mod tests;
23
24const CRATE_NAME: &str = env!("CARGO_PKG_NAME");
25const BINARY_NAME: &str = "codex-mobile-bridge";
26const SERVICE_NAME: &str = "codex-mobile-bridge.service";
27const DEFAULT_LISTEN_ADDR: &str = "0.0.0.0:8787";
28const DEFAULT_RUNTIME_LIMIT: usize = 4;
29
30#[derive(Debug, Parser)]
31pub struct ManageCli {
32 #[command(subcommand)]
33 command: ManageCommand,
34}
35
36#[derive(Debug, Subcommand)]
37enum ManageCommand {
38 Activate(ActivateArgs),
39 SelfUpdate(SelfUpdateArgs),
40 Rollback(RollbackArgs),
41 Repair(RepairArgs),
42 Metadata,
43}
44
45#[derive(Debug, Clone, Args, Default)]
46struct EnvOverrides {
47 #[arg(long)]
48 bridge_token: Option<String>,
49
50 #[arg(long)]
51 listen_addr: Option<String>,
52
53 #[arg(long)]
54 runtime_limit: Option<usize>,
55
56 #[arg(long)]
57 db_path: Option<PathBuf>,
58
59 #[arg(long)]
60 codex_home: Option<PathBuf>,
61
62 #[arg(long)]
63 codex_binary: Option<String>,
64
65 #[arg(long)]
66 launch_path: Option<String>,
67}
68
69#[derive(Debug, Clone, Args)]
70pub struct ActivateArgs {
71 #[command(flatten)]
72 env: EnvOverrides,
73
74 #[arg(long, value_enum, default_value_t = ActivateOperation::Install)]
75 operation: ActivateOperation,
76}
77
78#[derive(Debug, Clone, Copy, Eq, PartialEq, ValueEnum)]
79pub enum ActivateOperation {
80 Install,
81 Update,
82 Repair,
83}
84
85impl ActivateOperation {
86 fn as_str(self) -> &'static str {
87 match self {
88 Self::Install => "install",
89 Self::Update => "update",
90 Self::Repair => "repair",
91 }
92 }
93}
94
95#[derive(Debug, Clone, Args)]
96pub struct SelfUpdateArgs {
97 #[command(flatten)]
98 env: EnvOverrides,
99
100 #[arg(long)]
101 target_version: String,
102
103 #[arg(long, default_value = "cargo")]
104 cargo_binary: String,
105
106 #[arg(long, default_value = "crates-io")]
107 registry: String,
108}
109
110#[derive(Debug, Clone, Args)]
111pub struct RollbackArgs {
112 #[command(flatten)]
113 env: EnvOverrides,
114
115 #[arg(long, default_value = "cargo")]
116 cargo_binary: String,
117
118 #[arg(long, default_value = "crates-io")]
119 registry: String,
120}
121
122#[derive(Debug, Clone, Args)]
123pub struct RepairArgs {
124 #[command(flatten)]
125 env: EnvOverrides,
126
127 #[arg(long, default_value = "cargo")]
128 cargo_binary: String,
129
130 #[arg(long, default_value = "crates-io")]
131 registry: String,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
135struct BridgeEnvValues {
136 bridge_token: String,
137 listen_addr: String,
138 runtime_limit: usize,
139 db_path: PathBuf,
140 codex_home: Option<PathBuf>,
141 codex_binary: String,
142 launch_path: String,
143}
144
145#[derive(Debug, Clone)]
146struct ManagedPaths {
147 share_dir: PathBuf,
148 releases_dir: PathBuf,
149 current_link: PathBuf,
150 config_dir: PathBuf,
151 systemd_user_dir: PathBuf,
152 state_dir: PathBuf,
153 env_file: PathBuf,
154 unit_file: PathBuf,
155 install_record_file: PathBuf,
156 bridge_db_path: PathBuf,
157}
158
159impl ManagedPaths {
160 fn new(home_dir: PathBuf) -> Self {
161 let share_dir = home_dir.join(".local/share/codex-mobile");
162 let config_dir = home_dir.join(".config/codex-mobile");
163 let systemd_user_dir = home_dir.join(".config/systemd/user");
164 let state_dir = home_dir.join(".local/state/codex-mobile");
165 Self {
166 current_link: share_dir.join("current"),
167 releases_dir: share_dir.join("releases"),
168 env_file: config_dir.join("bridge.env"),
169 unit_file: systemd_user_dir.join(SERVICE_NAME),
170 install_record_file: state_dir.join("install.json"),
171 bridge_db_path: state_dir.join("bridge.db"),
172 share_dir,
173 config_dir,
174 systemd_user_dir,
175 state_dir,
176 }
177 }
178
179 fn release_root_for_version(&self, version: &str) -> PathBuf {
180 self.releases_dir.join(format!("{CRATE_NAME}-{version}"))
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, Default)]
185#[serde(rename_all = "camelCase")]
186#[serde(default)]
187struct InstallRecord {
188 install_state: String,
189 current_artifact_id: Option<String>,
190 current_version: Option<String>,
191 current_build_hash: Option<String>,
192 current_sha256: Option<String>,
193 current_protocol_version: Option<u32>,
194 current_release_path: Option<String>,
195 previous_artifact_id: Option<String>,
196 previous_version: Option<String>,
197 previous_build_hash: Option<String>,
198 previous_sha256: Option<String>,
199 previous_protocol_version: Option<u32>,
200 previous_release_path: Option<String>,
201 last_operation: Option<String>,
202 last_operation_status: Option<String>,
203 last_operation_at_ms: i64,
204 installed_at_ms: i64,
205 updated_at_ms: i64,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210struct ReleaseMetadata {
211 artifact_id: String,
212 version: String,
213 build_hash: String,
214 sha256: String,
215 protocol_version: u32,
216 release_root: String,
217 executable_path: String,
218}
219
220pub fn run(cli: ManageCli) -> Result<()> {
221 match cli.command {
222 ManageCommand::Activate(args) => commands::run_activate(args),
223 ManageCommand::SelfUpdate(args) => commands::run_self_update(args),
224 ManageCommand::Rollback(args) => commands::run_rollback(args),
225 ManageCommand::Repair(args) => commands::run_repair(args),
226 ManageCommand::Metadata => commands::print_metadata(),
227 }
228}