finery 0.5.0

Keyboard-first Jira backlog and Composer with optional MCP access
Documentation
#![cfg_attr(test, allow(unused_must_use))]

mod app;
mod app_settings;
pub mod cli;
mod components;
mod jira;
mod mcp;
mod pages;
mod service;
mod speed_reader_settings;
mod storage;
mod store;

use std::{net::SocketAddr, sync::mpsc::Sender, thread};

pub fn run() -> Result<(), Box<dyn std::error::Error>> {
    let (service, change_sets) = service::AppService::initialize()?;
    tuicore::TreeApp::new(app::root(service.clone(), change_sets)).run()?;
    service.flush()?;
    Ok(())
}

pub fn run_mcp() -> Result<(), Box<dyn std::error::Error>> {
    let (service, _) = service::AppService::initialize()?;
    tokio::runtime::Runtime::new()?.block_on(mcp::run_stdio(service))
}

pub fn run_http(bind: SocketAddr) -> Result<(), Box<dyn std::error::Error>> {
    let (service, _) = service::AppService::initialize()?;
    tokio::runtime::Runtime::new()?.block_on(mcp::run_http(service, bind))
}

pub fn run_http_with_startup(
    bind: SocketAddr,
    startup: Sender<Result<(), String>>,
) -> Result<(), Box<dyn std::error::Error>> {
    let (service, _) = match service::AppService::initialize() {
        Ok(service) => service,
        Err(error) => {
            let _ = startup.send(Err(error.to_string()));
            return Err(error);
        }
    };
    let runtime = match tokio::runtime::Runtime::new() {
        Ok(runtime) => runtime,
        Err(error) => {
            let _ = startup.send(Err(error.to_string()));
            return Err(Box::new(error));
        }
    };
    runtime.block_on(mcp::run_http_with_startup(service, bind, startup))
}

pub fn run_dev(bind: SocketAddr) -> Result<(), Box<dyn std::error::Error>> {
    let (service, change_sets) = service::AppService::initialize()?;
    let (startup_tx, startup_rx) = std::sync::mpsc::channel();
    let http_service = service.clone();
    thread::Builder::new()
        .name("finery-mcp-http".into())
        .spawn(move || {
            let runtime = tokio::runtime::Runtime::new();
            match runtime {
                Ok(runtime) => {
                    if let Err(error) =
                        runtime.block_on(mcp::run_http_with_startup(http_service, bind, startup_tx))
                    {
                        eprintln!("HTTP MCP stopped: {error}");
                    }
                }
                Err(error) => {
                    let _ = startup_tx.send(Err(error.to_string()));
                }
            }
        })?;
    startup_rx
        .recv()
        .map_err(|_| "HTTP MCP startup thread exited without status")?
        .map_err(|error| format!("HTTP MCP startup failed: {error}"))?;
    tuicore::TreeApp::new(app::root(service.clone(), change_sets)).run()?;
    service.flush()?;
    Ok(())
}