Consolidate workspace into a single crate
Two-crate workspace added complexity without benefit for a solo project.
Library stays at src/, binary at src/bin/ranger/, tests at tests/.

Assisted-by: Claude Opus 4.6 via pi
change kvqmulwrnrrowrunvsyutqpolpzvpomz
commit 5b464afc21f19b27785a21877cd0b98b261675f2
author Alpha Chen <alpha@kejadlen.dev>
date
parent toqytlvp
diff --git a/AGENTS.md b/AGENTS.md
index 5b418c3..495646f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,16 +2,16 @@
 
 ## Project
 
-Personal task tracker. Rust workspace: `ranger-lib` (library) + `ranger-cli` (binary). SQLite via sqlx, async with tokio, CLI with clap.
+Personal task tracker. Single Rust crate with a library and binary target. SQLite via sqlx, async with tokio, CLI with clap.
 
 ## Commands
 
 ```bash
-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
+just fmt                          # Format all code
+just check                        # Type-check
+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
 ```
 
@@ -90,39 +90,37 @@ Always use the `working-in-the-open` skill when working on ranger tasks. Use `ra
 ## Architecture
 
 ```
-crates/
-├── ranger-lib/          # Core library
-│   ├── src/
-│   │   ├── db.rs        # SQLite connection, migrations
-│   │   ├── error.rs     # Error types
-│   │   ├── key.rs       # jj-style key generation
-│   │   ├── models.rs    # Data types (Backlog, Task, Comment, Tag, Blocker)
-│   │   ├── position.rs  # Lexicographic fractional indexing
-│   │   └── ops/         # CRUD operations per model
-│   └── migrations/      # SQL schema
-└── ranger-cli/          # CLI binary
-    ├── src/
-    │   ├── main.rs      # Entrypoint, clap setup, DB path resolution
-    │   ├── output.rs    # Human/JSON output helpers
-    │   └── commands/    # One module per subcommand group
-    └── tests/
-        └── cli.rs       # End-to-end integration test
+src/
+├── lib.rs               # Library root
+├── db.rs                # SQLite connection, migrations
+├── error.rs             # Error types
+├── key.rs               # jj-style key generation
+├── models.rs            # Data types (Backlog, Task, Comment, Tag, Blocker)
+├── position.rs          # Lexicographic fractional indexing
+├── ops/                 # CRUD operations per model
+└── bin/ranger/          # CLI binary
+    ├── main.rs          # Entrypoint, clap setup, DB path resolution
+    ├── output.rs        # Human/JSON output helpers
+    └── commands/        # One module per subcommand group
+migrations/              # SQL schema
+tests/
+└── cli.rs               # End-to-end integration test
 ```
 
 ## Key Design Decisions
 
 - **Keys**: jj-style random strings (16 chars, `k-z` alphabet). Reference by shortest unique prefix.
 - **Positioning**: Lexicographic string-based ordering within backlogs. Insert between two positions without renumbering.
+- **Single crate**: Library (`src/lib.rs`) and binary (`src/bin/ranger/`) in one crate. No workspace.
 - **Tasks in multiple backlogs**: A task can belong to multiple backlogs via `backlog_tasks` join table, with independent positions.
 - **Subtasks are tasks**: `parent_id` on tasks — subtasks get full task capabilities.
 - **No compile-time checked queries**: Using `sqlx::query_as` with runtime binding, not `query_as!` macros. No need for `DATABASE_URL` at build time.
-- **Dependencies unpinned**: `Cargo.toml` uses `"*"` versions; `Cargo.lock` pins exact versions.
 
 ## Testing
 
 Tests use `tempfile` for isolated SQLite databases. Each test creates its own DB — no shared state.
 
-The integration test (`crates/ranger-cli/tests/cli.rs`) exercises the full workflow via the compiled binary using `assert_cmd`.
+The integration test (`tests/cli.rs`) exercises the full workflow via the compiled binary using `assert_cmd`.
 
 ## Gotchas
 
diff --git a/Cargo.lock b/Cargo.lock
index 76b8add..007d55d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1171,25 +1171,13 @@ dependencies = [
 ]
 
 [[package]]
-name = "ranger-cli"
+name = "ranger"
 version = "0.1.0"
 dependencies = [
  "anyhow",
  "assert_cmd",
- "clap",
- "ranger-lib",
- "serde",
- "serde_json",
- "tempfile",
- "tokio",
- "xdg",
-]
-
-[[package]]
-name = "ranger-lib"
-version = "0.1.0"
-dependencies = [
  "chrono",
+ "clap",
  "rand 0.9.2",
  "serde",
  "serde_json",
@@ -1197,6 +1185,7 @@ dependencies = [
  "tempfile",
  "thiserror",
  "tokio",
+ "xdg",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 0ed4b39..6437243 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,24 @@
-[workspace]
-members = ["crates/ranger-lib", "crates/ranger-cli"]
-resolver = "2"
+[package]
+name = "ranger"
+version = "0.1.0"
+edition = "2024"
+
+[[bin]]
+name = "ranger"
+path = "src/bin/ranger/main.rs"
+
+[dependencies]
+anyhow = "*"
+chrono = { version = "*", features = ["serde"] }
+clap = { version = "*", features = ["derive", "env"] }
+rand = "0.9"
+serde = { version = "*", features = ["derive"] }
+serde_json = "*"
+sqlx = { version = "*", features = ["runtime-tokio", "sqlite"] }
+thiserror = "*"
+tokio = { version = "*", features = ["full"] }
+xdg = "*"
+
+[dev-dependencies]
+assert_cmd = "*"
+tempfile = "*"
diff --git a/README.md b/README.md
index 9485d2c..3caef38 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ The webapp uses an expanding modal for editing tasks — no cluttered inline edi
 Build and install:
 
 ```
-cargo install --path crates/ranger-cli
+cargo install --path .
 ```
 
 Or run directly:
@@ -71,10 +71,10 @@ The database lives at `$XDG_DATA_HOME/ranger/ranger.db` by default. Override wit
 
 ## Architecture
 
-Ranger ships as three artifacts from one Rust codebase:
+Ranger is a single Rust crate with a library and binary target:
 
-- **Library** (`ranger-lib`) — core data model, database operations, key generation
-- **CLI** (`ranger-cli`) — clap-based binary for humans and AI agents
+- **Library** (`ranger`) — core data model, database operations, key generation
+- **CLI** (`ranger` binary) — clap-based binary for humans and AI agents
 - **Webapp** — for human use (planned)
 
 The CLI exists primarily so AI agents can manage tasks programmatically. The webapp exists for humans who prefer a visual interface.
diff --git a/crates/ranger-cli/Cargo.toml b/crates/ranger-cli/Cargo.toml
deleted file mode 100644
index 7d6f82f..0000000
--- a/crates/ranger-cli/Cargo.toml
+++ /dev/null
@@ -1,22 +0,0 @@
-[package]
-name = "ranger-cli"
-version = "0.1.0"
-edition = "2024"
-
-[[bin]]
-name = "ranger"
-path = "src/main.rs"
-
-[dependencies]
-ranger-lib = { path = "../ranger-lib" }
-clap = { version = "*", features = ["derive", "env"] }
-tokio = { version = "*", features = ["full"] }
-serde = { version = "*", features = ["derive"] }
-serde_json = "*"
-anyhow = "*"
-xdg = "*"
-
-[dev-dependencies]
-assert_cmd = "*"
-serde_json = "*"
-tempfile = "*"
diff --git a/crates/ranger-lib/Cargo.toml b/crates/ranger-lib/Cargo.toml
deleted file mode 100644
index 5eaca21..0000000
--- a/crates/ranger-lib/Cargo.toml
+++ /dev/null
@@ -1,16 +0,0 @@
-[package]
-name = "ranger-lib"
-version = "0.1.0"
-edition = "2024"
-
-[dependencies]
-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 = "*"
diff --git a/crates/ranger-lib/migrations/001_initial.sql b/migrations/001_initial.sql
similarity index 100%
rename from crates/ranger-lib/migrations/001_initial.sql
rename to migrations/001_initial.sql
diff --git a/crates/ranger-cli/src/commands/backlog.rs b/src/bin/ranger/commands/backlog.rs
similarity index 93%
rename from crates/ranger-cli/src/commands/backlog.rs
rename to src/bin/ranger/commands/backlog.rs
index e3b803e..668f1f3 100644
--- a/crates/ranger-cli/src/commands/backlog.rs
+++ b/src/bin/ranger/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::db::SqlitePool;
+use ranger::models::Backlog;
+use ranger::ops;
 
 use crate::output;
 
diff --git a/crates/ranger-cli/src/commands/blocker.rs b/src/bin/ranger/commands/blocker.rs
similarity index 96%
rename from crates/ranger-cli/src/commands/blocker.rs
rename to src/bin/ranger/commands/blocker.rs
index cfae4e0..921e9d3 100644
--- a/crates/ranger-cli/src/commands/blocker.rs
+++ b/src/bin/ranger/commands/blocker.rs
@@ -1,6 +1,6 @@
 use clap::Subcommand;
-use ranger_lib::db::SqlitePool;
-use ranger_lib::ops;
+use ranger::db::SqlitePool;
+use ranger::ops;
 
 use crate::output;
 
diff --git a/crates/ranger-cli/src/commands/comment.rs b/src/bin/ranger/commands/comment.rs
similarity index 95%
rename from crates/ranger-cli/src/commands/comment.rs
rename to src/bin/ranger/commands/comment.rs
index 134822f..95b2cff 100644
--- a/crates/ranger-cli/src/commands/comment.rs
+++ b/src/bin/ranger/commands/comment.rs
@@ -1,6 +1,6 @@
 use clap::Subcommand;
-use ranger_lib::db::SqlitePool;
-use ranger_lib::ops;
+use ranger::db::SqlitePool;
+use ranger::ops;
 
 use crate::output;
 
diff --git a/crates/ranger-cli/src/commands/mod.rs b/src/bin/ranger/commands/mod.rs
similarity index 100%
rename from crates/ranger-cli/src/commands/mod.rs
rename to src/bin/ranger/commands/mod.rs
diff --git a/crates/ranger-cli/src/commands/tag.rs b/src/bin/ranger/commands/tag.rs
similarity index 89%
rename from crates/ranger-cli/src/commands/tag.rs
rename to src/bin/ranger/commands/tag.rs
index 8c87496..841956f 100644
--- a/crates/ranger-cli/src/commands/tag.rs
+++ b/src/bin/ranger/commands/tag.rs
@@ -1,6 +1,6 @@
 use clap::Subcommand;
-use ranger_lib::db::SqlitePool;
-use ranger_lib::ops;
+use ranger::db::SqlitePool;
+use ranger::ops;
 
 use crate::output;
 
diff --git a/crates/ranger-cli/src/commands/task.rs b/src/bin/ranger/commands/task.rs
similarity index 99%
rename from crates/ranger-cli/src/commands/task.rs
rename to src/bin/ranger/commands/task.rs
index 18423c6..b6950ab 100644
--- a/crates/ranger-cli/src/commands/task.rs
+++ b/src/bin/ranger/commands/task.rs
@@ -1,7 +1,7 @@
 use clap::Subcommand;
-use ranger_lib::db::SqlitePool;
-use ranger_lib::models::Task;
-use ranger_lib::ops;
+use ranger::db::SqlitePool;
+use ranger::models::Task;
+use ranger::ops;
 
 use crate::output;
 
diff --git a/crates/ranger-cli/src/main.rs b/src/bin/ranger/main.rs
similarity index 97%
rename from crates/ranger-cli/src/main.rs
rename to src/bin/ranger/main.rs
index 0fa6f48..eb40d2c 100644
--- a/crates/ranger-cli/src/main.rs
+++ b/src/bin/ranger/main.rs
@@ -61,7 +61,7 @@ fn resolve_db_path(cli_path: Option<PathBuf>) -> PathBuf {
 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?;
+    let pool = ranger::db::connect(&db_path).await?;
 
     match cli.command {
         Commands::Backlog { command } => {
diff --git a/crates/ranger-cli/src/output.rs b/src/bin/ranger/output.rs
similarity index 100%
rename from crates/ranger-cli/src/output.rs
rename to src/bin/ranger/output.rs
diff --git a/crates/ranger-lib/src/db.rs b/src/db.rs
similarity index 100%
rename from crates/ranger-lib/src/db.rs
rename to src/db.rs
diff --git a/crates/ranger-lib/src/error.rs b/src/error.rs
similarity index 100%
rename from crates/ranger-lib/src/error.rs
rename to src/error.rs
diff --git a/crates/ranger-lib/src/key.rs b/src/key.rs
similarity index 100%
rename from crates/ranger-lib/src/key.rs
rename to src/key.rs
diff --git a/crates/ranger-lib/src/lib.rs b/src/lib.rs
similarity index 100%
rename from crates/ranger-lib/src/lib.rs
rename to src/lib.rs
diff --git a/crates/ranger-lib/src/models.rs b/src/models.rs
similarity index 100%
rename from crates/ranger-lib/src/models.rs
rename to src/models.rs
diff --git a/crates/ranger-lib/src/ops/backlog.rs b/src/ops/backlog.rs
similarity index 100%
rename from crates/ranger-lib/src/ops/backlog.rs
rename to src/ops/backlog.rs
diff --git a/crates/ranger-lib/src/ops/blocker.rs b/src/ops/blocker.rs
similarity index 100%
rename from crates/ranger-lib/src/ops/blocker.rs
rename to src/ops/blocker.rs
diff --git a/crates/ranger-lib/src/ops/comment.rs b/src/ops/comment.rs
similarity index 100%
rename from crates/ranger-lib/src/ops/comment.rs
rename to src/ops/comment.rs
diff --git a/crates/ranger-lib/src/ops/mod.rs b/src/ops/mod.rs
similarity index 100%
rename from crates/ranger-lib/src/ops/mod.rs
rename to src/ops/mod.rs
diff --git a/crates/ranger-lib/src/ops/tag.rs b/src/ops/tag.rs
similarity index 100%
rename from crates/ranger-lib/src/ops/tag.rs
rename to src/ops/tag.rs
diff --git a/crates/ranger-lib/src/ops/task.rs b/src/ops/task.rs
similarity index 100%
rename from crates/ranger-lib/src/ops/task.rs
rename to src/ops/task.rs
diff --git a/crates/ranger-lib/src/position.rs b/src/position.rs
similarity index 100%
rename from crates/ranger-lib/src/position.rs
rename to src/position.rs
diff --git a/crates/ranger-cli/tests/cli.rs b/tests/cli.rs
similarity index 100%
rename from crates/ranger-cli/tests/cli.rs
rename to tests/cli.rs
index 0eb181f..ea57721 100644
--- a/crates/ranger-cli/tests/cli.rs
+++ b/tests/cli.rs
@@ -1,5 +1,5 @@
-use assert_cmd::Command;
 use assert_cmd::cargo::cargo_bin_cmd;
+use assert_cmd::Command;
 use tempfile::tempdir;
 
 fn ranger(db_path: &str) -> Command {