things3_cli/lib.rs
1//! Things CLI library
2//! This module provides real-time updates and progress tracking capabilities
3#![allow(deprecated)]
4
5pub mod bulk_operations;
6
7#[cfg(feature = "observability")]
8pub mod dashboard;
9
10pub mod events;
11
12#[cfg(feature = "observability")]
13pub mod health;
14
15pub mod logging;
16
17#[cfg(feature = "mcp-server")]
18pub mod mcp;
19
20#[cfg(feature = "observability")]
21pub mod metrics;
22
23#[cfg(feature = "observability")]
24pub mod monitoring;
25
26pub mod progress;
27// pub mod thread_safe_db; // Removed - ThingsDatabase is now Send + Sync
28pub mod websocket;
29
30use crate::events::EventBroadcaster;
31use crate::websocket::WebSocketServer;
32use clap::{Parser, Subcommand};
33use std::io::Write;
34use std::path::PathBuf;
35use std::sync::Arc;
36use things3_core::{Result, ThingsDatabase};
37
38#[derive(Parser, Debug)]
39#[command(name = "things3")]
40#[command(about = "Things 3 CLI with integrated MCP server")]
41#[command(version)]
42pub struct Cli {
43 /// Database path (defaults to Things 3 default location)
44 #[arg(long, short)]
45 pub database: Option<PathBuf>,
46
47 /// Fall back to default database path if specified path doesn't exist
48 #[arg(long)]
49 pub fallback_to_default: bool,
50
51 /// Verbose output
52 #[arg(long, short)]
53 pub verbose: bool,
54
55 /// Use the deprecated direct-SQLite mutation backend instead of AppleScript.
56 ///
57 /// CulturedCode warns against direct database writes
58 /// (https://culturedcode.com/things/support/articles/5510170/). This flag
59 /// re-enables the deprecated SqlxBackend and is required to use
60 /// `restore_database`. Will be removed in a future release.
61 #[arg(long, env = "THINGS_UNSAFE_DIRECT_DB")]
62 pub unsafe_direct_db: bool,
63
64 #[command(subcommand)]
65 pub command: Commands,
66}
67
68#[derive(Subcommand, Debug, PartialEq, Eq)]
69pub enum Commands {
70 /// Get inbox tasks
71 Inbox {
72 /// Limit number of results
73 #[arg(long, short)]
74 limit: Option<usize>,
75 },
76 /// Get today's tasks
77 Today {
78 /// Limit number of results
79 #[arg(long, short)]
80 limit: Option<usize>,
81 },
82 /// Get projects
83 Projects {
84 /// Filter by area UUID
85 #[arg(long)]
86 area: Option<String>,
87 /// Limit number of results
88 #[arg(long, short)]
89 limit: Option<usize>,
90 },
91 /// Get areas
92 Areas {
93 /// Limit number of results
94 #[arg(long, short)]
95 limit: Option<usize>,
96 },
97 /// Search tasks
98 Search {
99 /// Search query
100 query: String,
101 /// Limit number of results
102 #[arg(long, short)]
103 limit: Option<usize>,
104 },
105 /// Start MCP server mode
106 #[cfg(feature = "mcp-server")]
107 Mcp,
108 /// Health check
109 Health,
110 /// Start health check server
111 #[cfg(feature = "observability")]
112 HealthServer {
113 /// Port to listen on
114 #[arg(long, short, default_value = "8080")]
115 port: u16,
116 },
117 /// Start monitoring dashboard
118 #[cfg(feature = "observability")]
119 Dashboard {
120 /// Port to listen on
121 #[arg(long, short, default_value = "3000")]
122 port: u16,
123 },
124 /// Start WebSocket server for real-time updates
125 Server {
126 /// Port to listen on
127 #[arg(long, short, default_value = "8080")]
128 port: u16,
129 },
130 /// Watch for real-time updates
131 Watch {
132 /// WebSocket server URL
133 #[arg(long, short, default_value = "ws://127.0.0.1:8080")]
134 url: String,
135 },
136 /// Validate real-time features health
137 Validate,
138 /// Bulk operations with progress tracking
139 Bulk {
140 #[command(subcommand)]
141 operation: BulkOperation,
142 },
143}
144
145#[derive(Subcommand, Debug, PartialEq, Eq)]
146pub enum BulkOperation {
147 /// Export all tasks with progress tracking
148 Export {
149 /// Export format (json, csv, xml)
150 #[arg(long, short, default_value = "json")]
151 format: String,
152 },
153 /// Update multiple tasks status
154 UpdateStatus {
155 /// Task IDs to update (comma-separated)
156 task_ids: String,
157 /// New status (completed, cancelled, trashed, incomplete)
158 status: String,
159 },
160 /// Search and process tasks
161 SearchAndProcess {
162 /// Search query
163 query: String,
164 },
165}
166
167/// Print tasks to the given writer
168///
169/// # Examples
170///
171/// ```no_run
172/// use things3_cli::print_tasks;
173/// use things3_core::ThingsDatabase;
174/// use std::io;
175///
176/// # async fn example() -> things3_core::Result<()> {
177/// let db = ThingsDatabase::new(std::path::Path::new("test.db")).await?;
178/// let tasks = db.get_inbox(Some(10)).await?;
179/// print_tasks(&db, &tasks, &mut io::stdout())?;
180/// # Ok(())
181/// # }
182/// ```
183///
184/// # Errors
185/// Returns an error if writing fails
186pub fn print_tasks<W: Write>(
187 _db: &ThingsDatabase,
188 tasks: &[things3_core::Task],
189 writer: &mut W,
190) -> Result<()> {
191 if tasks.is_empty() {
192 writeln!(writer, "No tasks found")?;
193 return Ok(());
194 }
195
196 writeln!(writer, "Found {} tasks:", tasks.len())?;
197 for task in tasks {
198 writeln!(writer, " • {} ({:?})", task.title, task.task_type)?;
199 if let Some(notes) = &task.notes {
200 writeln!(writer, " Notes: {notes}")?;
201 }
202 if let Some(deadline) = &task.deadline {
203 writeln!(writer, " Deadline: {deadline}")?;
204 }
205 if !task.tags.is_empty() {
206 writeln!(writer, " Tags: {}", task.tags.join(", "))?;
207 }
208 writeln!(writer)?;
209 }
210 Ok(())
211}
212
213/// Print projects to the given writer
214///
215/// # Examples
216///
217/// ```no_run
218/// use things3_cli::print_projects;
219/// use things3_core::ThingsDatabase;
220/// use std::io;
221///
222/// # async fn example() -> things3_core::Result<()> {
223/// let db = ThingsDatabase::new(std::path::Path::new("test.db")).await?;
224/// let projects = db.get_projects(None).await?;
225/// print_projects(&db, &projects, &mut io::stdout())?;
226/// # Ok(())
227/// # }
228/// ```
229///
230/// # Errors
231/// Returns an error if writing fails
232pub fn print_projects<W: Write>(
233 _db: &ThingsDatabase,
234 projects: &[things3_core::Project],
235 writer: &mut W,
236) -> Result<()> {
237 if projects.is_empty() {
238 writeln!(writer, "No projects found")?;
239 return Ok(());
240 }
241
242 writeln!(writer, "Found {} projects:", projects.len())?;
243 for project in projects {
244 writeln!(writer, " • {} ({:?})", project.title, project.status)?;
245 if let Some(notes) = &project.notes {
246 writeln!(writer, " Notes: {notes}")?;
247 }
248 if let Some(deadline) = &project.deadline {
249 writeln!(writer, " Deadline: {deadline}")?;
250 }
251 if !project.tags.is_empty() {
252 writeln!(writer, " Tags: {}", project.tags.join(", "))?;
253 }
254 writeln!(writer)?;
255 }
256 Ok(())
257}
258
259/// Print areas to the given writer
260///
261/// # Examples
262///
263/// ```no_run
264/// use things3_cli::print_areas;
265/// use things3_core::ThingsDatabase;
266/// use std::io;
267///
268/// # async fn example() -> things3_core::Result<()> {
269/// let db = ThingsDatabase::new(std::path::Path::new("test.db")).await?;
270/// let areas = db.get_areas().await?;
271/// print_areas(&db, &areas, &mut io::stdout())?;
272/// # Ok(())
273/// # }
274/// ```
275///
276/// # Errors
277/// Returns an error if writing fails
278pub fn print_areas<W: Write>(
279 _db: &ThingsDatabase,
280 areas: &[things3_core::Area],
281 writer: &mut W,
282) -> Result<()> {
283 if areas.is_empty() {
284 writeln!(writer, "No areas found")?;
285 return Ok(());
286 }
287
288 writeln!(writer, "Found {} areas:", areas.len())?;
289 for area in areas {
290 writeln!(writer, " • {}", area.title)?;
291 if let Some(notes) = &area.notes {
292 writeln!(writer, " Notes: {notes}")?;
293 }
294 if !area.tags.is_empty() {
295 writeln!(writer, " Tags: {}", area.tags.join(", "))?;
296 }
297 writeln!(writer)?;
298 }
299 Ok(())
300}
301
302/// Perform a health check on the database
303///
304/// # Examples
305///
306/// ```no_run
307/// use things3_cli::health_check;
308/// use things3_core::ThingsDatabase;
309///
310/// # async fn example() -> things3_core::Result<()> {
311/// let db = ThingsDatabase::new(std::path::Path::new("test.db")).await?;
312/// health_check(&db).await?;
313/// # Ok(())
314/// # }
315/// ```
316///
317/// # Errors
318/// Returns an error if the database is not accessible
319pub async fn health_check(db: &ThingsDatabase) -> Result<()> {
320 println!("🔍 Checking Things 3 database connection...");
321
322 // Check if database is connected
323 if !db.is_connected().await {
324 return Err(things3_core::ThingsError::unknown(
325 "Database is not connected".to_string(),
326 ));
327 }
328
329 // Get database statistics
330 let stats = db.get_stats().await?;
331 println!("✅ Database connection successful!");
332 println!(
333 " Found {} tasks, {} projects, {} areas",
334 stats.task_count, stats.project_count, stats.area_count
335 );
336
337 println!("🎉 All systems operational!");
338 Ok(())
339}
340
341// Temporarily disabled during SQLx migration
342// /// Start the MCP server
343// ///
344// /// # Errors
345// /// Returns an error if the server fails to start
346// pub fn start_mcp_server(db: Arc<SqlxThingsDatabase>, config: ThingsConfig) -> Result<()> {
347// println!("🚀 Starting MCP server...");
348// println!("🚧 MCP server is temporarily disabled during SQLx migration");
349// Err(things3_core::ThingsError::unknown("MCP server temporarily disabled".to_string()))
350// }
351
352/// Start the WebSocket server for real-time updates
353///
354/// # Examples
355///
356/// ```no_run
357/// use things3_cli::start_websocket_server;
358///
359/// # async fn example() -> things3_core::Result<()> {
360/// // Start WebSocket server on port 8080
361/// start_websocket_server(8080).await?;
362/// # Ok(())
363/// # }
364/// ```
365///
366/// # Errors
367/// Returns an error if the server fails to start
368pub async fn start_websocket_server(port: u16) -> Result<()> {
369 println!("🚀 Starting WebSocket server on port {port}...");
370
371 let server = WebSocketServer::new(port);
372 let _event_broadcaster = Arc::new(EventBroadcaster::new());
373
374 // Start the server
375 server
376 .start()
377 .await
378 .map_err(|e| things3_core::ThingsError::unknown(e.to_string()))?;
379
380 Ok(())
381}
382
383/// Watch for real-time updates via WebSocket
384///
385/// # Examples
386///
387/// ```
388/// use things3_cli::watch_updates;
389///
390/// # fn example() -> things3_core::Result<()> {
391/// // Connect to WebSocket server
392/// watch_updates("ws://127.0.0.1:8080")?;
393/// # Ok(())
394/// # }
395/// ```
396///
397/// # Errors
398/// Returns an error if the connection fails
399pub fn watch_updates(url: &str) -> Result<()> {
400 println!("👀 Connecting to WebSocket server at {url}...");
401
402 // In a real implementation, this would connect to the WebSocket server
403 // For now, we'll just print that it would connect
404 println!("✅ Would connect to WebSocket server");
405 println!(" (This is a placeholder - actual WebSocket client implementation would go here)");
406
407 Ok(())
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use things3_core::test_utils::create_test_database;
414 use tokio::runtime::Runtime;
415
416 #[test]
417 fn test_health_check() {
418 let temp_file = tempfile::NamedTempFile::new().unwrap();
419 let db_path = temp_file.path();
420 let rt = Runtime::new().unwrap();
421 rt.block_on(async { create_test_database(db_path).await.unwrap() });
422 let db = rt.block_on(async { ThingsDatabase::new(db_path).await.unwrap() });
423 let result = rt.block_on(async { health_check(&db).await });
424 assert!(result.is_ok());
425 }
426
427 #[tokio::test]
428 #[cfg(feature = "mcp-server")]
429 async fn test_start_mcp_server() {
430 let temp_file = tempfile::NamedTempFile::new().unwrap();
431 let db_path = temp_file.path();
432 create_test_database(db_path).await.unwrap();
433 let db = ThingsDatabase::new(db_path).await.unwrap();
434 let config = things3_core::ThingsConfig::default();
435
436 // Note: We can't actually run start_mcp_server in a test because it's an infinite
437 // loop that reads from stdin. Instead, we verify the server can be created.
438 let _server = crate::mcp::ThingsMcpServer::new(db.into(), config, true);
439 // Server created successfully
440 }
441
442 #[test]
443 fn test_start_websocket_server_function_exists() {
444 // Test that the function exists and can be referenced
445 // We don't actually call it as it would hang
446 // Test that function exists and can be referenced
447 // Function reference test passed if we get here
448 }
449
450 #[test]
451 fn test_watch_updates() {
452 let result = watch_updates("ws://127.0.0.1:8080");
453 assert!(result.is_ok());
454 }
455}