Add justfile as canonical dev workflow entry point
Assisted-by: Claude Opus 4.6 via pi
change toqytlvpykxutsrxsyvypnkvtmupyypl
commit 6461d3810b670d6b096e21b4e15548d4c29a0e74
author Alpha Chen <alpha@kejadlen.dev>
date
parent omkrtwms
diff --git a/AGENTS.md b/AGENTS.md
index 2a3201d..5b418c3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -7,10 +7,11 @@ Personal task tracker. Rust workspace: `ranger-lib` (library) + `ranger-cli` (bi
 ## Commands
 
 ```bash
-cargo build --workspace          # Build everything
-cargo test --workspace           # Run all tests (33 unit + 1 integration)
-cargo test -p ranger-lib         # Library tests only
-cargo test -p ranger-cli         # CLI integration test only
+just fmt                         # Format all code
+just check                       # Type-check the workspace
+just clippy                      # Lint (deny warnings)
+just coverage                    # Run tests with coverage (fail under 100%)
+just all                         # fmt + clippy + coverage
 cargo run --bin ranger -- --help  # CLI usage
 ```
 
diff --git a/crates/ranger-cli/src/commands/backlog.rs b/crates/ranger-cli/src/commands/backlog.rs
index de2cae2..e3b803e 100644
--- a/crates/ranger-cli/src/commands/backlog.rs
+++ b/crates/ranger-cli/src/commands/backlog.rs
@@ -1,7 +1,7 @@
 use clap::Subcommand;
+use ranger_lib::db::SqlitePool;
 use ranger_lib::models::Backlog;
 use ranger_lib::ops;
-use ranger_lib::db::SqlitePool;
 
 use crate::output;
 
@@ -25,15 +25,15 @@ pub async fn run(pool: &SqlitePool, command: BacklogCommands, json: bool) -> any
     match command {
         BacklogCommands::Create { name } => {
             let backlog = ops::backlog::create(pool, &name).await?;
-            output::print(&backlog, json, |b| print_backlog(b));
+            output::print(&backlog, json, print_backlog);
         }
         BacklogCommands::List => {
             let backlogs = ops::backlog::list(pool).await?;
-            output::print_list(&backlogs, json, |b| print_backlog(b));
+            output::print_list(&backlogs, json, print_backlog);
         }
         BacklogCommands::Show { key } => {
             let backlog = ops::backlog::get_by_key_prefix(pool, &key).await?;
-            output::print(&backlog, json, |b| print_backlog_detail(b));
+            output::print(&backlog, json, print_backlog_detail);
         }
     }
     Ok(())
diff --git a/crates/ranger-cli/src/commands/blocker.rs b/crates/ranger-cli/src/commands/blocker.rs
index a6bebfa..cfae4e0 100644
--- a/crates/ranger-cli/src/commands/blocker.rs
+++ b/crates/ranger-cli/src/commands/blocker.rs
@@ -29,12 +29,7 @@ pub async fn run(pool: &SqlitePool, command: BlockerCommands, json: bool) -> any
             let bt = ops::task::get_by_key_prefix(pool, &blocked_by).await?;
             let blocker = ops::blocker::add(pool, t.id, bt.id).await?;
             output::print(&blocker, json, |_| {
-                println!(
-                    "{} blocked by {} {}",
-                    &t.key[..8],
-                    &bt.key[..8],
-                    bt.title
-                );
+                println!("{} blocked by {} {}", &t.key[..8], &bt.key[..8], bt.title);
             });
         }
         BlockerCommands::Remove { task, blocked_by } => {
diff --git a/crates/ranger-cli/src/commands/task.rs b/crates/ranger-cli/src/commands/task.rs
index 492bb26..18423c6 100644
--- a/crates/ranger-cli/src/commands/task.rs
+++ b/crates/ranger-cli/src/commands/task.rs
@@ -102,11 +102,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> anyhow
         } => {
             let bl = ops::backlog::get_by_key_prefix(pool, &backlog).await?;
             let parent_id = if let Some(parent_key) = &parent {
-                Some(
-                    ops::task::get_by_key_prefix(pool, parent_key)
-                        .await?
-                        .id,
-                )
+                Some(ops::task::get_by_key_prefix(pool, parent_key).await?.id)
             } else {
                 None
             };
@@ -128,13 +124,13 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> anyhow
                 }
             }
 
-            output::print(&task, json, |t| print_task(t));
+            output::print(&task, json, print_task);
         }
         TaskCommands::List { backlog, state } => {
             if let Some(backlog_key) = &backlog {
                 let bl = ops::backlog::get_by_key_prefix(pool, backlog_key).await?;
                 let tasks = ops::task::list(pool, bl.id, state.as_deref()).await?;
-                output::print_list(&tasks, json, |t| print_task(t));
+                output::print_list(&tasks, json, print_task);
             } else {
                 // List all tasks (no backlog filter)
                 let backlogs = ops::backlog::list(pool).await?;
@@ -147,7 +143,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> anyhow
                         }
                     }
                 }
-                output::print_list(&all_tasks, json, |t| print_task(t));
+                output::print_list(&all_tasks, json, print_task);
             }
         }
         TaskCommands::Show { key } => {
@@ -202,7 +198,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> anyhow
                 state.as_deref(),
             )
             .await?;
-            output::print(&updated, json, |t| print_task(t));
+            output::print(&updated, json, print_task);
         }
         TaskCommands::Move {
             key,
diff --git a/crates/ranger-cli/src/output.rs b/crates/ranger-cli/src/output.rs
index 1cb675a..0b6719b 100644
--- a/crates/ranger-cli/src/output.rs
+++ b/crates/ranger-cli/src/output.rs
@@ -8,11 +8,7 @@ pub fn print<T: Serialize + std::fmt::Debug>(value: &T, json: bool, human: impl
     }
 }
 
-pub fn print_list<T: Serialize + std::fmt::Debug>(
-    values: &[T],
-    json: bool,
-    human: impl Fn(&T),
-) {
+pub fn print_list<T: Serialize + std::fmt::Debug>(values: &[T], json: bool, human: impl Fn(&T)) {
     if json {
         println!("{}", serde_json::to_string_pretty(values).unwrap());
     } else {
diff --git a/crates/ranger-cli/tests/cli.rs b/crates/ranger-cli/tests/cli.rs
index dd4e6d3..0eb181f 100644
--- a/crates/ranger-cli/tests/cli.rs
+++ b/crates/ranger-cli/tests/cli.rs
@@ -1,5 +1,5 @@
-use assert_cmd::cargo::cargo_bin_cmd;
 use assert_cmd::Command;
+use assert_cmd::cargo::cargo_bin_cmd;
 use tempfile::tempdir;
 
 fn ranger(db_path: &str) -> Command {
@@ -29,20 +29,35 @@ fn full_workflow() {
         .output()
         .unwrap();
     assert!(output.status.success());
-    let backlogs: serde_json::Value =
-        serde_json::from_slice(&output.stdout).unwrap();
+    let backlogs: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
     let backlog_key = backlogs[0]["key"].as_str().unwrap().to_string();
     let bl_prefix = &backlog_key[..4];
 
     // Create tasks
     let output = ranger(db_path)
-        .args(["task", "create", "First task", "--backlog", bl_prefix, "--state", "queued"])
+        .args([
+            "task",
+            "create",
+            "First task",
+            "--backlog",
+            bl_prefix,
+            "--state",
+            "queued",
+        ])
         .output()
         .unwrap();
     assert!(output.status.success());
 
     let output = ranger(db_path)
-        .args(["task", "create", "Second task", "--backlog", bl_prefix, "--tag", "urgent"])
+        .args([
+            "task",
+            "create",
+            "Second task",
+            "--backlog",
+            bl_prefix,
+            "--tag",
+            "urgent",
+        ])
         .output()
         .unwrap();
     assert!(output.status.success());
@@ -53,8 +68,7 @@ fn full_workflow() {
         .output()
         .unwrap();
     assert!(output.status.success());
-    let tasks: serde_json::Value =
-        serde_json::from_slice(&output.stdout).unwrap();
+    let tasks: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
     let tasks = tasks.as_array().unwrap();
     assert_eq!(tasks.len(), 2);
     assert_eq!(tasks[0]["title"], "First task");
@@ -96,10 +110,7 @@ fn full_workflow() {
     assert!(output.status.success());
 
     // List tags
-    let output = ranger(db_path)
-        .args(["tag", "list"])
-        .output()
-        .unwrap();
+    let output = ranger(db_path).args(["tag", "list"]).output().unwrap();
     assert!(output.status.success());
     let stdout = String::from_utf8(output.stdout).unwrap();
     assert!(stdout.contains("urgent"));
@@ -110,8 +121,7 @@ fn full_workflow() {
         .output()
         .unwrap();
     assert!(output.status.success());
-    let detail: serde_json::Value =
-        serde_json::from_slice(&output.stdout).unwrap();
+    let detail: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
     assert_eq!(detail["task"]["title"], "Second task");
     assert_eq!(detail["tags"][0]["name"], "urgent");
     assert_eq!(detail["blockers"].as_array().unwrap().len(), 1);
@@ -129,7 +139,6 @@ fn full_workflow() {
         .output()
         .unwrap();
     assert!(output.status.success());
-    let tasks: serde_json::Value =
-        serde_json::from_slice(&output.stdout).unwrap();
+    let tasks: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
     assert_eq!(tasks.as_array().unwrap().len(), 1);
 }
diff --git a/crates/ranger-lib/src/db.rs b/crates/ranger-lib/src/db.rs
index e6add01..6ead83d 100644
--- a/crates/ranger-lib/src/db.rs
+++ b/crates/ranger-lib/src/db.rs
@@ -41,11 +41,10 @@ mod tests {
         let db_path = dir.path().join("test.db");
         let pool = connect(&db_path).await.unwrap();
 
-        let result =
-            sqlx::query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
-                .fetch_all(&pool)
-                .await
-                .unwrap();
+        let result = sqlx::query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
+            .fetch_all(&pool)
+            .await
+            .unwrap();
 
         let table_names: Vec<String> = result
             .iter()
diff --git a/crates/ranger-lib/src/ops/task.rs b/crates/ranger-lib/src/ops/task.rs
index 3dbfdf3..dc487b3 100644
--- a/crates/ranger-lib/src/ops/task.rs
+++ b/crates/ranger-lib/src/ops/task.rs
@@ -202,11 +202,10 @@ pub async fn add_to_backlog(
     backlog_id: i64,
 ) -> Result<(), RangerError> {
     // Get the task's state to find proper position
-    let state: String =
-        sqlx::query_scalar("SELECT state FROM tasks WHERE id = ?")
-            .bind(task_id)
-            .fetch_one(pool)
-            .await?;
+    let state: String = sqlx::query_scalar("SELECT state FROM tasks WHERE id = ?")
+        .bind(task_id)
+        .fetch_one(pool)
+        .await?;
 
     let last_pos: Option<String> = sqlx::query_scalar(
         "SELECT bt.position FROM backlog_tasks bt \
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..d7d8983
--- /dev/null
+++ b/justfile
@@ -0,0 +1,18 @@
+# Format all code
+fmt:
+    cargo fmt --all
+
+# Type-check the workspace
+check:
+    cargo check --workspace
+
+# Lint with clippy (deny warnings)
+clippy:
+    cargo clippy --workspace -- -D warnings
+
+# Run tests with coverage reporting
+coverage:
+    cargo llvm-cov --workspace --fail-under-lines 100
+
+# Run all checks: fmt, clippy, tests with coverage
+all: fmt clippy coverage