Implement cmd::project handlers and retire flat project commands

Amolith created

Add src/cmd/project.rs with handlers for all five ProjectAction variants
(init, bind, unbind, delete, list). Wire dispatch in cmd/mod.rs.

Remove the now-superseded src/cmd/init.rs, use.rs, and projects.rs modules.
Update the two error strings in cmd/sync.rs that still referenced td init.

Change summary

src/cmd/init.rs     | 18 ---------
src/cmd/mod.rs      | 11 +----
src/cmd/project.rs  | 88 +++++++++++++++++++++++++++++++++++++++++++++++
src/cmd/projects.rs | 15 --------
src/cmd/sync.rs     |  4 +-
src/cmd/use.rs      | 18 ---------
6 files changed, 92 insertions(+), 62 deletions(-)

Detailed changes

src/cmd/init.rs 🔗

@@ -1,18 +0,0 @@
-use anyhow::Result;
-use std::path::Path;
-
-pub fn run(root: &Path, name: &str, json: bool) -> Result<()> {
-    crate::db::init(root, name)?;
-
-    if json {
-        println!(
-            "{}",
-            serde_json::json!({"success": true, "project": name, "bound_path": root})
-        );
-    } else {
-        let c = crate::color::stderr_theme();
-        eprintln!("{}info:{} initialized project '{name}'", c.blue, c.reset);
-    }
-
-    Ok(())
-}

src/cmd/mod.rs 🔗

@@ -3,12 +3,11 @@ mod dep;
 mod done;
 mod export;
 mod import;
-mod init;
 mod label;
 mod list;
 mod log;
 mod next;
-mod projects;
+mod project;
 mod ready;
 mod reopen;
 mod rm;
@@ -19,7 +18,6 @@ mod stats;
 pub mod sync;
 mod tidy;
 mod update;
-mod r#use;
 
 use crate::cli::{Cli, Command};
 use crate::db;
@@ -35,12 +33,7 @@ pub fn dispatch(cli: &Cli) -> Result<()> {
     }
 
     match &cli.command {
-        Command::Init { name } => {
-            let root = std::env::current_dir()?;
-            init::run(&root, name, cli.json)
-        }
-        Command::Use { name } => r#use::run(name, cli.json),
-        Command::Projects => projects::run(cli.json),
+        Command::Project { action } => project::run(action, cli.json),
         Command::Create {
             title,
             priority,

src/cmd/project.rs 🔗

@@ -0,0 +1,88 @@
+use anyhow::Result;
+use std::path::Path;
+
+use crate::cli::ProjectAction;
+
+pub fn run(action: &ProjectAction, json: bool) -> Result<()> {
+    let cwd = std::env::current_dir()?;
+
+    match action {
+        ProjectAction::Init { name } => init(&cwd, name, json),
+        ProjectAction::Bind { name } => bind(&cwd, name, json),
+        ProjectAction::Unbind => unbind(&cwd, json),
+        ProjectAction::Delete { name } => delete(name, json),
+        ProjectAction::List => list(json),
+    }
+}
+
+fn init(cwd: &Path, name: &str, json: bool) -> Result<()> {
+    crate::db::init(cwd, name)?;
+
+    if json {
+        println!(
+            "{}",
+            serde_json::json!({"success": true, "project": name, "bound_path": cwd})
+        );
+    } else {
+        let c = crate::color::stderr_theme();
+        eprintln!("{}info:{} initialized project '{name}'", c.blue, c.reset);
+    }
+
+    Ok(())
+}
+
+fn bind(cwd: &Path, name: &str, json: bool) -> Result<()> {
+    crate::db::use_project(cwd, name)?;
+
+    if json {
+        println!(
+            "{}",
+            serde_json::json!({"success": true, "project": name, "bound_path": cwd})
+        );
+    } else {
+        let c = crate::color::stdout_theme();
+        println!("{}bound{} {} -> {name}", c.green, c.reset, cwd.display());
+    }
+
+    Ok(())
+}
+
+fn unbind(cwd: &Path, json: bool) -> Result<()> {
+    crate::db::unbind_project(cwd)?;
+
+    if json {
+        println!("{}", serde_json::json!({"success": true}));
+    } else {
+        let c = crate::color::stderr_theme();
+        eprintln!("{}info:{} unbound {}", c.blue, c.reset, cwd.display());
+    }
+
+    Ok(())
+}
+
+fn delete(name: &str, json: bool) -> Result<()> {
+    crate::db::delete_project(name)?;
+
+    if json {
+        println!("{}", serde_json::json!({"success": true, "project": name}));
+    } else {
+        let c = crate::color::stderr_theme();
+        eprintln!("{}info:{} deleted project '{name}'", c.blue, c.reset);
+    }
+
+    Ok(())
+}
+
+fn list(json: bool) -> Result<()> {
+    let projects = crate::db::list_projects()?;
+
+    if json {
+        println!("{}", serde_json::to_string(&projects)?);
+    } else {
+        for project in projects {
+            println!("{project}");
+        }
+    }
+
+    Ok(())
+}

src/cmd/projects.rs 🔗

@@ -1,15 +0,0 @@
-use anyhow::Result;
-
-pub fn run(json: bool) -> Result<()> {
-    let projects = crate::db::list_projects()?;
-
-    if json {
-        println!("{}", serde_json::to_string(&projects)?);
-    } else {
-        for project in projects {
-            println!("{project}");
-        }
-    }
-
-    Ok(())
-}

src/cmd/sync.rs 🔗

@@ -114,7 +114,7 @@ pub async fn exchange(store: &db::Store, mut wormhole: Wormhole) -> Result<SyncR
             if my_project_id != project_id {
                 let _ = wormhole.close().await;
                 bail!(
-                    "project identity mismatch: local '{}' ({}) vs peer '{}' ({}). If this is the same logical project, remove the accidentally initted local copy and bootstrap with 'td sync' instead of running 'td init' on both machines",
+                    "project identity mismatch: local '{}' ({}) vs peer '{}' ({}). If this is the same logical project, remove the accidentally initted local copy and bootstrap with 'td sync' instead of running 'td project init <project>' on both machines",
                     my_project_name,
                     my_project_id,
                     project_name,
@@ -220,7 +220,7 @@ async fn bootstrap_exchange(
         SyncHandshake::Bootstrap { .. } => {
             let _ = wormhole.close().await;
             bail!(
-                "both peers are in bootstrap mode. Run 'td init <project>' on one machine first, then run 'td sync' on the other"
+                "both peers are in bootstrap mode. Run 'td project init <project>' on one machine first, then run 'td sync' on the other"
             );
         }
     };

src/cmd/use.rs 🔗

@@ -1,18 +0,0 @@
-use anyhow::Result;
-
-pub fn run(name: &str, json: bool) -> Result<()> {
-    let cwd = std::env::current_dir()?;
-    crate::db::use_project(&cwd, name)?;
-
-    if json {
-        println!(
-            "{}",
-            serde_json::json!({"success": true, "project": name, "bound_path": cwd})
-        );
-    } else {
-        let c = crate::color::stdout_theme();
-        println!("{}bound{} {} -> {name}", c.green, c.reset, cwd.display());
-    }
-
-    Ok(())
-}