Add CLI scaffolding and backlog commands
Unpin dependency versions to "*" while here.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/Cargo.lock b/Cargo.lock
index cb149e1..1c9ee71 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1115,8 +1115,10 @@ dependencies = [
name = "ranger-cli"
version = "0.1.0"
dependencies = [
+ "anyhow",
"clap",
"ranger-lib",
+ "serde",
"serde_json",
"tokio",
"xdg",
diff --git a/crates/ranger-cli/Cargo.toml b/crates/ranger-cli/Cargo.toml
index ba679ce..6fe1697 100644
--- a/crates/ranger-cli/Cargo.toml
+++ b/crates/ranger-cli/Cargo.toml
@@ -9,7 +9,9 @@ path = "src/main.rs"
[dependencies]
ranger-lib = { path = "../ranger-lib" }
-clap = { version = "4", features = ["derive"] }
-tokio = { version = "1", features = ["full"] }
-serde_json = "1"
-xdg = "2"
+clap = { version = "*", features = ["derive", "env"] }
+tokio = { version = "*", features = ["full"] }
+serde = { version = "*", features = ["derive"] }
+serde_json = "*"
+anyhow = "*"
+xdg = "*"
diff --git a/crates/ranger-cli/src/commands/backlog.rs b/crates/ranger-cli/src/commands/backlog.rs
new file mode 100644
index 0000000..de2cae2
--- /dev/null
+++ b/crates/ranger-cli/src/commands/backlog.rs
@@ -0,0 +1,51 @@
+use clap::Subcommand;
+use ranger_lib::models::Backlog;
+use ranger_lib::ops;
+use ranger_lib::db::SqlitePool;
+
+use crate::output;
+
+#[derive(Subcommand)]
+pub enum BacklogCommands {
+ /// Create a new backlog
+ Create {
+ /// Name for the backlog
+ name: String,
+ },
+ /// List all backlogs
+ List,
+ /// Show a backlog's details
+ Show {
+ /// Key or key prefix of the backlog
+ key: String,
+ },
+}
+
+pub async fn run(pool: &SqlitePool, command: BacklogCommands, json: bool) -> anyhow::Result<()> {
+ match command {
+ BacklogCommands::Create { name } => {
+ let backlog = ops::backlog::create(pool, &name).await?;
+ output::print(&backlog, json, |b| print_backlog(b));
+ }
+ BacklogCommands::List => {
+ let backlogs = ops::backlog::list(pool).await?;
+ output::print_list(&backlogs, json, |b| print_backlog(b));
+ }
+ BacklogCommands::Show { key } => {
+ let backlog = ops::backlog::get_by_key_prefix(pool, &key).await?;
+ output::print(&backlog, json, |b| print_backlog_detail(b));
+ }
+ }
+ Ok(())
+}
+
+fn print_backlog(b: &Backlog) {
+ println!("{} {}", &b.key[..8], b.name);
+}
+
+fn print_backlog_detail(b: &Backlog) {
+ println!("Key: {}", b.key);
+ println!("Name: {}", b.name);
+ println!("Created: {}", b.created_at);
+ println!("Updated: {}", b.updated_at);
+}
diff --git a/crates/ranger-cli/src/commands/mod.rs b/crates/ranger-cli/src/commands/mod.rs
new file mode 100644
index 0000000..40d080e
--- /dev/null
+++ b/crates/ranger-cli/src/commands/mod.rs
@@ -0,0 +1 @@
+pub mod backlog;
diff --git a/crates/ranger-cli/src/main.rs b/crates/ranger-cli/src/main.rs
index 30e3539..6dcae42 100644
--- a/crates/ranger-cli/src/main.rs
+++ b/crates/ranger-cli/src/main.rs
@@ -1,3 +1,53 @@
-fn main() {
- println!("{}", ranger_lib::hello());
+mod commands;
+mod output;
+
+use clap::{Parser, Subcommand};
+use std::path::PathBuf;
+
+#[derive(Parser)]
+#[command(name = "ranger", about = "Personal task tracker")]
+struct Cli {
+ /// Output as JSON
+ #[arg(long, global = true)]
+ json: bool,
+
+ /// Path to database file (default: $XDG_DATA_HOME/ranger/ranger.db)
+ #[arg(long, env = "RANGER_DB", global = true)]
+ db: Option<PathBuf>,
+
+ #[command(subcommand)]
+ command: Commands,
+}
+
+#[derive(Subcommand)]
+enum Commands {
+ /// Manage backlogs
+ Backlog {
+ #[command(subcommand)]
+ command: commands::backlog::BacklogCommands,
+ },
+}
+
+fn resolve_db_path(cli_path: Option<PathBuf>) -> PathBuf {
+ if let Some(path) = cli_path {
+ return path;
+ }
+ let xdg = xdg::BaseDirectories::with_prefix("ranger").expect("failed to resolve XDG dirs");
+ xdg.place_data_file("ranger.db")
+ .expect("failed to create data directory")
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ let cli = Cli::parse();
+ let db_path = resolve_db_path(cli.db);
+ let pool = ranger_lib::db::connect(&db_path).await?;
+
+ match cli.command {
+ Commands::Backlog { command } => {
+ commands::backlog::run(&pool, command, cli.json).await?;
+ }
+ }
+
+ Ok(())
}
diff --git a/crates/ranger-cli/src/output.rs b/crates/ranger-cli/src/output.rs
new file mode 100644
index 0000000..1cb675a
--- /dev/null
+++ b/crates/ranger-cli/src/output.rs
@@ -0,0 +1,23 @@
+use serde::Serialize;
+
+pub fn print<T: Serialize + std::fmt::Debug>(value: &T, json: bool, human: impl FnOnce(&T)) {
+ if json {
+ println!("{}", serde_json::to_string_pretty(value).unwrap());
+ } else {
+ human(value);
+ }
+}
+
+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 {
+ for v in values {
+ human(v);
+ }
+ }
+}
diff --git a/crates/ranger-lib/Cargo.toml b/crates/ranger-lib/Cargo.toml
index bee6326..5eaca21 100644
--- a/crates/ranger-lib/Cargo.toml
+++ b/crates/ranger-lib/Cargo.toml
@@ -4,13 +4,13 @@ version = "0.1.0"
edition = "2024"
[dependencies]
-sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
-tokio = { version = "1", features = ["full"] }
-serde = { version = "1", features = ["derive"] }
-serde_json = "1"
-rand = "0.9"
-chrono = { version = "0.4", features = ["serde"] }
-thiserror = "2"
+sqlx = { version = "*", features = ["runtime-tokio", "sqlite"] }
+tokio = { version = "*", features = ["full"] }
+serde = { version = "*", features = ["derive"] }
+serde_json = "*"
+rand = "*"
+chrono = { version = "*", features = ["serde"] }
+thiserror = "*"
[dev-dependencies]
-tempfile = "3"
+tempfile = "*"
diff --git a/crates/ranger-lib/src/db.rs b/crates/ranger-lib/src/db.rs
index acb6e05..e6add01 100644
--- a/crates/ranger-lib/src/db.rs
+++ b/crates/ranger-lib/src/db.rs
@@ -1,7 +1,9 @@
use crate::error::RangerError;
-use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
+use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use std::path::Path;
+pub use sqlx::SqlitePool;
+
pub async fn connect(path: &Path) -> Result<SqlitePool, RangerError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;