codex-mobile-bridge 0.3.3

Remote bridge and service manager for codex-mobile.
Documentation
use std::collections::HashMap;
use std::env;
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::{PermissionsExt, symlink};
use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{Context, Result, bail};
use clap::{Args, Parser, Subcommand, ValueEnum};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::{Digest, Sha256};

use crate::{BRIDGE_BUILD_HASH, BRIDGE_PROTOCOL_VERSION, BRIDGE_VERSION};

mod commands;
mod environment;
mod records;
mod release;
mod tasks;
#[cfg(test)]
mod tests;

const CRATE_NAME: &str = env!("CARGO_PKG_NAME");
const BINARY_NAME: &str = "codex-mobile-bridge";
const SERVICE_NAME: &str = "codex-mobile-bridge.service";
const DEFAULT_LISTEN_ADDR: &str = "0.0.0.0:8787";
const DEFAULT_RUNTIME_LIMIT: usize = 4;

#[derive(Debug, Parser)]
pub struct ManageCli {
    #[command(subcommand)]
    command: ManageCommand,
}

#[derive(Debug, Subcommand)]
enum ManageCommand {
    Activate(ActivateArgs),
    SelfUpdate(SelfUpdateArgs),
    Rollback(RollbackArgs),
    Repair(RepairArgs),
    Metadata,
    RunTask(RunTaskArgs),
}

#[derive(Debug, Clone, Args, Default)]
struct EnvOverrides {
    #[arg(long)]
    bridge_token: Option<String>,

    #[arg(long)]
    listen_addr: Option<String>,

    #[arg(long)]
    runtime_limit: Option<usize>,

    #[arg(long)]
    db_path: Option<PathBuf>,

    #[arg(long)]
    codex_home: Option<PathBuf>,

    #[arg(long)]
    codex_binary: Option<String>,

    #[arg(long)]
    launch_path: Option<String>,
}

#[derive(Debug, Clone, Args)]
pub struct ActivateArgs {
    #[command(flatten)]
    env: EnvOverrides,

    #[arg(long, value_enum, default_value_t = ActivateOperation::Install)]
    operation: ActivateOperation,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq, ValueEnum)]
pub enum ActivateOperation {
    Install,
    Update,
    Repair,
}

impl ActivateOperation {
    fn as_str(self) -> &'static str {
        match self {
            Self::Install => "install",
            Self::Update => "update",
            Self::Repair => "repair",
        }
    }
}

#[derive(Debug, Clone, Args)]
pub struct SelfUpdateArgs {
    #[command(flatten)]
    env: EnvOverrides,

    #[arg(long)]
    target_version: String,

    #[arg(long, default_value = "cargo")]
    cargo_binary: String,

    #[arg(long, default_value = "crates-io")]
    registry: String,
}

#[derive(Debug, Clone, Args)]
pub struct RollbackArgs {
    #[command(flatten)]
    env: EnvOverrides,

    #[arg(long, default_value = "cargo")]
    cargo_binary: String,

    #[arg(long, default_value = "crates-io")]
    registry: String,
}

#[derive(Debug, Clone, Args)]
pub struct RepairArgs {
    #[command(flatten)]
    env: EnvOverrides,

    #[arg(long, default_value = "cargo")]
    cargo_binary: String,

    #[arg(long, default_value = "crates-io")]
    registry: String,
}

#[derive(Debug, Clone, Args)]
pub struct RunTaskArgs {
    #[arg(long)]
    task_id: String,

    #[arg(long)]
    db_path: PathBuf,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct BridgeEnvValues {
    bridge_token: String,
    listen_addr: String,
    runtime_limit: usize,
    db_path: PathBuf,
    codex_home: Option<PathBuf>,
    codex_binary: String,
    launch_path: String,
}

#[derive(Debug, Clone)]
struct ManagedPaths {
    share_dir: PathBuf,
    releases_dir: PathBuf,
    current_link: PathBuf,
    config_dir: PathBuf,
    systemd_user_dir: PathBuf,
    state_dir: PathBuf,
    env_file: PathBuf,
    unit_file: PathBuf,
    install_record_file: PathBuf,
    bridge_db_path: PathBuf,
}

impl ManagedPaths {
    fn new(home_dir: PathBuf) -> Self {
        let share_dir = home_dir.join(".local/share/codex-mobile");
        let config_dir = home_dir.join(".config/codex-mobile");
        let systemd_user_dir = home_dir.join(".config/systemd/user");
        let state_dir = home_dir.join(".local/state/codex-mobile");
        Self {
            current_link: share_dir.join("current"),
            releases_dir: share_dir.join("releases"),
            env_file: config_dir.join("bridge.env"),
            unit_file: systemd_user_dir.join(SERVICE_NAME),
            install_record_file: state_dir.join("install.json"),
            bridge_db_path: state_dir.join("bridge.db"),
            share_dir,
            config_dir,
            systemd_user_dir,
            state_dir,
        }
    }

    fn release_root_for_version(&self, version: &str) -> PathBuf {
        self.releases_dir.join(format!("{CRATE_NAME}-{version}"))
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
struct InstallRecord {
    install_state: String,
    current_artifact_id: Option<String>,
    current_version: Option<String>,
    current_build_hash: Option<String>,
    current_sha256: Option<String>,
    current_protocol_version: Option<u32>,
    current_release_path: Option<String>,
    previous_artifact_id: Option<String>,
    previous_version: Option<String>,
    previous_build_hash: Option<String>,
    previous_sha256: Option<String>,
    previous_protocol_version: Option<u32>,
    previous_release_path: Option<String>,
    last_operation: Option<String>,
    last_operation_status: Option<String>,
    last_operation_at_ms: i64,
    installed_at_ms: i64,
    updated_at_ms: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ReleaseMetadata {
    artifact_id: String,
    version: String,
    build_hash: String,
    sha256: String,
    protocol_version: u32,
    release_root: String,
    executable_path: String,
}

pub fn run(cli: ManageCli) -> Result<()> {
    match cli.command {
        ManageCommand::Activate(args) => commands::run_activate(args),
        ManageCommand::SelfUpdate(args) => commands::run_self_update(args),
        ManageCommand::Rollback(args) => commands::run_rollback(args),
        ManageCommand::Repair(args) => commands::run_repair(args),
        ManageCommand::Metadata => commands::print_metadata(),
        ManageCommand::RunTask(args) => tasks::run_bridge_management_task(args),
    }
}

pub fn spawn_bridge_management_task(task_id: &str, db_path: &Path) -> Result<()> {
    tasks::spawn_bridge_management_task(task_id, db_path)
}

pub fn current_bridge_management_snapshot(
    db_path: &Path,
) -> Result<crate::bridge_protocol::ManagedBridgeSnapshot> {
    tasks::current_bridge_management_snapshot(db_path)
}

pub fn inspect_remote_state(
    db_path: &Path,
) -> Result<crate::bridge_protocol::RemoteInspectionReport> {
    tasks::inspect_remote_state(db_path)
}