Add justfile as canonical dev workflow entry point
Assisted-by: Claude Opus 4.6 via pi
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