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
19pub 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 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#[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 let public_dir = config.public_dir.as_ref().map_or_else(
51 || {
52 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 let (tx, _) = broadcast::channel::<String>(256);
75
76 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 let _watchers = watcher::setup_file_watcher(&tasks_dir, &projects_dir, tx);
89
90 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 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
116async 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}