Skip to main content

flow_server/
lib.rs

1pub mod error;
2pub mod helpers;
3pub mod routes;
4pub mod sse;
5pub mod state;
6pub mod watcher;
7pub mod ws;
8
9pub use error::{AppError, AppResult};
10pub use state::{AppState, MetadataCache};
11
12use axum::{routing::{delete, get, post}, Router};
13use flow_core::AgentConfig;
14use std::sync::Arc;
15use tokio::sync::{broadcast, RwLock};
16use tower_http::services::ServeDir;
17use tracing::info;
18
19/// Build the axum router with all routes
20pub fn build_router(state: Arc<AppState>) -> Router {
21    let mut app = Router::new()
22        .route("/api/sessions", get(routes::sessions::list_sessions))
23        .route("/api/sessions/:session_id", get(routes::sessions::get_session))
24        .route("/api/tasks/all", get(routes::tasks::get_all_tasks))
25        .route("/api/tasks/:session_id/:task_id/note", post(routes::tasks::add_note))
26        .route("/api/tasks/:session_id/:task_id", delete(routes::tasks::delete_task))
27        .route("/api/events", get(sse::sse_handler))
28        .route("/api/ws", get(ws::ws_handler))
29        .route("/api/theme", get(routes::theme::get_theme))
30        .route("/api/theme", post(routes::theme::set_theme));
31
32    // Add feature routes if database is available
33    if state.db.is_some() {
34        app = app.nest("/api/features", routes::features::feature_routes());
35    }
36
37    app.with_state(state)
38}
39
40/// Run the axum server with the given configuration
41#[allow(clippy::cognitive_complexity)]
42pub async fn run_server(config: AgentConfig) -> flow_core::Result<()> {
43    let tasks_dir = config.tasks_dir();
44    let projects_dir = config.projects_dir();
45
46    info!("Tasks directory: {}", tasks_dir.display());
47    info!("Projects directory: {}", projects_dir.display());
48
49    // Determine public directory
50    let public_dir = config.public_dir.as_ref().map_or_else(
51        || {
52            // Default to ../public relative to binary, or ./public as fallback
53            std::env::current_exe()
54                .ok()
55                .and_then(|p| p.parent().map(std::path::Path::to_path_buf))
56                .map_or_else(
57                    || std::path::PathBuf::from("public"),
58                    |exe| {
59                        let candidate = exe.join("..").join("public");
60                        if candidate.exists() {
61                            candidate
62                        } else {
63                            std::path::PathBuf::from("public")
64                        }
65                    },
66                )
67        },
68        std::clone::Clone::clone,
69    );
70
71    info!("Public directory: {}", public_dir.display());
72
73    // Create broadcast channel for SSE/WS (buffer 256 messages)
74    let (tx, _) = broadcast::channel::<String>(256);
75
76    // Database is optional - can be added later for feature management
77    let db = None;
78
79    let state = Arc::new(AppState {
80        tasks_dir: tasks_dir.clone(),
81        projects_dir: projects_dir.clone(),
82        tx: tx.clone(),
83        metadata_cache: RwLock::new(MetadataCache::new()),
84        db,
85    });
86
87    // Set up file watchers (keep handles alive)
88    let _watchers = watcher::setup_file_watcher(&tasks_dir, &projects_dir, tx);
89
90    // Build router with fallback to serve static files
91    let app = build_router(state.clone())
92        .fallback_service(ServeDir::new(&public_dir));
93
94    let addr = std::net::SocketAddr::from(([0, 0, 0, 0], config.port));
95    info!("Server running at http://localhost:{}", config.port);
96
97    // Open browser if requested (cross-platform)
98    if config.open_browser {
99        let url = format!("http://localhost:{}", config.port);
100        tokio::spawn(async move {
101            let _ = open_browser(&url).await;
102        });
103    }
104
105    let listener = tokio::net::TcpListener::bind(addr)
106        .await
107        .map_err(flow_core::FlowError::Io)?;
108
109    axum::serve(listener, app)
110        .await
111        .map_err(|e| flow_core::FlowError::Io(std::io::Error::other(e)))?;
112
113    Ok(())
114}
115
116/// Open a URL in the default browser (cross-platform).
117async fn open_browser(url: &str) -> Result<(), std::io::Error> {
118    #[cfg(target_os = "macos")]
119    {
120        tokio::process::Command::new("open")
121            .arg(url)
122            .status()
123            .await?;
124    }
125    #[cfg(target_os = "linux")]
126    {
127        tokio::process::Command::new("xdg-open")
128            .arg(url)
129            .status()
130            .await?;
131    }
132    #[cfg(target_os = "windows")]
133    {
134        tokio::process::Command::new("cmd")
135            .args(["/C", "start", "", url])
136            .status()
137            .await?;
138    }
139    Ok(())
140}