1pub mod bulk_operations;
5
6#[cfg(feature = "observability")]
7pub mod dashboard;
8
9pub mod events;
10
11#[cfg(feature = "observability")]
12pub mod health;
13
14pub mod logging;
15
16#[cfg(feature = "mcp-server")]
17pub mod mcp;
18
19#[cfg(feature = "observability")]
20pub mod metrics;
21
22#[cfg(feature = "observability")]
23pub mod monitoring;
24
25pub mod progress;
26pub mod websocket;
28
29use crate::events::EventBroadcaster;
30use crate::websocket::WebSocketServer;
31use clap::{Parser, Subcommand};
32use std::io::Write;
33use std::path::PathBuf;
34use std::sync::Arc;
35use things3_core::{Result, ThingsDatabase};
36
37#[derive(Parser, Debug)]
38#[command(name = "things3")]
39#[command(about = "Things 3 CLI with integrated MCP server")]
40#[command(version)]
41pub struct Cli {
42 #[arg(long, short)]
44 pub database: Option<PathBuf>,
45
46 #[arg(long)]
48 pub fallback_to_default: bool,
49
50 #[arg(long, short)]
52 pub verbose: bool,
53
54 #[command(subcommand)]
55 pub command: Commands,
56}
57
58#[derive(Subcommand, Debug, PartialEq, Eq)]
59pub enum Commands {
60 Inbox {
62 #[arg(long, short)]
64 limit: Option<usize>,
65 },
66 Today {
68 #[arg(long, short)]
70 limit: Option<usize>,
71 },
72 Projects {
74 #[arg(long)]
76 area: Option<String>,
77 #[arg(long, short)]
79 limit: Option<usize>,
80 },
81 Areas {
83 #[arg(long, short)]
85 limit: Option<usize>,
86 },
87 Search {
89 query: String,
91 #[arg(long, short)]
93 limit: Option<usize>,
94 },
95 #[cfg(feature = "mcp-server")]
97 Mcp,
98 Health,
100 #[cfg(feature = "observability")]
102 HealthServer {
103 #[arg(long, short, default_value = "8080")]
105 port: u16,
106 },
107 #[cfg(feature = "observability")]
109 Dashboard {
110 #[arg(long, short, default_value = "3000")]
112 port: u16,
113 },
114 Server {
116 #[arg(long, short, default_value = "8080")]
118 port: u16,
119 },
120 Watch {
122 #[arg(long, short, default_value = "ws://127.0.0.1:8080")]
124 url: String,
125 },
126 Validate,
128 Bulk {
130 #[command(subcommand)]
131 operation: BulkOperation,
132 },
133}
134
135#[derive(Subcommand, Debug, PartialEq, Eq)]
136pub enum BulkOperation {
137 Export {
139 #[arg(long, short, default_value = "json")]
141 format: String,
142 },
143 UpdateStatus {
145 task_ids: String,
147 status: String,
149 },
150 SearchAndProcess {
152 query: String,
154 },
155}
156
157pub fn print_tasks<W: Write>(
177 _db: &ThingsDatabase,
178 tasks: &[things3_core::Task],
179 writer: &mut W,
180) -> Result<()> {
181 if tasks.is_empty() {
182 writeln!(writer, "No tasks found")?;
183 return Ok(());
184 }
185
186 writeln!(writer, "Found {} tasks:", tasks.len())?;
187 for task in tasks {
188 writeln!(writer, " • {} ({:?})", task.title, task.task_type)?;
189 if let Some(notes) = &task.notes {
190 writeln!(writer, " Notes: {notes}")?;
191 }
192 if let Some(deadline) = &task.deadline {
193 writeln!(writer, " Deadline: {deadline}")?;
194 }
195 if !task.tags.is_empty() {
196 writeln!(writer, " Tags: {}", task.tags.join(", "))?;
197 }
198 writeln!(writer)?;
199 }
200 Ok(())
201}
202
203pub fn print_projects<W: Write>(
223 _db: &ThingsDatabase,
224 projects: &[things3_core::Project],
225 writer: &mut W,
226) -> Result<()> {
227 if projects.is_empty() {
228 writeln!(writer, "No projects found")?;
229 return Ok(());
230 }
231
232 writeln!(writer, "Found {} projects:", projects.len())?;
233 for project in projects {
234 writeln!(writer, " • {} ({:?})", project.title, project.status)?;
235 if let Some(notes) = &project.notes {
236 writeln!(writer, " Notes: {notes}")?;
237 }
238 if let Some(deadline) = &project.deadline {
239 writeln!(writer, " Deadline: {deadline}")?;
240 }
241 if !project.tags.is_empty() {
242 writeln!(writer, " Tags: {}", project.tags.join(", "))?;
243 }
244 writeln!(writer)?;
245 }
246 Ok(())
247}
248
249pub fn print_areas<W: Write>(
269 _db: &ThingsDatabase,
270 areas: &[things3_core::Area],
271 writer: &mut W,
272) -> Result<()> {
273 if areas.is_empty() {
274 writeln!(writer, "No areas found")?;
275 return Ok(());
276 }
277
278 writeln!(writer, "Found {} areas:", areas.len())?;
279 for area in areas {
280 writeln!(writer, " • {}", area.title)?;
281 if let Some(notes) = &area.notes {
282 writeln!(writer, " Notes: {notes}")?;
283 }
284 if !area.tags.is_empty() {
285 writeln!(writer, " Tags: {}", area.tags.join(", "))?;
286 }
287 writeln!(writer)?;
288 }
289 Ok(())
290}
291
292pub async fn health_check(db: &ThingsDatabase) -> Result<()> {
310 println!("🔍 Checking Things 3 database connection...");
311
312 if !db.is_connected().await {
314 return Err(things3_core::ThingsError::unknown(
315 "Database is not connected".to_string(),
316 ));
317 }
318
319 let stats = db.get_stats().await?;
321 println!("✅ Database connection successful!");
322 println!(
323 " Found {} tasks, {} projects, {} areas",
324 stats.task_count, stats.project_count, stats.area_count
325 );
326
327 println!("🎉 All systems operational!");
328 Ok(())
329}
330
331pub async fn start_websocket_server(port: u16) -> Result<()> {
359 println!("🚀 Starting WebSocket server on port {port}...");
360
361 let server = WebSocketServer::new(port);
362 let _event_broadcaster = Arc::new(EventBroadcaster::new());
363
364 server
366 .start()
367 .await
368 .map_err(|e| things3_core::ThingsError::unknown(e.to_string()))?;
369
370 Ok(())
371}
372
373pub fn watch_updates(url: &str) -> Result<()> {
390 println!("👀 Connecting to WebSocket server at {url}...");
391
392 println!("✅ Would connect to WebSocket server");
395 println!(" (This is a placeholder - actual WebSocket client implementation would go here)");
396
397 Ok(())
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403 use things3_core::test_utils::create_test_database;
404 use tokio::runtime::Runtime;
405
406 #[test]
407 fn test_health_check() {
408 let temp_file = tempfile::NamedTempFile::new().unwrap();
409 let db_path = temp_file.path();
410 let rt = Runtime::new().unwrap();
411 rt.block_on(async { create_test_database(db_path).await.unwrap() });
412 let db = rt.block_on(async { ThingsDatabase::new(db_path).await.unwrap() });
413 let result = rt.block_on(async { health_check(&db).await });
414 assert!(result.is_ok());
415 }
416
417 #[tokio::test]
418 #[cfg(feature = "mcp-server")]
419 async fn test_start_mcp_server() {
420 let temp_file = tempfile::NamedTempFile::new().unwrap();
421 let db_path = temp_file.path();
422 create_test_database(db_path).await.unwrap();
423 let db = ThingsDatabase::new(db_path).await.unwrap();
424 let config = things3_core::ThingsConfig::default();
425
426 let _server = crate::mcp::ThingsMcpServer::new(db.into(), config);
429 }
431
432 #[test]
433 fn test_start_websocket_server_function_exists() {
434 }
439
440 #[test]
441 fn test_watch_updates() {
442 let result = watch_updates("ws://127.0.0.1:8080");
443 assert!(result.is_ok());
444 }
445}