chore: plan hltb CLI (HowLongToBeat search tool)

Milestone vibed-f05l and its seven subtasks laying out a new Rust CLI
binary that queries howlongtobeat.com and prints completion times.
Covers project init, request/response types, CLI arg parsing, the
HTTP search call, output formatting, and a validation/smoke-test pass.
No implementation yet — planning only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
main
Elijah Voigt 6 days ago
parent 2a2c607e70
commit e42c3c731c

@ -0,0 +1,24 @@
---
# vibed-0t6e
title: Add API request types
status: todo
type: task
created_at: 2026-04-11T22:34:39Z
updated_at: 2026-04-11T22:34:39Z
parent: vibed-f05l
blocked_by:
- vibed-ca4q
---
Implement the full `SearchRequest` struct tree for the HLTB POST body.
## Todo
- [ ] `SearchRequest` with searchType, searchTerms, searchPage, size, searchOptions, useCache
- [ ] `SearchOptions` with games, users, lists, filter, sort, randomizer
- [ ] `GamesOptions` with userId, platform, sortCategory, rangeCategory, rangeTime, gameplay, rangeYear, modifier
- [ ] `RangeTime { min: Option<u32>, max: Option<u32> }`
- [ ] `Gameplay` with perspective, flow, genre, subGenre
- [ ] `RangeYear { min: &str, max: &str }`
- [ ] `UsersOptions` and `ListsOptions` with sortCategory
- [ ] All structs derive `serde::Serialize` with correct `#[serde(rename = ...)]` annotations
- [ ] `cargo check` passes

@ -0,0 +1,21 @@
---
# vibed-54u4
title: Add CLI argument parsing
status: todo
type: task
created_at: 2026-04-11T22:34:31Z
updated_at: 2026-04-11T22:34:31Z
parent: vibed-f05l
blocked_by:
- vibed-ca4q
---
Implement the `Args` struct using `clap` derive API.
## Todo
- [ ] Add `use clap::Parser;` and derive `Args` struct
- [ ] Variadic positional `title: Vec<String>` with `num_args = 1..`
- [ ] `--json` bool flag (output raw JSON)
- [ ] `--all` bool flag (show all results, not just top match)
- [ ] Call `Args::parse()` in `main` and join title words for display
- [ ] `cargo check` passes

@ -0,0 +1,25 @@
---
# vibed-90il
title: Validation & smoke test
status: todo
type: task
created_at: 2026-04-11T22:35:32Z
updated_at: 2026-04-11T22:35:32Z
parent: vibed-f05l
blocked_by:
- vibed-iq16
- vibed-mlgv
---
Run all validation commands and live smoke tests against the real API.
## Todo
- [ ] `cargo fmt --check` passes
- [ ] `cargo check` passes
- [ ] `cargo clippy -- -D warnings` passes
- [ ] `cargo build --release` succeeds
- [ ] `./target/release/hltb halo` returns results
- [ ] `./target/release/hltb the witcher 3` works without quotes
- [ ] `./target/release/hltb --all zelda` shows numbered list
- [ ] `./target/release/hltb --json hollow knight` outputs valid JSON
- [ ] `./target/release/hltb xyzzy_no_game_zzzq` prints "No results found."

@ -0,0 +1,16 @@
---
# vibed-ca4q
title: Initialize Rust project
status: todo
type: task
created_at: 2026-04-11T22:34:25Z
updated_at: 2026-04-11T22:34:25Z
parent: vibed-f05l
---
Create `Cargo.toml` and `src/main.rs` skeleton for the `hltb` binary.
## Todo
- [ ] Write `Cargo.toml` with package metadata, [[bin]], dependencies (clap, reqwest, serde, serde_json, tokio), and release profile
- [ ] Write minimal `src/main.rs` with a stub `main()`
- [ ] Run `cargo check` and confirm it passes

@ -0,0 +1,10 @@
---
# vibed-f05l
title: hltb CLI — HowLongToBeat search tool
status: todo
type: milestone
created_at: 2026-04-11T22:33:37Z
updated_at: 2026-04-11T22:33:37Z
---
A Rust CLI binary that searches HowLongToBeat.com and prints completion times (Main Story / Main + Extras / Completionist) for any game. Single binary named `hltb`, uses reqwest + clap + serde + tokio.

@ -0,0 +1,22 @@
---
# vibed-iq16
title: Implement HTTP search function
status: todo
type: task
created_at: 2026-04-11T22:35:13Z
updated_at: 2026-04-11T22:35:13Z
parent: vibed-f05l
blocked_by:
- vibed-0t6e
- vibed-mn9a
---
Implement `async fn search(query: &str) -> Result<SearchResponse, AppError>` using reqwest.
## Todo
- [ ] Define `AppError { Network(reqwest::Error), Parse(serde_json::Error) }` with Display + From impls
- [ ] Implement `search()`: split query on whitespace for searchTerms, build full SearchRequest body
- [ ] POST to `https://howlongtobeat.com/api/search` with headers: User-Agent (Firefox), Referer, Content-Type
- [ ] Use `.json(&body)` for request and `.json::<SearchResponse>()` for response
- [ ] Wire into `main`: call search, match Ok/Err, print error to stderr and exit(1) on failure
- [ ] `cargo check` passes

@ -0,0 +1,24 @@
---
# vibed-mlgv
title: Implement display formatting
status: todo
type: task
created_at: 2026-04-11T22:35:25Z
updated_at: 2026-04-11T22:35:25Z
parent: vibed-f05l
blocked_by:
- vibed-mn9a
- vibed-54u4
---
Implement output formatting functions and the main display dispatcher.
## Todo
- [ ] `fn format_duration(seconds: u32) -> String`: return "N/A" for 0, else "Xh YYm"
- [ ] `fn print_game(game: &GameResult, prefix: Option<&str>)`: print name then 3 time lines with fixed-width labels
- [ ] `fn display(resp: SearchResponse, args: &Args)`: check for zero results, dispatch to --json / --all / default paths
- [ ] Default path: show only `data[0]`
- [ ] `--all` path: numbered list of all results
- [ ] `--json` path: `serde_json::to_string_pretty` of `data[0]` (or full vec with --all)
- [ ] Zero results: print "No results found." and return
- [ ] `cargo check` passes

@ -0,0 +1,19 @@
---
# vibed-mn9a
title: Add API response types
status: todo
type: task
created_at: 2026-04-11T22:35:02Z
updated_at: 2026-04-11T22:35:02Z
parent: vibed-f05l
blocked_by:
- vibed-ca4q
---
Implement `SearchResponse` and `GameResult` deserialization types.
## Todo
- [ ] `SearchResponse { count: u32, data: Vec<GameResult> }` with `Deserialize`
- [ ] `GameResult` with game_id, game_name, comp_main, comp_plus, comp_100, profile_steam, similarity — derive both `Deserialize` and `Serialize` (needed for --json output)
- [ ] Note: comp_main/plus/100 are in **seconds**; 0 means N/A
- [ ] `cargo check` passes
Loading…
Cancel
Save