diff --git a/crates/bevy-tasks-cli/Cargo.toml b/crates/bevy-tasks-cli/Cargo.toml deleted file mode 100644 index 3671f0a..0000000 --- a/crates/bevy-tasks-cli/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "bevy-tasks-cli" -version = "0.1.0" -edition = "2021" -description = "CLI frontend for Bevy Tasks, a local-first task management app" -license = "GPL-3.0-or-later" -repository = "https://github.com/SteelDynamite/bevy-tasks" - -[[bin]] -name = "bevy-tasks" -path = "src/main.rs" - -[dependencies] -bevy-tasks-core = { path = "../bevy-tasks-core" } -clap = { version = "4.5", features = ["derive", "env"] } -colored = "2.0" -anyhow = { workspace = true } -chrono = { workspace = true } -uuid = { workspace = true } -fs_extra = "1.3" -tokio = { workspace = true } -rpassword = "5.0" diff --git a/crates/bevy-tasks-cli/src/commands/group.rs b/crates/bevy-tasks-cli/src/commands/group.rs deleted file mode 100644 index 23cf69f..0000000 --- a/crates/bevy-tasks-cli/src/commands/group.rs +++ /dev/null @@ -1,39 +0,0 @@ -use anyhow::{Context, Result}; -use crate::output; -use crate::commands::get_repository; - -pub fn enable(list_name: String, workspace: Option) -> Result<()> { - let (mut repo, _workspace_name) = get_repository(workspace)?; - - let lists = repo.get_lists() - .context("Failed to get lists")?; - - let list = lists.iter() - .find(|l| l.title == list_name) - .ok_or_else(|| anyhow::anyhow!("List '{}' not found", list_name))?; - - repo.set_group_by_due_date(list.id, true) - .context("Failed to enable grouping")?; - - output::success(&format!("Enabled group-by-due-date for list \"{}\"", list_name)); - - Ok(()) -} - -pub fn disable(list_name: String, workspace: Option) -> Result<()> { - let (mut repo, _workspace_name) = get_repository(workspace)?; - - let lists = repo.get_lists() - .context("Failed to get lists")?; - - let list = lists.iter() - .find(|l| l.title == list_name) - .ok_or_else(|| anyhow::anyhow!("List '{}' not found", list_name))?; - - repo.set_group_by_due_date(list.id, false) - .context("Failed to disable grouping")?; - - output::success(&format!("Disabled group-by-due-date for list \"{}\"", list_name)); - - Ok(()) -} diff --git a/crates/bevy-tasks-cli/src/commands/init.rs b/crates/bevy-tasks-cli/src/commands/init.rs deleted file mode 100644 index 743b7f7..0000000 --- a/crates/bevy-tasks-cli/src/commands/init.rs +++ /dev/null @@ -1,43 +0,0 @@ -use anyhow::{Context, Result}; -use bevy_tasks_core::{AppConfig, TaskRepository, WorkspaceConfig}; -use std::path::PathBuf; -use crate::output; - -pub fn execute(path: String, name: String) -> Result<()> { - let path_buf = PathBuf::from(path); - let path_buf = if path_buf.is_relative() { - std::env::current_dir()?.join(path_buf) - } else { - path_buf - }; - - // Initialize the repository - let mut repo = TaskRepository::init(path_buf.clone()) - .context("Failed to initialize tasks folder")?; - - // Create default list if it doesn't exist - let lists = repo.get_lists().context("Failed to get lists")?; - if !lists.iter().any(|l| l.title == "My Tasks") { - repo.create_list("My Tasks".to_string()) - .context("Failed to create default list")?; - } - - // Load or create config - let config_path = AppConfig::get_config_path(); - let mut config = AppConfig::load_from_file(&config_path) - .unwrap_or_else(|_| AppConfig::new()); - - // Add workspace - config.add_workspace(name.clone(), WorkspaceConfig::new(path_buf.clone())); - config.set_current_workspace(name.clone())?; - - // Save config - config.save_to_file(&config_path) - .context("Failed to save config")?; - - output::success(&format!("Initialized workspace \"{}\" at {}", name, path_buf.display())); - output::success("Created default list \"My Tasks\""); - output::success(&format!("Set \"{}\" as current workspace", name)); - - Ok(()) -} diff --git a/crates/bevy-tasks-cli/src/commands/list.rs b/crates/bevy-tasks-cli/src/commands/list.rs deleted file mode 100644 index 56fa32b..0000000 --- a/crates/bevy-tasks-cli/src/commands/list.rs +++ /dev/null @@ -1,91 +0,0 @@ -use anyhow::{Context, Result}; -use colored::*; -use bevy_tasks_core::{Task, TaskStatus}; -use crate::output; -use crate::commands::get_repository; - -fn print_tasks(tasks: &[Task]) { - if tasks.is_empty() { - output::item("No tasks"); - return; - } - for task in tasks { - let checkbox = if task.status == TaskStatus::Completed { "[✓]".green() } else { "[ ]".normal() }; - let due_str = task.due_date.map(|d| format!(" (due: {})", d.format("%Y-%m-%d")).yellow().to_string()).unwrap_or_default(); - output::item(&format!("{} {}{} {}", checkbox, task.title, due_str, task.id.to_string().dimmed())); - } -} - -pub fn create(name: String, workspace: Option) -> Result<()> { - let (mut repo, _workspace_name) = get_repository(workspace)?; - - repo.create_list(name.clone()) - .context("Failed to create list")?; - - output::success(&format!("Created list \"{}\"", name)); - - Ok(()) -} - -pub fn show(list_name: Option, workspace: Option) -> Result<()> { - let (repo, _workspace_name) = get_repository(workspace)?; - - let lists = repo.get_lists() - .context("Failed to get lists")?; - - if lists.is_empty() { - output::info("No lists found. Create one with 'bevy-tasks list create '"); - return Ok(()); - } - - // If a specific list is requested, show only that one - if let Some(name) = list_name { - let list = lists.iter() - .find(|l| l.title == name) - .ok_or_else(|| anyhow::anyhow!("List '{}' not found", name))?; - - output::header(&format!("{} ({})", list.title, format!("{} tasks", list.tasks.len()).dimmed())); - print_tasks(&list.tasks); - } else { - // Show all lists - for list in &lists { - output::header(&format!("{} ({})", list.title, format!("{} tasks", list.tasks.len()).dimmed())); - print_tasks(&list.tasks); - output::blank(); - } - } - - Ok(()) -} - -pub fn delete(name: String, workspace: Option) -> Result<()> { - let (mut repo, _workspace_name) = get_repository(workspace)?; - - let lists = repo.get_lists() - .context("Failed to get lists")?; - - let list = lists.iter() - .find(|l| l.title == name) - .ok_or_else(|| anyhow::anyhow!("List '{}' not found", name))?; - - // Confirm - output::warning(&format!("This will delete list \"{}\" and all its tasks", name)); - print!("Continue? (y/n): "); - use std::io::{self, Write}; - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - - if input.trim().to_lowercase() != "y" { - output::info("Cancelled"); - return Ok(()); - } - - repo.delete_list(list.id) - .context("Failed to delete list")?; - - output::success(&format!("Deleted list \"{}\"", name)); - - Ok(()) -} diff --git a/crates/bevy-tasks-cli/src/commands/mod.rs b/crates/bevy-tasks-cli/src/commands/mod.rs deleted file mode 100644 index fe9d468..0000000 --- a/crates/bevy-tasks-cli/src/commands/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -pub mod init; -pub mod workspace; -pub mod list; -pub mod task; -pub mod group; -pub mod sync; - -use bevy_tasks_core::{AppConfig, TaskRepository}; -use anyhow::{Context, Result}; -use std::path::PathBuf; - -pub fn get_config_path() -> PathBuf { - AppConfig::get_config_path() -} - -pub fn load_config() -> Result { - let path = get_config_path(); - AppConfig::load_from_file(&path).context("Failed to load config") -} - -pub fn save_config(config: &AppConfig) -> Result<()> { - let path = get_config_path(); - config.save_to_file(&path).context("Failed to save config") -} - -pub fn get_repository(workspace_name: Option) -> Result<(TaskRepository, String)> { - let config = load_config()?; - - let (name, workspace_config) = if let Some(name) = workspace_name { - let workspace_config = config.get_workspace(&name) - .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))?; - (name, workspace_config.clone()) - } else { - let (name, workspace_config) = config.get_current_workspace() - .context("No workspace set. Use 'bevy-tasks init' to create one.")?; - (name.clone(), workspace_config.clone()) - }; - - let repo = TaskRepository::new(workspace_config.path.clone()) - .context(format!("Failed to open workspace '{}'", name))?; - - Ok((repo, name)) -} diff --git a/crates/bevy-tasks-cli/src/commands/sync.rs b/crates/bevy-tasks-cli/src/commands/sync.rs deleted file mode 100644 index 6e00b3d..0000000 --- a/crates/bevy-tasks-cli/src/commands/sync.rs +++ /dev/null @@ -1,229 +0,0 @@ -use anyhow::{Context, Result}; -use colored::Colorize; -use bevy_tasks_core::sync::{SyncMode, sync_workspace, get_sync_status}; -use bevy_tasks_core::webdav::{WebDavClient, store_credentials, load_credentials}; -use crate::output; -use super::{load_config, save_config}; - -/// Run sync setup: prompt for URL, username, password, test connection, store credentials. -pub fn setup(workspace_name: Option) -> Result<()> { - let mut config = load_config()?; - - let (name, workspace) = if let Some(name) = workspace_name { - let ws = config.get_workspace(&name) - .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))? - .clone(); - (name, ws) - } else { - let (n, ws) = config.get_current_workspace() - .context("No workspace set. Use 'bevy-tasks init' to create one.")?; - (n.clone(), ws.clone()) - }; - - // Prompt for WebDAV URL - output::header(&format!("WebDAV sync setup for workspace \"{}\"", name.green())); - output::blank(); - - let url = prompt("WebDAV URL: ")?; - if url.is_empty() { - output::error("URL cannot be empty"); - return Ok(()); - } - - let username = prompt("Username: ")?; - let password = rpassword::read_password_from_tty(Some("Password: ")) - .context("Failed to read password")?; - - // Test connection - output::blank(); - output::info("Testing connection..."); - - let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?; - let client = WebDavClient::new(&url, &username, &password); - - match rt.block_on(client.test_connection()) { - Ok(()) => { - output::success("Connection successful!"); - } - Err(e) => { - output::error(&format!("Connection failed: {}", e)); - return Ok(()); - } - } - - // Store credentials in keychain - let domain = extract_domain(&url); - match store_credentials(&domain, &username, &password) { - Ok(()) => output::info("Credentials stored in system keychain"), - Err(e) => { - output::warning(&format!( - "Could not store in keychain ({}). Set BEVY_TASKS_WEBDAV_USER and BEVY_TASKS_WEBDAV_PASS env vars instead.", - e - )); - } - } - - // Update workspace config with WebDAV URL - let mut ws = workspace; - ws.webdav_url = Some(url); - config.add_workspace(name, ws); - save_config(&config)?; - - output::success("Sync setup complete. Run 'bevy-tasks sync' to sync."); - Ok(()) -} - -/// Execute a sync operation. -pub fn execute(mode: SyncMode, workspace_name: Option) -> Result<()> { - let config = load_config()?; - - let (name, workspace) = if let Some(name) = workspace_name { - let ws = config.get_workspace(&name) - .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))? - .clone(); - (name, ws) - } else { - let (n, ws) = config.get_current_workspace() - .context("No workspace set. Use 'bevy-tasks init' to create one.")?; - (n.clone(), ws.clone()) - }; - - let url = workspace.webdav_url.as_ref() - .ok_or_else(|| anyhow::anyhow!( - "No WebDAV URL configured for workspace '{}'. Run 'bevy-tasks sync --setup' first.", name - ))?; - - let domain = extract_domain(url); - let (username, password) = load_credentials(&domain) - .context("Failed to load credentials")?; - - let mode_str = match mode { - SyncMode::Full => "Syncing", - SyncMode::Push => "Pushing", - SyncMode::Pull => "Pulling", - }; - output::info(&format!("{} workspace \"{}\"...", mode_str, name.green())); - - let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?; - let result = rt.block_on(sync_workspace( - &workspace.path, - url, - &username, - &password, - mode, - Some(Box::new(|msg: &str| { println!("{}", msg); })), - )).context("Sync failed")?; - - // Print summary - let mut parts = Vec::new(); - if result.uploaded > 0 { parts.push(format!("{} uploaded", result.uploaded)); } - if result.downloaded > 0 { parts.push(format!("{} downloaded", result.downloaded)); } - if result.deleted_local > 0 { parts.push(format!("{} deleted locally", result.deleted_local)); } - if result.deleted_remote > 0 { parts.push(format!("{} deleted remotely", result.deleted_remote)); } - if result.conflicts > 0 { parts.push(format!("{} conflicts", result.conflicts)); } - - if parts.is_empty() { - output::success("Already in sync, nothing to do."); - } else { - let summary = parts.join(", "); - if result.errors.is_empty() { - output::success(&format!("Sync complete: {}", summary)); - } else { - output::warning(&format!("Sync complete with errors: {}", summary)); - for err in &result.errors { - output::error(err); - } - } - } - - Ok(()) -} - -/// Show sync status for a workspace. -pub fn status(workspace_name: Option, all: bool) -> Result<()> { - let config = load_config()?; - - if all { - // Show status for all workspaces that have sync configured - let mut found_any = false; - let mut names: Vec<_> = config.workspaces.keys().cloned().collect(); - names.sort(); - for name in names { - let ws = config.get_workspace(&name).unwrap(); - if ws.webdav_url.is_some() { - found_any = true; - print_workspace_status(&name, &ws.path, ws.webdav_url.as_deref())?; - output::blank(); - } - } - if !found_any { - output::info("No workspaces have sync configured. Run 'bevy-tasks sync --setup' to set up."); - } - return Ok(()); - } - - let (name, workspace) = if let Some(name) = workspace_name { - let ws = config.get_workspace(&name) - .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))? - .clone(); - (name, ws) - } else { - let (n, ws) = config.get_current_workspace() - .context("No workspace set.")?; - (n.clone(), ws.clone()) - }; - - print_workspace_status(&name, &workspace.path, workspace.webdav_url.as_deref())?; - Ok(()) -} - -fn print_workspace_status(name: &str, path: &std::path::Path, webdav_url: Option<&str>) -> Result<()> { - output::header(&format!("Workspace: {}", name.green())); - - if let Some(url) = webdav_url { - output::detail("WebDAV URL", url); - } else { - output::detail("WebDAV", &"not configured".dimmed().to_string()); - return Ok(()); - } - - let info = get_sync_status(path)?; - - if let Some(last) = info.last_sync { - output::detail("Last sync", &last.format("%Y-%m-%d %H:%M:%S UTC").to_string()); - } else { - output::detail("Last sync", &"never".dimmed().to_string()); - } - - output::detail("Tracked files", &info.tracked_files.to_string()); - output::detail("Pending changes", &info.pending_changes.to_string()); - if info.queued_operations > 0 { - output::detail("Queued operations", &format!("{}", info.queued_operations).yellow().to_string()); - } - - Ok(()) -} - -/// Extract domain from a URL for credential storage. -fn extract_domain(url: &str) -> String { - url.split("://") - .nth(1) - .unwrap_or(url) - .split('/') - .next() - .unwrap_or(url) - .split(':') - .next() - .unwrap_or(url) - .to_string() -} - -/// Prompt the user for text input. -fn prompt(message: &str) -> Result { - use std::io::Write; - print!("{}", message); - std::io::stdout().flush()?; - let mut input = String::new(); - std::io::stdin().read_line(&mut input)?; - Ok(input.trim().to_string()) -} diff --git a/crates/bevy-tasks-cli/src/commands/task.rs b/crates/bevy-tasks-cli/src/commands/task.rs deleted file mode 100644 index 33a4593..0000000 --- a/crates/bevy-tasks-cli/src/commands/task.rs +++ /dev/null @@ -1,222 +0,0 @@ -use anyhow::{Context, Result}; -use bevy_tasks_core::Task; -use chrono::{DateTime, Utc}; -use uuid::Uuid; -use crate::output; -use crate::commands::get_repository; - -pub fn add(title: String, list_name: Option, due_str: Option, workspace: Option) -> Result<()> { - let (mut repo, _workspace_name) = get_repository(workspace)?; - - // Get lists - let lists = repo.get_lists() - .context("Failed to get lists")?; - - if lists.is_empty() { - anyhow::bail!("No lists found. Create one with 'bevy-tasks list create '"); - } - - // Find the target list - let list = if let Some(name) = list_name { - lists.iter() - .find(|l| l.title == name) - .ok_or_else(|| anyhow::anyhow!("List '{}' not found", name))? - } else { - // Use the first list - &lists[0] - }; - - // Create task - let mut task = Task::new(title.clone()); - - // Parse due date if provided - if let Some(due_str) = due_str { - let due_date = parse_due_date(&due_str)?; - task.due_date = Some(due_date); - } - - // Save task - repo.create_task(list.id, task.clone()) - .context("Failed to create task")?; - - let due_info = if let Some(due) = task.due_date { - format!("\n Due: {}", due.format("%Y-%m-%d")) - } else { - String::new() - }; - - output::success(&format!("Created task \"{}\" ({}){}", title, task.id, due_info)); - - Ok(()) -} - -pub fn complete(task_id_str: String, workspace: Option) -> Result<()> { - let (mut repo, _workspace_name) = get_repository(workspace)?; - - let task_id = Uuid::parse_str(&task_id_str) - .context("Invalid task ID")?; - - // Find the task across all lists - let lists = repo.get_lists()?; - let mut found = false; - - for list in lists { - if let Some(mut task) = list.tasks.iter().find(|t| t.id == task_id).cloned() { - task.complete(); - repo.update_task(list.id, task.clone()) - .context("Failed to update task")?; - - output::success(&format!("Completed task \"{}\"", task.title)); - found = true; - break; - } - } - - if !found { - anyhow::bail!("Task not found: {}", task_id_str); - } - - Ok(()) -} - -pub fn delete(task_id_str: String, workspace: Option) -> Result<()> { - let (mut repo, _workspace_name) = get_repository(workspace)?; - - let task_id = Uuid::parse_str(&task_id_str) - .context("Invalid task ID")?; - - // Find the task across all lists - let lists = repo.get_lists()?; - let mut found = false; - - for list in lists { - if let Some(task) = list.tasks.iter().find(|t| t.id == task_id) { - let title = task.title.clone(); - - output::warning(&format!("This will delete task \"{}\"", title)); - print!("Continue? (y/n): "); - use std::io::{self, Write}; - io::stdout().flush()?; - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - if input.trim().to_lowercase() != "y" { - output::info("Cancelled"); - return Ok(()); - } - - repo.delete_task(list.id, task_id) - .context("Failed to delete task")?; - - output::success(&format!("Deleted task \"{}\"", title)); - found = true; - break; - } - } - - if !found { - anyhow::bail!("Task not found: {}", task_id_str); - } - - Ok(()) -} - -pub fn edit(task_id_str: String, workspace: Option) -> Result<()> { - let (mut repo, _workspace_name) = get_repository(workspace)?; - - let task_id = Uuid::parse_str(&task_id_str) - .context("Invalid task ID")?; - - // Find the task across all lists - let lists = repo.get_lists()?; - let mut task_list_id = None; - let mut task_to_edit = None; - - for list in lists { - if let Some(task) = list.tasks.iter().find(|t| t.id == task_id).cloned() { - task_list_id = Some(list.id); - task_to_edit = Some(task); - break; - } - } - - let (list_id, task) = match (task_list_id, task_to_edit) { - (Some(lid), Some(t)) => (lid, t), - _ => anyhow::bail!("Task not found: {}", task_id_str), - }; - - // Create temporary file with task content - let temp_dir = std::env::temp_dir(); - let temp_file = temp_dir.join(format!("bevy-tasks-{}.md", task.id)); - - // Write current task content to temp file - let content = format!("# {}\n\n{}", task.title, task.description); - std::fs::write(&temp_file, content)?; - - // Get editor from environment - let editor = std::env::var("EDITOR").unwrap_or_else(|_| { - if cfg!(windows) { - "notepad".to_string() - } else { - "nano".to_string() - } - }); - - // Open editor - let status = std::process::Command::new(&editor) - .arg(&temp_file) - .status() - .context(format!("Failed to open editor: {}", editor))?; - - if !status.success() { - anyhow::bail!("Editor exited with non-zero status"); - } - - // Read updated content - let updated_content = std::fs::read_to_string(&temp_file)?; - - // Parse the content - let lines: Vec<&str> = updated_content.lines().collect(); - let (title, description) = if !lines.is_empty() && lines[0].starts_with("# ") { - let title = lines[0].trim_start_matches("# ").trim().to_string(); - let description = if lines.len() > 2 { - lines[2..].join("\n").trim().to_string() - } else { - String::new() - }; - (title, description) - } else { - (task.title.clone(), updated_content.trim().to_string()) - }; - - // Update task - let mut updated_task = task.clone(); - updated_task.title = title; - updated_task.description = description; - updated_task.updated_at = Utc::now(); - - repo.update_task(list_id, updated_task.clone()) - .context("Failed to update task")?; - - // Clean up temp file - std::fs::remove_file(&temp_file).ok(); - - output::success(&format!("Updated task \"{}\"", updated_task.title)); - - Ok(()) -} - -fn parse_due_date(s: &str) -> Result> { - // Try parsing as date only (YYYY-MM-DD) - if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") { - let naive_datetime = naive_date.and_hms_opt(0, 0, 0) - .ok_or_else(|| anyhow::anyhow!("Invalid date"))?; - return Ok(DateTime::from_naive_utc_and_offset(naive_datetime, Utc)); - } - - // Try parsing as full datetime (ISO 8601) - if let Ok(dt) = DateTime::parse_from_rfc3339(s) { - return Ok(dt.with_timezone(&Utc)); - } - - anyhow::bail!("Invalid date format. Use YYYY-MM-DD or ISO 8601 format (YYYY-MM-DDTHH:MM:SS)") -} diff --git a/crates/bevy-tasks-cli/src/commands/workspace.rs b/crates/bevy-tasks-cli/src/commands/workspace.rs deleted file mode 100644 index b616472..0000000 --- a/crates/bevy-tasks-cli/src/commands/workspace.rs +++ /dev/null @@ -1,209 +0,0 @@ -use anyhow::{Context, Result}; -use bevy_tasks_core::{TaskRepository, WorkspaceConfig}; -use std::path::PathBuf; -use colored::*; -use crate::output; -use crate::commands::{load_config, save_config}; - -pub fn add(name: String, path: String) -> Result<()> { - let path_buf = PathBuf::from(path); - let path_buf = if path_buf.is_relative() { - std::env::current_dir()?.join(path_buf) - } else { - path_buf - }; - - // Initialize the repository - let mut repo = TaskRepository::init(path_buf.clone()) - .context("Failed to initialize tasks folder")?; - - // Create default list if it doesn't exist - let lists = repo.get_lists().context("Failed to get lists")?; - if !lists.iter().any(|l| l.title == "My Tasks") { - repo.create_list("My Tasks".to_string()) - .context("Failed to create default list")?; - } - - // Load config - let mut config = load_config()?; - - // Check if workspace already exists - if config.get_workspace(&name).is_some() { - anyhow::bail!("Workspace '{}' already exists", name); - } - - // Add workspace - config.add_workspace(name.clone(), WorkspaceConfig::new(path_buf.clone())); - - // Save config - save_config(&config)?; - - output::success(&format!("Added workspace \"{}\" at {}", name, path_buf.display())); - output::success("Created default list \"My Tasks\""); - - Ok(()) -} - -pub fn list() -> Result<()> { - let config = load_config()?; - - if config.workspaces.is_empty() { - output::info("No workspaces configured. Use 'bevy-tasks init' to create one."); - return Ok(()); - } - - let current = config.current_workspace.as_deref(); - - let mut workspaces: Vec<_> = config.workspaces.iter().collect(); - workspaces.sort_by(|a, b| a.0.cmp(b.0)); - - for (name, workspace_config) in workspaces { - let marker = if Some(name.as_str()) == current { - " (current)".green() - } else { - "".normal() - }; - output::item(&format!("{}: {}{}", name, workspace_config.path.display(), marker)); - } - - Ok(()) -} - -pub fn switch(name: String) -> Result<()> { - let mut config = load_config()?; - - // Verify workspace exists - if config.get_workspace(&name).is_none() { - anyhow::bail!("Workspace '{}' not found", name); - } - - config.set_current_workspace(name.clone())?; - save_config(&config)?; - - output::success(&format!("Switched to workspace \"{}\"", name)); - - Ok(()) -} - -pub fn remove(name: String) -> Result<()> { - let mut config = load_config()?; - - // Verify workspace exists - if config.get_workspace(&name).is_none() { - anyhow::bail!("Workspace '{}' not found", name); - } - - // Confirm - output::warning("This will delete workspace config (files remain on disk)"); - print!("Continue? (y/n): "); - use std::io::{self, Write}; - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - - if input.trim().to_lowercase() != "y" { - output::info("Cancelled"); - return Ok(()); - } - - config.remove_workspace(&name); - save_config(&config)?; - - output::success(&format!("Removed workspace \"{}\"", name)); - - Ok(()) -} - -pub fn retarget(name: String, path: String) -> Result<()> { - let path_buf = PathBuf::from(path); - let path_buf = if path_buf.is_relative() { - std::env::current_dir()?.join(path_buf) - } else { - path_buf - }; - - let mut config = load_config()?; - - // Verify workspace exists - if config.get_workspace(&name).is_none() { - anyhow::bail!("Workspace '{}' not found", name); - } - - // Update path - config.add_workspace(name.clone(), WorkspaceConfig::new(path_buf.clone())); - save_config(&config)?; - - output::success(&format!("Workspace \"{}\" now points to {}", name, path_buf.display())); - - Ok(()) -} - -pub fn migrate(name: String, new_path: String) -> Result<()> { - let new_path_buf = PathBuf::from(new_path); - let new_path_buf = if new_path_buf.is_relative() { - std::env::current_dir()?.join(new_path_buf) - } else { - new_path_buf - }; - - let mut config = load_config()?; - - // Get current workspace config - let old_path = config.get_workspace(&name) - .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))? - .path.clone(); - - // Confirm - output::warning(&format!("This will move all files from {} to {}", old_path.display(), new_path_buf.display())); - print!("Continue? (y/n): "); - use std::io::{self, Write}; - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - - if input.trim().to_lowercase() != "y" { - output::info("Cancelled"); - return Ok(()); - } - - // Create destination directory - std::fs::create_dir_all(&new_path_buf)?; - - // Move files - output::info("Moving files..."); - let entries = std::fs::read_dir(&old_path)?; - let mut count = 0; - - for entry in entries { - let entry = entry?; - let file_name = entry.file_name(); - let dest = new_path_buf.join(&file_name); - - if entry.path().is_dir() { - let mut options = fs_extra::dir::CopyOptions::new(); - options.copy_inside = true; - fs_extra::dir::move_dir(entry.path(), &new_path_buf, &options)?; - output::item(&format!("Moved {}/", file_name.to_string_lossy())); - } else { - std::fs::rename(entry.path(), dest)?; - output::item(&format!("Moved {}", file_name.to_string_lossy())); - } - count += 1; - } - - // Remove old directory if empty - if old_path.read_dir()?.next().is_none() { - std::fs::remove_dir(&old_path)?; - } - - // Update config - config.add_workspace(name.clone(), WorkspaceConfig::new(new_path_buf.clone())); - save_config(&config)?; - - output::success(&format!("Migrated {} items to {}", count, new_path_buf.display())); - output::success(&format!("Workspace \"{}\" now points to {}", name, new_path_buf.display())); - - Ok(()) -} diff --git a/crates/bevy-tasks-cli/src/main.rs b/crates/bevy-tasks-cli/src/main.rs deleted file mode 100644 index 6c0268e..0000000 --- a/crates/bevy-tasks-cli/src/main.rs +++ /dev/null @@ -1,277 +0,0 @@ -mod commands; -mod output; - -use anyhow::Result; -use clap::{Parser, Subcommand}; -use commands::*; - -#[derive(Parser)] -#[command(name = "bevy-tasks")] -#[command(about = "A local-first, cross-platform tasks application", long_about = None)] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - /// Initialize a new workspace - Init { - /// Path to store tasks - path: String, - /// Name of the workspace - #[arg(short, long)] - name: String, - }, - - /// Manage workspaces - #[command(subcommand)] - Workspace(WorkspaceCommands), - - /// Manage task lists - #[command(subcommand)] - List(ListCommands), - - /// Add a new task - Add { - /// Task title - title: String, - /// List to add task to - #[arg(short, long)] - list: Option, - /// Due date (ISO 8601 format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) - #[arg(short, long)] - due: Option, - /// Workspace to use - #[arg(short, long)] - workspace: Option, - }, - - /// Mark a task as complete - Complete { - /// Task ID - task_id: String, - /// Workspace to use - #[arg(short, long)] - workspace: Option, - }, - - /// Delete a task - Delete { - /// Task ID - task_id: String, - /// Workspace to use - #[arg(short, long)] - workspace: Option, - }, - - /// Edit a task - Edit { - /// Task ID - task_id: String, - /// Workspace to use - #[arg(short, long)] - workspace: Option, - }, - - /// Toggle group-by-due-date for a list - #[command(subcommand)] - Group(GroupCommands), - - /// Sync workspace with WebDAV server - Sync { - /// Run initial setup (URL, credentials) - #[arg(long)] - setup: bool, - /// Push-only sync (upload local changes) - #[arg(long, conflicts_with_all = ["pull", "setup", "status"])] - push: bool, - /// Pull-only sync (download remote changes) - #[arg(long, conflicts_with_all = ["push", "setup", "status"])] - pull: bool, - /// Show sync status - #[arg(long, conflicts_with_all = ["push", "pull", "setup"])] - status: bool, - /// Show status for all workspaces (with --status) - #[arg(long, requires = "status")] - all: bool, - /// Workspace to use - #[arg(short, long)] - workspace: Option, - }, -} - -#[derive(Subcommand)] -enum WorkspaceCommands { - /// Add a new workspace - Add { - /// Name of the workspace - name: String, - /// Path to store tasks - path: String, - }, - - /// List all workspaces - List, - - /// Switch to a different workspace - Switch { - /// Name of the workspace - name: String, - }, - - /// Remove a workspace - Remove { - /// Name of the workspace - name: String, - }, - - /// Update workspace path without moving files - Retarget { - /// Name of the workspace - name: String, - /// New path - path: String, - }, - - /// Move workspace files to a new location - Migrate { - /// Name of the workspace - name: String, - /// New path - path: String, - }, -} - -#[derive(Subcommand)] -enum ListCommands { - /// Create a new task list - Create { - /// Name of the list - name: String, - /// Workspace to use - #[arg(short, long)] - workspace: Option, - }, - - /// Show all tasks (or tasks in a specific list) - Show { - /// Name of the list to show - #[arg(short, long)] - list: Option, - /// Workspace to use - #[arg(short, long)] - workspace: Option, - }, - - /// Delete a task list - Delete { - /// Name of the list to delete - name: String, - /// Workspace to use - #[arg(short, long)] - workspace: Option, - }, -} - -#[derive(Subcommand)] -enum GroupCommands { - /// Enable group-by-due-date for a list - Enable { - /// Name of the list - #[arg(short, long)] - list: String, - /// Workspace to use - #[arg(short, long)] - workspace: Option, - }, - - /// Disable group-by-due-date for a list - Disable { - /// Name of the list - #[arg(short, long)] - list: String, - /// Workspace to use - #[arg(short, long)] - workspace: Option, - }, -} - -fn main() -> Result<()> { - let cli = Cli::parse(); - - match cli.command { - Commands::Init { path, name } => { - init::execute(path, name)?; - } - Commands::Workspace(cmd) => match cmd { - WorkspaceCommands::Add { name, path } => { - workspace::add(name, path)?; - } - WorkspaceCommands::List => { - workspace::list()?; - } - WorkspaceCommands::Switch { name } => { - workspace::switch(name)?; - } - WorkspaceCommands::Remove { name } => { - workspace::remove(name)?; - } - WorkspaceCommands::Retarget { name, path } => { - workspace::retarget(name, path)?; - } - WorkspaceCommands::Migrate { name, path } => { - workspace::migrate(name, path)?; - } - }, - Commands::List(cmd) => match cmd { - ListCommands::Create { name, workspace } => { - list::create(name, workspace)?; - } - ListCommands::Show { list, workspace } => { - list::show(list, workspace)?; - } - ListCommands::Delete { name, workspace } => { - list::delete(name, workspace)?; - } - }, - Commands::Add { title, list, due, workspace } => { - task::add(title, list, due, workspace)?; - } - Commands::Complete { task_id, workspace } => { - task::complete(task_id, workspace)?; - } - Commands::Delete { task_id, workspace } => { - task::delete(task_id, workspace)?; - } - Commands::Edit { task_id, workspace } => { - task::edit(task_id, workspace)?; - } - Commands::Group(cmd) => match cmd { - GroupCommands::Enable { list, workspace } => { - group::enable(list, workspace)?; - } - GroupCommands::Disable { list, workspace } => { - group::disable(list, workspace)?; - } - }, - Commands::Sync { setup, push, pull, status, all, workspace } => { - if setup { - sync::setup(workspace)?; - } else if status { - sync::status(workspace, all)?; - } else { - let mode = if push { - bevy_tasks_core::sync::SyncMode::Push - } else if pull { - bevy_tasks_core::sync::SyncMode::Pull - } else { - bevy_tasks_core::sync::SyncMode::Full - }; - sync::execute(mode, workspace)?; - } - }, - } - - Ok(()) -} diff --git a/crates/bevy-tasks-cli/src/output.rs b/crates/bevy-tasks-cli/src/output.rs deleted file mode 100644 index dd49031..0000000 --- a/crates/bevy-tasks-cli/src/output.rs +++ /dev/null @@ -1,33 +0,0 @@ -use colored::*; - -pub fn success(message: &str) { - println!("{} {}", "✓".green(), message); -} - -pub fn error(message: &str) { - eprintln!("{} {}", "✗".red(), message); -} - -pub fn warning(message: &str) { - println!("{} {}", "⚠".yellow(), message); -} - -pub fn info(message: &str) { - println!("{} {}", "ℹ".blue(), message); -} - -pub fn header(message: &str) { - println!("{}", message.bold()); -} - -pub fn detail(label: &str, value: &str) { - println!(" {}: {}", label, value); -} - -pub fn item(message: &str) { - println!(" {}", message); -} - -pub fn blank() { - println!(); -}