pub mod bulk_operations;
#[cfg(feature = "observability")]
pub mod dashboard;
pub mod events;
#[cfg(feature = "observability")]
pub mod health;
pub mod logging;
#[cfg(feature = "mcp-server")]
pub mod mcp;
#[cfg(feature = "observability")]
pub mod metrics;
#[cfg(feature = "observability")]
pub mod monitoring;
pub mod progress;
pub mod websocket;
use crate::events::EventBroadcaster;
use crate::websocket::WebSocketServer;
use clap::{Parser, Subcommand};
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use things3_core::{Result, ThingsDatabase};
#[derive(Parser, Debug)]
#[command(name = "things3")]
#[command(about = "Things 3 CLI with integrated MCP server")]
#[command(version)]
pub struct Cli {
#[arg(long, short)]
pub database: Option<PathBuf>,
#[arg(long)]
pub fallback_to_default: bool,
#[arg(long, short)]
pub verbose: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug, PartialEq, Eq)]
pub enum Commands {
Inbox {
#[arg(long, short)]
limit: Option<usize>,
},
Today {
#[arg(long, short)]
limit: Option<usize>,
},
Projects {
#[arg(long)]
area: Option<String>,
#[arg(long, short)]
limit: Option<usize>,
},
Areas {
#[arg(long, short)]
limit: Option<usize>,
},
Search {
query: String,
#[arg(long, short)]
limit: Option<usize>,
},
#[cfg(feature = "mcp-server")]
Mcp,
Health,
#[cfg(feature = "observability")]
HealthServer {
#[arg(long, short, default_value = "8080")]
port: u16,
},
#[cfg(feature = "observability")]
Dashboard {
#[arg(long, short, default_value = "3000")]
port: u16,
},
Server {
#[arg(long, short, default_value = "8080")]
port: u16,
},
Watch {
#[arg(long, short, default_value = "ws://127.0.0.1:8080")]
url: String,
},
Validate,
Bulk {
#[command(subcommand)]
operation: BulkOperation,
},
}
#[derive(Subcommand, Debug, PartialEq, Eq)]
pub enum BulkOperation {
Export {
#[arg(long, short, default_value = "json")]
format: String,
},
UpdateStatus {
task_ids: String,
status: String,
},
SearchAndProcess {
query: String,
},
}
pub fn print_tasks<W: Write>(
_db: &ThingsDatabase,
tasks: &[things3_core::Task],
writer: &mut W,
) -> Result<()> {
if tasks.is_empty() {
writeln!(writer, "No tasks found")?;
return Ok(());
}
writeln!(writer, "Found {} tasks:", tasks.len())?;
for task in tasks {
writeln!(writer, " • {} ({:?})", task.title, task.task_type)?;
if let Some(notes) = &task.notes {
writeln!(writer, " Notes: {notes}")?;
}
if let Some(deadline) = &task.deadline {
writeln!(writer, " Deadline: {deadline}")?;
}
if !task.tags.is_empty() {
writeln!(writer, " Tags: {}", task.tags.join(", "))?;
}
writeln!(writer)?;
}
Ok(())
}
pub fn print_projects<W: Write>(
_db: &ThingsDatabase,
projects: &[things3_core::Project],
writer: &mut W,
) -> Result<()> {
if projects.is_empty() {
writeln!(writer, "No projects found")?;
return Ok(());
}
writeln!(writer, "Found {} projects:", projects.len())?;
for project in projects {
writeln!(writer, " • {} ({:?})", project.title, project.status)?;
if let Some(notes) = &project.notes {
writeln!(writer, " Notes: {notes}")?;
}
if let Some(deadline) = &project.deadline {
writeln!(writer, " Deadline: {deadline}")?;
}
if !project.tags.is_empty() {
writeln!(writer, " Tags: {}", project.tags.join(", "))?;
}
writeln!(writer)?;
}
Ok(())
}
pub fn print_areas<W: Write>(
_db: &ThingsDatabase,
areas: &[things3_core::Area],
writer: &mut W,
) -> Result<()> {
if areas.is_empty() {
writeln!(writer, "No areas found")?;
return Ok(());
}
writeln!(writer, "Found {} areas:", areas.len())?;
for area in areas {
writeln!(writer, " • {}", area.title)?;
if let Some(notes) = &area.notes {
writeln!(writer, " Notes: {notes}")?;
}
if !area.tags.is_empty() {
writeln!(writer, " Tags: {}", area.tags.join(", "))?;
}
writeln!(writer)?;
}
Ok(())
}
pub async fn health_check(db: &ThingsDatabase) -> Result<()> {
println!("🔍 Checking Things 3 database connection...");
if !db.is_connected().await {
return Err(things3_core::ThingsError::unknown(
"Database is not connected".to_string(),
));
}
let stats = db.get_stats().await?;
println!("✅ Database connection successful!");
println!(
" Found {} tasks, {} projects, {} areas",
stats.task_count, stats.project_count, stats.area_count
);
println!("🎉 All systems operational!");
Ok(())
}
pub async fn start_websocket_server(port: u16) -> Result<()> {
println!("🚀 Starting WebSocket server on port {port}...");
let server = WebSocketServer::new(port);
let _event_broadcaster = Arc::new(EventBroadcaster::new());
server
.start()
.await
.map_err(|e| things3_core::ThingsError::unknown(e.to_string()))?;
Ok(())
}
pub fn watch_updates(url: &str) -> Result<()> {
println!("👀 Connecting to WebSocket server at {url}...");
println!("✅ Would connect to WebSocket server");
println!(" (This is a placeholder - actual WebSocket client implementation would go here)");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use things3_core::test_utils::create_test_database;
use tokio::runtime::Runtime;
#[test]
fn test_health_check() {
let temp_file = tempfile::NamedTempFile::new().unwrap();
let db_path = temp_file.path();
let rt = Runtime::new().unwrap();
rt.block_on(async { create_test_database(db_path).await.unwrap() });
let db = rt.block_on(async { ThingsDatabase::new(db_path).await.unwrap() });
let result = rt.block_on(async { health_check(&db).await });
assert!(result.is_ok());
}
#[tokio::test]
#[cfg(feature = "mcp-server")]
async fn test_start_mcp_server() {
let temp_file = tempfile::NamedTempFile::new().unwrap();
let db_path = temp_file.path();
create_test_database(db_path).await.unwrap();
let db = ThingsDatabase::new(db_path).await.unwrap();
let config = things3_core::ThingsConfig::default();
let _server = crate::mcp::ThingsMcpServer::new(db.into(), config);
}
#[test]
fn test_start_websocket_server_function_exists() {
}
#[test]
fn test_watch_updates() {
let result = watch_updates("ws://127.0.0.1:8080");
assert!(result.is_ok());
}
}