codex-mobile-bridge 0.3.7

Remote bridge and service manager for codex-mobile.
Documentation
use anyhow::Result;
use rusqlite::{Connection, OptionalExtension, params};

pub(super) fn ensure_column(
    conn: &Connection,
    table: &str,
    column: &str,
    definition: &str,
) -> Result<()> {
    let mut stmt = conn.prepare(&format!("PRAGMA table_info({table})"))?;
    let mut rows = stmt.query([])?;
    while let Some(row) = rows.next()? {
        let existing: String = row.get(1)?;
        if existing == column {
            return Ok(());
        }
    }

    conn.execute_batch(&format!(
        "ALTER TABLE {table} ADD COLUMN {column} {definition};"
    ))?;
    Ok(())
}

pub(super) fn ensure_thread_index_schema(conn: &Connection) -> Result<()> {
    if !table_exists(conn, "thread_index")? {
        create_thread_index_table(conn)?;
        return Ok(());
    }

    ensure_column(
        conn,
        "thread_index",
        "runtime_id",
        "TEXT NOT NULL DEFAULT 'primary'",
    )?;
    ensure_column(
        conn,
        "thread_index",
        "archived",
        "INTEGER NOT NULL DEFAULT 0",
    )?;

    if column_exists(conn, "thread_index", "workspace_id")?
        || column_exists(conn, "thread_index", "note")?
    {
        rebuild_thread_index_without_legacy_columns(conn)?;
    }

    Ok(())
}

fn create_thread_index_table(conn: &Connection) -> Result<()> {
    conn.execute_batch(
        "CREATE TABLE IF NOT EXISTS thread_index (
            thread_id TEXT PRIMARY KEY,
            runtime_id TEXT NOT NULL DEFAULT 'primary',
            name TEXT NULL,
            preview TEXT NOT NULL,
            cwd TEXT NOT NULL,
            status TEXT NOT NULL,
            model_provider TEXT NOT NULL,
            source TEXT NOT NULL,
            created_at_ms INTEGER NOT NULL,
            updated_at_ms INTEGER NOT NULL,
            is_loaded INTEGER NOT NULL,
            is_active INTEGER NOT NULL,
            archived INTEGER NOT NULL DEFAULT 0,
            raw_json TEXT NOT NULL
        );",
    )?;
    Ok(())
}

fn rebuild_thread_index_without_legacy_columns(conn: &Connection) -> Result<()> {
    conn.execute_batch(
        "ALTER TABLE thread_index RENAME TO thread_index_legacy;

        CREATE TABLE thread_index (
            thread_id TEXT PRIMARY KEY,
            runtime_id TEXT NOT NULL DEFAULT 'primary',
            name TEXT NULL,
            preview TEXT NOT NULL,
            cwd TEXT NOT NULL,
            status TEXT NOT NULL,
            model_provider TEXT NOT NULL,
            source TEXT NOT NULL,
            created_at_ms INTEGER NOT NULL,
            updated_at_ms INTEGER NOT NULL,
            is_loaded INTEGER NOT NULL,
            is_active INTEGER NOT NULL,
            archived INTEGER NOT NULL DEFAULT 0,
            raw_json TEXT NOT NULL
        );

        INSERT INTO thread_index (
            thread_id, runtime_id, name, preview, cwd, status,
            model_provider, source, created_at_ms, updated_at_ms, is_loaded,
            is_active, archived, raw_json
        )
        SELECT
            thread_id,
            COALESCE(runtime_id, 'primary'),
            name,
            preview,
            cwd,
            status,
            model_provider,
            source,
            created_at_ms,
            updated_at_ms,
            is_loaded,
            is_active,
            archived,
            raw_json
        FROM thread_index_legacy;

        DROP TABLE thread_index_legacy;",
    )?;
    Ok(())
}

pub(super) fn migrate_legacy_workspaces(conn: &Connection) -> Result<()> {
    if !table_exists(conn, "workspaces")? {
        return Ok(());
    }

    conn.execute_batch(
        "INSERT OR IGNORE INTO directory_bookmarks (path, display_name, created_at_ms, updated_at_ms)
         SELECT root_path, display_name, created_at_ms, updated_at_ms
         FROM workspaces;

         DROP TABLE workspaces;",
    )?;
    Ok(())
}

fn table_exists(conn: &Connection, table: &str) -> Result<bool> {
    let exists = conn
        .query_row(
            "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1 LIMIT 1",
            params![table],
            |_| Ok(()),
        )
        .optional()?
        .is_some();
    Ok(exists)
}

fn column_exists(conn: &Connection, table: &str, column: &str) -> Result<bool> {
    let mut stmt = conn.prepare(&format!("PRAGMA table_info({table})"))?;
    let mut rows = stmt.query([])?;
    while let Some(row) = rows.next()? {
        let existing: String = row.get(1)?;
        if existing == column {
            return Ok(true);
        }
    }
    Ok(false)
}