things3_cli/
lib.rs

1//! Things CLI library
2//! This module provides real-time updates and progress tracking capabilities
3
4pub 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;
26// pub mod thread_safe_db; // Removed - ThingsDatabase is now Send + Sync
27pub 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    /// Database path (defaults to Things 3 default location)
43    #[arg(long, short)]
44    pub database: Option<PathBuf>,
45
46    /// Fall back to default database path if specified path doesn't exist
47    #[arg(long)]
48    pub fallback_to_default: bool,
49
50    /// Verbose output
51    #[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    /// Get inbox tasks
61    Inbox {
62        /// Limit number of results
63        #[arg(long, short)]
64        limit: Option<usize>,
65    },
66    /// Get today's tasks
67    Today {
68        /// Limit number of results
69        #[arg(long, short)]
70        limit: Option<usize>,
71    },
72    /// Get projects
73    Projects {
74        /// Filter by area UUID
75        #[arg(long)]
76        area: Option<String>,
77        /// Limit number of results
78        #[arg(long, short)]
79        limit: Option<usize>,
80    },
81    /// Get areas
82    Areas {
83        /// Limit number of results
84        #[arg(long, short)]
85        limit: Option<usize>,
86    },
87    /// Search tasks
88    Search {
89        /// Search query
90        query: String,
91        /// Limit number of results
92        #[arg(long, short)]
93        limit: Option<usize>,
94    },
95    /// Start MCP server mode
96    #[cfg(feature = "mcp-server")]
97    Mcp,
98    /// Health check
99    Health,
100    /// Start health check server
101    #[cfg(feature = "observability")]
102    HealthServer {
103        /// Port to listen on
104        #[arg(long, short, default_value = "8080")]
105        port: u16,
106    },
107    /// Start monitoring dashboard
108    #[cfg(feature = "observability")]
109    Dashboard {
110        /// Port to listen on
111        #[arg(long, short, default_value = "3000")]
112        port: u16,
113    },
114    /// Start WebSocket server for real-time updates
115    Server {
116        /// Port to listen on
117        #[arg(long, short, default_value = "8080")]
118        port: u16,
119    },
120    /// Watch for real-time updates
121    Watch {
122        /// WebSocket server URL
123        #[arg(long, short, default_value = "ws://127.0.0.1:8080")]
124        url: String,
125    },
126    /// Validate real-time features health
127    Validate,
128    /// Bulk operations with progress tracking
129    Bulk {
130        #[command(subcommand)]
131        operation: BulkOperation,
132    },
133}
134
135#[derive(Subcommand, Debug, PartialEq, Eq)]
136pub enum BulkOperation {
137    /// Export all tasks with progress tracking
138    Export {
139        /// Export format (json, csv, xml)
140        #[arg(long, short, default_value = "json")]
141        format: String,
142    },
143    /// Update multiple tasks status
144    UpdateStatus {
145        /// Task IDs to update (comma-separated)
146        task_ids: String,
147        /// New status (completed, cancelled, trashed, incomplete)
148        status: String,
149    },
150    /// Search and process tasks
151    SearchAndProcess {
152        /// Search query
153        query: String,
154    },
155}
156
157/// Print tasks to the given writer
158///
159/// # Examples
160///
161/// ```no_run
162/// use things3_cli::print_tasks;
163/// use things3_core::ThingsDatabase;
164/// use std::io;
165///
166/// # async fn example() -> things3_core::Result<()> {
167/// let db = ThingsDatabase::new(std::path::Path::new("test.db")).await?;
168/// let tasks = db.get_inbox(Some(10)).await?;
169/// print_tasks(&db, &tasks, &mut io::stdout())?;
170/// # Ok(())
171/// # }
172/// ```
173///
174/// # Errors
175/// Returns an error if writing fails
176pub 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
203/// Print projects to the given writer
204///
205/// # Examples
206///
207/// ```no_run
208/// use things3_cli::print_projects;
209/// use things3_core::ThingsDatabase;
210/// use std::io;
211///
212/// # async fn example() -> things3_core::Result<()> {
213/// let db = ThingsDatabase::new(std::path::Path::new("test.db")).await?;
214/// let projects = db.get_projects(None).await?;
215/// print_projects(&db, &projects, &mut io::stdout())?;
216/// # Ok(())
217/// # }
218/// ```
219///
220/// # Errors
221/// Returns an error if writing fails
222pub 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
249/// Print areas to the given writer
250///
251/// # Examples
252///
253/// ```no_run
254/// use things3_cli::print_areas;
255/// use things3_core::ThingsDatabase;
256/// use std::io;
257///
258/// # async fn example() -> things3_core::Result<()> {
259/// let db = ThingsDatabase::new(std::path::Path::new("test.db")).await?;
260/// let areas = db.get_areas().await?;
261/// print_areas(&db, &areas, &mut io::stdout())?;
262/// # Ok(())
263/// # }
264/// ```
265///
266/// # Errors
267/// Returns an error if writing fails
268pub 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
292/// Perform a health check on the database
293///
294/// # Examples
295///
296/// ```no_run
297/// use things3_cli::health_check;
298/// use things3_core::ThingsDatabase;
299///
300/// # async fn example() -> things3_core::Result<()> {
301/// let db = ThingsDatabase::new(std::path::Path::new("test.db")).await?;
302/// health_check(&db).await?;
303/// # Ok(())
304/// # }
305/// ```
306///
307/// # Errors
308/// Returns an error if the database is not accessible
309pub async fn health_check(db: &ThingsDatabase) -> Result<()> {
310    println!("🔍 Checking Things 3 database connection...");
311
312    // Check if database is connected
313    if !db.is_connected().await {
314        return Err(things3_core::ThingsError::unknown(
315            "Database is not connected".to_string(),
316        ));
317    }
318
319    // Get database statistics
320    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
331// Temporarily disabled during SQLx migration
332// /// Start the MCP server
333// ///
334// /// # Errors
335// /// Returns an error if the server fails to start
336// pub fn start_mcp_server(db: Arc<SqlxThingsDatabase>, config: ThingsConfig) -> Result<()> {
337//     println!("🚀 Starting MCP server...");
338//     println!("🚧 MCP server is temporarily disabled during SQLx migration");
339//     Err(things3_core::ThingsError::unknown("MCP server temporarily disabled".to_string()))
340// }
341
342/// Start the WebSocket server for real-time updates
343///
344/// # Examples
345///
346/// ```no_run
347/// use things3_cli::start_websocket_server;
348///
349/// # async fn example() -> things3_core::Result<()> {
350/// // Start WebSocket server on port 8080
351/// start_websocket_server(8080).await?;
352/// # Ok(())
353/// # }
354/// ```
355///
356/// # Errors
357/// Returns an error if the server fails to start
358pub 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    // Start the server
365    server
366        .start()
367        .await
368        .map_err(|e| things3_core::ThingsError::unknown(e.to_string()))?;
369
370    Ok(())
371}
372
373/// Watch for real-time updates via WebSocket
374///
375/// # Examples
376///
377/// ```
378/// use things3_cli::watch_updates;
379///
380/// # fn example() -> things3_core::Result<()> {
381/// // Connect to WebSocket server
382/// watch_updates("ws://127.0.0.1:8080")?;
383/// # Ok(())
384/// # }
385/// ```
386///
387/// # Errors
388/// Returns an error if the connection fails
389pub fn watch_updates(url: &str) -> Result<()> {
390    println!("👀 Connecting to WebSocket server at {url}...");
391
392    // In a real implementation, this would connect to the WebSocket server
393    // For now, we'll just print that it would connect
394    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        // Note: We can't actually run start_mcp_server in a test because it's an infinite
427        // loop that reads from stdin. Instead, we verify the server can be created.
428        let _server = crate::mcp::ThingsMcpServer::new(db.into(), config);
429        // Server created successfully
430    }
431
432    #[test]
433    fn test_start_websocket_server_function_exists() {
434        // Test that the function exists and can be referenced
435        // We don't actually call it as it would hang
436        // Test that function exists and can be referenced
437        // Function reference test passed if we get here
438    }
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}