Skip to main content

codex_mobile_bridge/manage/
mod.rs

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