Add CLI scaffolding and backlog commands
Unpin dependency versions to "*" while here.

Assisted-by: Claude Opus 4.6 via pi
change kkwylplrvqxvzrxvvxkzuxmyynszmlts
commit e4b66ec34a30885eca91aefec47766d5bb7cdfde
author Alpha Chen <alpha@kejadlen.dev>
date
parent olzmxwon
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)?;