refactor(cli): restructure commands from flat to nested subcommands

Change CLI structure from `brewlog {verb}-{entity}` to `brewlog {entity} {verb}`:
- brewlog add-roaster → brewlog roaster add
- brewlog list-bags → brewlog bag list
- brewlog create-token → brewlog token create
- etc.

Each entity module now owns its subcommand enum and dispatch logic,
simplifying main.rs and improving discoverability via `brewlog {entity} --help`.

- Add RoasterCommands, RoastCommands, BagCommands, GearCommands, TokenCommands enums
- Add run() dispatcher to each entity module
- Simplify top-level Commands enum to delegate to entity modules
- Update all CLI tests and bootstrap script
This commit is contained in:
Jon Seager 2026-02-02 17:02:46 +00:00
parent 4edc66f44a
commit 0090c4ba43
No known key found for this signature in database
14 changed files with 392 additions and 285 deletions

View file

@ -3,7 +3,7 @@ set -euo pipefail
cargo build
BREWLOG_TOKEN="$(./target/debug/brewlog create-token --name "bootstrap-token" --username admin --password password | grep -Po "BREWLOG_TOKEN=\K.+$")"
BREWLOG_TOKEN="$(./target/debug/brewlog token create --name "bootstrap-token" --username admin --password password | grep -Po "BREWLOG_TOKEN=\K.+$")"
export BREWLOG_TOKEN
if [[ -z "$BREWLOG_TOKEN" ]]; then
@ -12,15 +12,15 @@ if [[ -z "$BREWLOG_TOKEN" ]]; then
fi
# Tim Wendelboe (Norway)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "Tim Wendelboe" \
--country "Norway" \
--city "Oslo" \
--homepage "https://timwendelboe.no" \
--notes "World-renowned Nordic micro-roastery dedicated to clarity and sustainability."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Tim Wendelboe") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Tim Wendelboe") | .id')" \
--name "Ben Saïd Natural" \
--origin "Ethiopia" \
--region "Sidamo" \
@ -28,8 +28,8 @@ fi
--process "Natural" \
--tasting-notes "Bergamot, Apricot, Floral"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Tim Wendelboe") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Tim Wendelboe") | .id')" \
--name "Finca Tamana Washed" \
--origin "Colombia" \
--region "El Pital, Huila" \
@ -39,15 +39,15 @@ fi
# Coffee Collective (Denmark)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "Coffee Collective" \
--country "Denmark" \
--city "Copenhagen" \
--homepage "https://coffeecollective.dk" \
--notes "Pioneers of transparency and sustainability; multi-time Nordic roaster award winners."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Coffee Collective") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Coffee Collective") | .id')" \
--name "Daterra Sweet Collection" \
--origin "Brazil" \
--region "Cerrado" \
@ -55,8 +55,8 @@ fi
--process "Pulped Natural" \
--tasting-notes "Hazelnut, Milk Chocolate, Yellow Fruit"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Coffee Collective") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Coffee Collective") | .id')" \
--name "Kieni" \
--origin "Kenya" \
--region "Nyeri" \
@ -66,15 +66,15 @@ fi
# Drop Coffee (Sweden)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "Drop Coffee" \
--country "Sweden" \
--city "Stockholm" \
--homepage "https://dropcoffee.com" \
--notes "Award-winning Swedish roastery prized for its elegance and clean Scandinavian style."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Drop Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Drop Coffee") | .id')" \
--name "La Linda" \
--origin "Bolivia" \
--region "Caranavi" \
@ -82,8 +82,8 @@ fi
--process "Washed" \
--tasting-notes "Red Apple, Caramel, Floral"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Drop Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Drop Coffee") | .id')" \
--name "El Sunzita" \
--origin "El Salvador" \
--region "Ahuachapan" \
@ -93,15 +93,15 @@ fi
# La Cabra (Denmark)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "La Cabra" \
--country "Denmark" \
--city "Aarhus" \
--homepage "https://www.lacabra.dk" \
--notes "Scandinavian minimalist roastery known for clarity and innovative sourcing."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="La Cabra") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="La Cabra") | .id')" \
--name "Halo Beriti" \
--origin "Ethiopia" \
--region "Yirgacheffe" \
@ -109,8 +109,8 @@ fi
--process "Washed" \
--tasting-notes "Jasmine, Lemon, Stone Fruit"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="La Cabra") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="La Cabra") | .id')" \
--name "Cerro Azul" \
--origin "Colombia" \
--region "Valle del Cauca" \
@ -120,15 +120,15 @@ fi
# April Coffee (Denmark)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "April Coffee" \
--country "Denmark" \
--city "Copenhagen" \
--homepage "https://aprilcoffeeroasters.com" \
--notes "Modern approach to Nordic coffee, emphasizing transparency and traceability."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="April Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="April Coffee") | .id')" \
--name "El Salvador Pacamara" \
--origin "El Salvador" \
--region "Santa Ana" \
@ -136,8 +136,8 @@ fi
--process "Honey" \
--tasting-notes "Grapefruit, Sugar Cane, Plum"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="April Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="April Coffee") | .id')" \
--name "Guji Highland" \
--origin "Ethiopia" \
--region "Guji" \
@ -147,15 +147,15 @@ fi
# Assembly Coffee (UK)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "Assembly Coffee" \
--country "UK" \
--city "London" \
--homepage "https://assemblycoffee.co.uk" \
--notes "Based in Brixton, Assembly focuses on collaborative sourcing and education."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Assembly Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Assembly Coffee") | .id')" \
--name "Kochere" \
--origin "Ethiopia" \
--region "Yirgacheffe" \
@ -163,8 +163,8 @@ fi
--process "Washed" \
--tasting-notes "Peach, Lemon, Jasmine"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Assembly Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Assembly Coffee") | .id')" \
--name "La Laja" \
--origin "Mexico" \
--region "Veracruz" \
@ -174,15 +174,15 @@ fi
# Square Mile (UK)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "Square Mile Coffee" \
--country "UK" \
--city "London" \
--homepage "https://squaremilecoffee.com" \
--notes "One of London's pioneers; delivers balanced and clear, fruit-forward coffees."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Square Mile Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Square Mile Coffee") | .id')" \
--name "Red Brick Espresso" \
--origin "Blend" \
--region "Multiple Origins" \
@ -190,8 +190,8 @@ fi
--process "Washed, Natural" \
--tasting-notes "Berry, Chocolate, Citrus"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Square Mile Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Square Mile Coffee") | .id')" \
--name "Kamwangi" \
--origin "Kenya" \
--region "Kirinyaga" \
@ -201,15 +201,15 @@ fi
# Dak Coffee Roasters (Netherlands)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "Dak Coffee Roasters" \
--country "Netherlands" \
--city "Amsterdam" \
--homepage "https://www.dakcoffeeroasters.com" \
--notes "Highly experimental Dutch roastery; celebrates vibrant acidity and alternative processing."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Dak Coffee Roasters") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Dak Coffee Roasters") | .id')" \
--name "El Paraiso 92 Anaerobic" \
--origin "Colombia" \
--region "Cauca" \
@ -217,8 +217,8 @@ fi
--process "Thermal Shock Anaerobic" \
--tasting-notes "Passionfruit, Raspberry, Yogurt"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Dak Coffee Roasters") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Dak Coffee Roasters") | .id')" \
--name "Oreti SL28" \
--origin "Kenya" \
--region "Kirinyaga" \
@ -228,15 +228,15 @@ fi
# Bonanza Coffee (Germany)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "Bonanza Coffee" \
--country "Germany" \
--city "Berlin" \
--homepage "https://www.bonanzacoffee.de" \
--notes "Pioneering Berlin roastery focused on brightness, balance, and freshness."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Bonanza Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Bonanza Coffee") | .id')" \
--name "Gatomboya" \
--origin "Kenya" \
--region "Nyeri" \
@ -244,8 +244,8 @@ fi
--process "Washed" \
--tasting-notes "Blackcurrant, Lime, Tomato"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Bonanza Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Bonanza Coffee") | .id')" \
--name "Los Pirineos" \
--origin "El Salvador" \
--region "Usulután" \
@ -255,15 +255,15 @@ fi
# Friedhats (Netherlands)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "Friedhats" \
--country "Netherlands" \
--city "Amsterdam" \
--homepage "https://friedhats.com" \
--notes "Quirky branding meets serious, awarded, fruit-forward coffees from Amsterdam."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Friedhats") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Friedhats") | .id')" \
--name "Sidamo Guji" \
--origin "Ethiopia" \
--region "Guji" \
@ -271,8 +271,8 @@ fi
--process "Natural" \
--tasting-notes "Peach, Raspberry, Rosehip"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Friedhats") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Friedhats") | .id')" \
--name "La Esmeralda Geisha" \
--origin "Panama" \
--region "Boquete" \
@ -282,15 +282,15 @@ fi
# Origin Coffee (UK)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "Origin Coffee" \
--country "UK" \
--city "Porthleven" \
--homepage "https://origincoffee.co.uk" \
--notes "Specialty roaster with close partnerships at origin; leading UK scene with cutting-edge lots."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Origin Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Origin Coffee") | .id')" \
--name "San Fermin" \
--origin "Colombia" \
--region "Tolima" \
@ -298,8 +298,8 @@ fi
--process "Washed" \
--tasting-notes "Red Grape, Caramel, Blood Orange"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Origin Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Origin Coffee") | .id')" \
--name "Aricha" \
--origin "Ethiopia" \
--region "Yirgacheffe" \
@ -309,15 +309,15 @@ fi
# Dark Arts Coffee (UK)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "Dark Arts Coffee" \
--country "UK" \
--city "London" \
--homepage "https://www.darkartscoffee.co.uk" \
--notes "Playful, disruptive roaster with a cult following and flavor-forward offerings."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Dark Arts Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Dark Arts Coffee") | .id')" \
--name "Death to Decaf" \
--origin "Brazil" \
--region "Minas Gerais" \
@ -325,8 +325,8 @@ fi
--process "Swiss Water Decaf" \
--tasting-notes "Cocoa, Cherry, Almond"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Dark Arts Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Dark Arts Coffee") | .id')" \
--name "Snoop" \
--origin "Guatemala" \
--region "Huehuetenango" \
@ -336,15 +336,15 @@ fi
# KAWA Coffee (France)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "KAWA Coffee" \
--country "France" \
--city "Paris" \
--homepage "https://www.kawa.coffee" \
--notes "One of Paris most exciting specialty roasteries, known for unusual and competition-level lots."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="KAWA Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="KAWA Coffee") | .id')" \
--name "Sudan Rume" \
--origin "Colombia" \
--region "Cauca" \
@ -352,8 +352,8 @@ fi
--process "Natural" \
--tasting-notes "Strawberry, Cinnamon, Grape"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="KAWA Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="KAWA Coffee") | .id')" \
--name "Arbegona" \
--origin "Ethiopia" \
--region "Sidama" \
@ -363,15 +363,15 @@ fi
# Stow Coffee (Slovenia)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "Stow Coffee" \
--country "Slovenia" \
--city "Ljubljana" \
--homepage "https://www.stowcoffee.com" \
--notes "Slovenias specialty leader, awarded for pure, brightly acidic profiles and innovation."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Stow Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Stow Coffee") | .id')" \
--name "Santa Barbara" \
--origin "Honduras" \
--region "Santa Barbara" \
@ -379,8 +379,8 @@ fi
--process "Honey" \
--tasting-notes "Red Currant, Honeydew, Cocoa"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Stow Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Stow Coffee") | .id')" \
--name "Suke Quto" \
--origin "Ethiopia" \
--region "Guji" \
@ -390,15 +390,15 @@ fi
# Bows Coffee (Canada)
./target/debug/brewlog add-roaster \
./target/debug/brewlog roaster add \
--name "Bows Coffee" \
--country "Canada" \
--city "Victoria" \
--homepage "https://bowscoffee.com" \
--notes "Canadian micro-roaster with focus on clarity, complexity, and ethical sourcing."
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Bows Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Bows Coffee") | .id')" \
--name "La Chumeca" \
--origin "Costa Rica" \
--region "Tarrazú" \
@ -406,8 +406,8 @@ fi
--process "White Honey" \
--tasting-notes "Mandarin, Honeycomb, Almond"
./target/debug/brewlog add-roast \
--roaster-id "$(./target/debug/brewlog list-roasters | jq -r '.[] | select(.name=="Bows Coffee") | .id')" \
./target/debug/brewlog roast add \
--roaster-id "$(./target/debug/brewlog roaster list | jq -r '.[] | select(.name=="Bows Coffee") | .id')" \
--name "Simbi" \
--origin "Rwanda" \
--region "Huye" \
@ -420,74 +420,74 @@ fi
# ============================================================================
# Tim Wendelboe - Ben Saïd Natural (250g)
./target/debug/brewlog add-bag \
--roast-id "$(./target/debug/brewlog list-roasts | jq -r '.[] | select(.name=="Ben Saïd Natural") | .id')" \
./target/debug/brewlog bag add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Ben Saïd Natural") | .id')" \
--roast-date "2026-01-15" \
--amount 250
# Tim Wendelboe - Finca Tamana Washed (350g)
./target/debug/brewlog add-bag \
--roast-id "$(./target/debug/brewlog list-roasts | jq -r '.[] | select(.name=="Finca Tamana Washed") | .id')" \
./target/debug/brewlog bag add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Finca Tamana Washed") | .id')" \
--roast-date "2026-01-18" \
--amount 350
# Coffee Collective - Daterra Sweet Collection (200g)
./target/debug/brewlog add-bag \
--roast-id "$(./target/debug/brewlog list-roasts | jq -r '.[] | select(.name=="Daterra Sweet Collection") | .id')" \
./target/debug/brewlog bag add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Daterra Sweet Collection") | .id')" \
--roast-date "2026-01-10" \
--amount 200
# Drop Coffee - La Linda (500g)
./target/debug/brewlog add-bag \
--roast-id "$(./target/debug/brewlog list-roasts | jq -r '.[] | select(.name=="La Linda") | .id')" \
./target/debug/brewlog bag add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="La Linda") | .id')" \
--roast-date "2026-01-20" \
--amount 500
# La Cabra - Halo Beriti (150g)
./target/debug/brewlog add-bag \
--roast-id "$(./target/debug/brewlog list-roasts | jq -r '.[] | select(.name=="Halo Beriti") | .id')" \
./target/debug/brewlog bag add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Halo Beriti") | .id')" \
--roast-date "2026-01-12" \
--amount 150
# April Coffee - Guji Highland (300g)
./target/debug/brewlog add-bag \
--roast-id "$(./target/debug/brewlog list-roasts | jq -r '.[] | select(.name=="Guji Highland") | .id')" \
./target/debug/brewlog bag add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Guji Highland") | .id')" \
--roast-date "2026-01-22" \
--amount 300
# Assembly Coffee - Kochere (250g)
./target/debug/brewlog add-bag \
--roast-id "$(./target/debug/brewlog list-roasts | jq -r '.[] | select(.name=="Kochere") | .id')" \
./target/debug/brewlog bag add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Kochere") | .id')" \
--roast-date "2026-01-08" \
--amount 250
# Square Mile Coffee - Red Brick Espresso (400g)
./target/debug/brewlog add-bag \
--roast-id "$(./target/debug/brewlog list-roasts | jq -r '.[] | select(.name=="Red Brick Espresso") | .id')" \
./target/debug/brewlog bag add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Red Brick Espresso") | .id')" \
--roast-date "2026-01-25" \
--amount 400
# Dak Coffee Roasters - El Paraiso 92 Anaerobic (100g - small competition lot)
./target/debug/brewlog add-bag \
--roast-id "$(./target/debug/brewlog list-roasts | jq -r '.[] | select(.name=="El Paraiso 92 Anaerobic") | .id')" \
./target/debug/brewlog bag add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="El Paraiso 92 Anaerobic") | .id')" \
--roast-date "2026-01-28" \
--amount 100
# Bonanza Coffee - Gatomboya (175g)
./target/debug/brewlog add-bag \
--roast-id "$(./target/debug/brewlog list-roasts | jq -r '.[] | select(.name=="Gatomboya") | .id')" \
./target/debug/brewlog bag add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Gatomboya") | .id')" \
--roast-date "2026-01-05" \
--amount 175
# Stow Coffee - Suke Quto (225g)
./target/debug/brewlog add-bag \
--roast-id "$(./target/debug/brewlog list-roasts | jq -r '.[] | select(.name=="Suke Quto") | .id')" \
./target/debug/brewlog bag add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Suke Quto") | .id')" \
--roast-date "2026-01-30" \
--amount 225
# Bows Coffee - Simbi (450g)
./target/debug/brewlog add-bag \
--roast-id "$(./target/debug/brewlog list-roasts | jq -r '.[] | select(.name=="Simbi") | .id')" \
./target/debug/brewlog bag add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Simbi") | .id')" \
--roast-date "2026-01-14" \
--amount 450
@ -496,26 +496,26 @@ fi
# ============================================================================
# Finish Gatomboya bag (oldest - Jan 5)
./target/debug/brewlog update-bag \
--id "$(./target/debug/brewlog list-bags | jq -r '.[] | select(.roast_name=="Gatomboya") | .id')" \
./target/debug/brewlog bag update \
--id "$(./target/debug/brewlog bag list | jq -r '.[] | select(.roast_name=="Gatomboya") | .id')" \
--closed true \
--finished-at "2026-01-20"
# Finish Kochere bag (Jan 8)
./target/debug/brewlog update-bag \
--id "$(./target/debug/brewlog list-bags | jq -r '.[] | select(.roast_name=="Kochere") | .id')" \
./target/debug/brewlog bag update \
--id "$(./target/debug/brewlog bag list | jq -r '.[] | select(.roast_name=="Kochere") | .id')" \
--closed true \
--finished-at "2026-01-22"
# Finish Daterra Sweet Collection bag (Jan 10)
./target/debug/brewlog update-bag \
--id "$(./target/debug/brewlog list-bags | jq -r '.[] | select(.roast_name=="Daterra Sweet Collection") | .id')" \
./target/debug/brewlog bag update \
--id "$(./target/debug/brewlog bag list | jq -r '.[] | select(.roast_name=="Daterra Sweet Collection") | .id')" \
--closed true \
--finished-at "2026-01-25"
# Finish Halo Beriti bag (Jan 12)
./target/debug/brewlog update-bag \
--id "$(./target/debug/brewlog list-bags | jq -r '.[] | select(.roast_name=="Halo Beriti") | .id')" \
./target/debug/brewlog bag update \
--id "$(./target/debug/brewlog bag list | jq -r '.[] | select(.roast_name=="Halo Beriti") | .id')" \
--closed true \
--finished-at "2026-01-28"
@ -524,32 +524,33 @@ fi
# ============================================================================
# Grinders
./target/debug/brewlog add-gear \
./target/debug/brewlog gear add \
--category "grinder" \
--make "Comandante" \
--model "C40 MK4"
./target/debug/brewlog add-gear \
./target/debug/brewlog gear add \
--category "grinder" \
--make "1Zpresso" \
--model "J-Max"
# Brewers
./target/debug/brewlog add-gear \
./target/debug/brewlog gear add \
--category "brewer" \
--make "Hario" \
--model "V60 02"
./target/debug/brewlog add-gear \
./target/debug/brewlog gear add \
--category "brewer" \
--make "AeroPress" \
--model "Original"
./target/debug/brewlog add-gear \
./target/debug/brewlog gear add \
--category "brewer" \
--make "Fellow" \
--model "Stagg XF"
echo
echo "Bootstrapped database"
echo
echo "Set token $BREWLOG_TOKEN to use the data added here."

View file

@ -21,43 +21,25 @@ async fn main() -> Result<()> {
match cli.command {
Commands::Serve(cmd) => run_server(cmd).await,
command => {
Commands::Roaster { command } => {
let client = BrewlogClient::from_base_url(&cli.api_url)?;
match command {
// Tokens
Commands::CreateToken(cmd) => tokens::create_token(&client, cmd).await,
Commands::ListTokens => tokens::list_tokens(&client).await,
Commands::RevokeToken(cmd) => tokens::revoke_token(&client, cmd).await,
// Roasters
Commands::AddRoaster(cmd) => roasters::add_roaster(&client, cmd).await,
Commands::ListRoasters => roasters::list_roasters(&client).await,
Commands::GetRoaster(cmd) => roasters::get_roaster(&client, cmd).await,
Commands::UpdateRoaster(cmd) => roasters::update_roaster(&client, cmd).await,
Commands::DeleteRoaster(cmd) => roasters::delete_roaster(&client, cmd).await,
// Roasts
Commands::AddRoast(cmd) => roasts::add_roast(&client, cmd).await,
Commands::ListRoasts(cmd) => roasts::list_roasts(&client, cmd).await,
Commands::GetRoast(cmd) => roasts::get_roast(&client, cmd).await,
Commands::DeleteRoast(cmd) => roasts::delete_roast(&client, cmd).await,
// Bags
Commands::AddBag(cmd) => bags::add_bag(&client, cmd).await,
Commands::ListBags(cmd) => bags::list_bags(&client, cmd).await,
Commands::GetBag(cmd) => bags::get_bag(&client, cmd).await,
Commands::UpdateBag(cmd) => bags::update_bag(&client, cmd).await,
Commands::DeleteBag(cmd) => bags::delete_bag(&client, cmd).await,
// Gear
Commands::AddGear(cmd) => gear::add_gear(&client, cmd).await,
Commands::ListGear(cmd) => gear::list_gear(&client, cmd).await,
Commands::GetGear(cmd) => gear::get_gear(&client, cmd).await,
Commands::UpdateGear(cmd) => gear::update_gear(&client, cmd).await,
Commands::DeleteGear(cmd) => gear::delete_gear(&client, cmd).await,
Commands::Serve(_) => unreachable!("serve command handled earlier"),
roasters::run(&client, command).await
}
Commands::Roast { command } => {
let client = BrewlogClient::from_base_url(&cli.api_url)?;
roasts::run(&client, command).await
}
Commands::Bag { command } => {
let client = BrewlogClient::from_base_url(&cli.api_url)?;
bags::run(&client, command).await
}
Commands::Gear { command } => {
let client = BrewlogClient::from_base_url(&cli.api_url)?;
gear::run(&client, command).await
}
Commands::Token { command } => {
let client = BrewlogClient::from_base_url(&cli.api_url)?;
tokens::run(&client, command).await
}
}
}

View file

@ -1,11 +1,35 @@
use anyhow::Result;
use clap::Args;
use clap::{Args, Subcommand};
use super::macros::{define_delete_command, define_get_command};
use super::print_json;
use crate::domain::ids::{BagId, RoastId};
use crate::infrastructure::client::BrewlogClient;
#[derive(Debug, Subcommand)]
pub enum BagCommands {
/// Add a new bag
Add(AddBagCommand),
/// List all bags
List(ListBagsCommand),
/// Get a bag by ID
Get(GetBagCommand),
/// Update a bag
Update(UpdateBagCommand),
/// Delete a bag
Delete(DeleteBagCommand),
}
pub async fn run(client: &BrewlogClient, cmd: BagCommands) -> Result<()> {
match cmd {
BagCommands::Add(c) => add_bag(client, c).await,
BagCommands::List(c) => list_bags(client, c).await,
BagCommands::Get(c) => get_bag(client, c).await,
BagCommands::Update(c) => update_bag(client, c).await,
BagCommands::Delete(c) => delete_bag(client, c).await,
}
}
#[derive(Debug, Args)]
pub struct AddBagCommand {
#[arg(long)]

View file

@ -1,11 +1,35 @@
use anyhow::Result;
use clap::Args;
use clap::{Args, Subcommand};
use super::macros::{define_delete_command, define_get_command};
use super::print_json;
use crate::domain::ids::GearId;
use crate::infrastructure::client::BrewlogClient;
#[derive(Debug, Subcommand)]
pub enum GearCommands {
/// Add new gear
Add(AddGearCommand),
/// List all gear
List(ListGearCommand),
/// Get gear by ID
Get(GetGearCommand),
/// Update gear
Update(UpdateGearCommand),
/// Delete gear
Delete(DeleteGearCommand),
}
pub async fn run(client: &BrewlogClient, cmd: GearCommands) -> Result<()> {
match cmd {
GearCommands::Add(c) => add_gear(client, c).await,
GearCommands::List(c) => list_gear(client, c).await,
GearCommands::Get(c) => get_gear(client, c).await,
GearCommands::Update(c) => update_gear(client, c).await,
GearCommands::Delete(c) => delete_gear(client, c).await,
}
}
#[derive(Debug, Args)]
pub struct AddGearCommand {
#[arg(long)]

View file

@ -7,12 +7,12 @@ pub mod tokens;
use std::net::SocketAddr;
use bags::{AddBagCommand, DeleteBagCommand, GetBagCommand, ListBagsCommand, UpdateBagCommand};
use bags::BagCommands;
use clap::{Args, Parser, Subcommand};
use gear::{AddGearCommand, DeleteGearCommand, GetGearCommand, ListGearCommand, UpdateGearCommand};
use roasters::{AddRoasterCommand, DeleteRoasterCommand, GetRoasterCommand, UpdateRoasterCommand};
use roasts::{AddRoastCommand, DeleteRoastCommand, GetRoastCommand, ListRoastsCommand};
use tokens::{CreateTokenCommand, RevokeTokenCommand};
use gear::GearCommands;
use roasters::RoasterCommands;
use roasts::RoastCommands;
use tokens::TokenCommands;
#[derive(Debug, Parser)]
#[command(author, version, about = "Track coffee roasts, brews, and cups", long_about = None)]
@ -31,62 +31,38 @@ pub struct Cli {
#[derive(Debug, Subcommand)]
pub enum Commands {
#[command(name = "serve")]
/// Run the HTTP server
Serve(ServeCommand),
// Tokens
#[command(name = "create-token")]
CreateToken(CreateTokenCommand),
#[command(name = "list-tokens")]
ListTokens,
#[command(name = "revoke-token")]
RevokeToken(RevokeTokenCommand),
/// Manage roasters
Roaster {
#[command(subcommand)]
command: RoasterCommands,
},
// Roasters
#[command(name = "add-roaster")]
AddRoaster(AddRoasterCommand),
#[command(name = "list-roasters")]
ListRoasters,
#[command(name = "get-roaster")]
GetRoaster(GetRoasterCommand),
#[command(name = "update-roaster")]
UpdateRoaster(UpdateRoasterCommand),
#[command(name = "delete-roaster")]
DeleteRoaster(DeleteRoasterCommand),
/// Manage roasts
Roast {
#[command(subcommand)]
command: RoastCommands,
},
// Roasts
#[command(name = "add-roast")]
AddRoast(AddRoastCommand),
#[command(name = "list-roasts")]
ListRoasts(ListRoastsCommand),
#[command(name = "get-roast")]
GetRoast(GetRoastCommand),
#[command(name = "delete-roast")]
DeleteRoast(DeleteRoastCommand),
/// Manage bags
Bag {
#[command(subcommand)]
command: BagCommands,
},
// Bags
#[command(name = "add-bag")]
AddBag(AddBagCommand),
#[command(name = "list-bags")]
ListBags(ListBagsCommand),
#[command(name = "get-bag")]
GetBag(GetBagCommand),
#[command(name = "update-bag")]
UpdateBag(UpdateBagCommand),
#[command(name = "delete-bag")]
DeleteBag(DeleteBagCommand),
/// Manage gear
Gear {
#[command(subcommand)]
command: GearCommands,
},
// Gear
#[command(name = "add-gear")]
AddGear(AddGearCommand),
#[command(name = "list-gear")]
ListGear(ListGearCommand),
#[command(name = "get-gear")]
GetGear(GetGearCommand),
#[command(name = "update-gear")]
UpdateGear(UpdateGearCommand),
#[command(name = "delete-gear")]
DeleteGear(DeleteGearCommand),
/// Manage API tokens
Token {
#[command(subcommand)]
command: TokenCommands,
},
}
#[derive(Debug, Args)]

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use clap::Args;
use clap::{Args, Subcommand};
use super::macros::{define_delete_command, define_get_command};
use super::print_json;
@ -7,6 +7,30 @@ use crate::domain::ids::RoasterId;
use crate::domain::roasters::{NewRoaster, UpdateRoaster};
use crate::infrastructure::client::BrewlogClient;
#[derive(Debug, Subcommand)]
pub enum RoasterCommands {
/// Add a new roaster
Add(AddRoasterCommand),
/// List all roasters
List,
/// Get a roaster by ID
Get(GetRoasterCommand),
/// Update a roaster
Update(UpdateRoasterCommand),
/// Delete a roaster
Delete(DeleteRoasterCommand),
}
pub async fn run(client: &BrewlogClient, cmd: RoasterCommands) -> Result<()> {
match cmd {
RoasterCommands::Add(c) => add_roaster(client, c).await,
RoasterCommands::List => list_roasters(client).await,
RoasterCommands::Get(c) => get_roaster(client, c).await,
RoasterCommands::Update(c) => update_roaster(client, c).await,
RoasterCommands::Delete(c) => delete_roaster(client, c).await,
}
}
#[derive(Debug, Args)]
pub struct AddRoasterCommand {
#[arg(long)]

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use clap::Args;
use clap::{Args, Subcommand};
use super::macros::{define_delete_command, define_get_command};
use super::print_json;
@ -7,6 +7,27 @@ use crate::domain::ids::{RoastId, RoasterId};
use crate::domain::roasts::NewRoast;
use crate::infrastructure::client::BrewlogClient;
#[derive(Debug, Subcommand)]
pub enum RoastCommands {
/// Add a new roast
Add(AddRoastCommand),
/// List all roasts
List(ListRoastsCommand),
/// Get a roast by ID
Get(GetRoastCommand),
/// Delete a roast
Delete(DeleteRoastCommand),
}
pub async fn run(client: &BrewlogClient, cmd: RoastCommands) -> Result<()> {
match cmd {
RoastCommands::Add(c) => add_roast(client, c).await,
RoastCommands::List(c) => list_roasts(client, c).await,
RoastCommands::Get(c) => get_roast(client, c).await,
RoastCommands::Delete(c) => delete_roast(client, c).await,
}
}
#[derive(Debug, Args)]
pub struct AddRoastCommand {
#[arg(long)]

View file

@ -1,11 +1,29 @@
use anyhow::{Context, Result};
use clap::Args;
use clap::{Args, Subcommand};
use std::io::{self, Write};
use super::print_json;
use crate::domain::ids::TokenId;
use crate::infrastructure::client::BrewlogClient;
#[derive(Debug, Subcommand)]
pub enum TokenCommands {
/// Create a new API token
Create(CreateTokenCommand),
/// List all tokens
List,
/// Revoke a token
Revoke(RevokeTokenCommand),
}
pub async fn run(client: &BrewlogClient, cmd: TokenCommands) -> Result<()> {
match cmd {
TokenCommands::Create(c) => create_token(client, c).await,
TokenCommands::List => list_tokens(client).await,
TokenCommands::Revoke(c) => revoke_token(client, c).await,
}
}
#[derive(Debug, Args)]
pub struct CreateTokenCommand {
/// A descriptive name for this token

View file

@ -5,9 +5,12 @@ use serde_json::Value;
fn test_add_bag_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["add-bag", "--roast-id", "123", "--amount", "250.0"], &[]);
let output = run_brewlog(
&["bag", "add", "--roast-id", "123", "--amount", "250.0"],
&[],
);
assert!(!output.status.success(), "add-bag without auth should fail");
assert!(!output.status.success(), "bag add without auth should fail");
}
#[test]
@ -21,7 +24,8 @@ fn test_add_bag_with_authentication() {
// Test: Add Bag
let output = run_brewlog(
&[
"add-bag",
"bag",
"add",
"--roast-id",
&roast_id,
"--amount",
@ -44,11 +48,14 @@ fn test_add_bag_with_authentication() {
fn test_update_bag_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["update-bag", "--id", "123", "--remaining", "100.0"], &[]);
let output = run_brewlog(
&["bag", "update", "--id", "123", "--remaining", "100.0"],
&[],
);
assert!(
!output.status.success(),
"update-bag without auth should fail"
"bag update without auth should fail"
);
}
@ -61,7 +68,7 @@ fn test_update_bag_with_authentication() {
let roast_id = create_roast(&roaster_id, "Bag Update Roast", &token);
let bag_output = run_brewlog(
&["add-bag", "--roast-id", &roast_id, "--amount", "250.0"],
&["bag", "add", "--roast-id", &roast_id, "--amount", "250.0"],
&[("BREWLOG_TOKEN", &token)],
);
let bag: Value = serde_json::from_slice(&bag_output.stdout).unwrap();
@ -70,7 +77,8 @@ fn test_update_bag_with_authentication() {
// Test: Update Bag
let output = run_brewlog(
&[
"update-bag",
"bag",
"update",
"--id",
&bag_id,
"--remaining",
@ -92,7 +100,7 @@ fn test_update_bag_with_authentication() {
fn test_list_bags_works_without_authentication() {
let _ = server_info();
// Listing without roast_id might return all or empty, but should succeed (200 OK)
let output = run_brewlog(&["list-bags"], &[]);
let output = run_brewlog(&["bag", "list"], &[]);
assert!(output.status.success());
}
@ -105,7 +113,7 @@ fn test_list_bags_shows_added_bag() {
let roast_id = create_roast(&roaster_id, "Bag List Roast", &token);
let bag_output = run_brewlog(
&["add-bag", "--roast-id", &roast_id, "--amount", "250.0"],
&["bag", "add", "--roast-id", &roast_id, "--amount", "250.0"],
&[("BREWLOG_TOKEN", &token)],
);
let bag: Value = serde_json::from_slice(&bag_output.stdout).unwrap();
@ -113,7 +121,7 @@ fn test_list_bags_shows_added_bag() {
// Test: List Bags (Authenticated)
let output = run_brewlog(
&["list-bags", "--roast-id", &roast_id],
&["bag", "list", "--roast-id", &roast_id],
&[("BREWLOG_TOKEN", &token)],
);
@ -133,14 +141,14 @@ fn test_list_bags_without_roast_id_shows_all_bags() {
let roast_id = create_roast(&roaster_id, "Bag List All Roast", &token);
let bag_output = run_brewlog(
&["add-bag", "--roast-id", &roast_id, "--amount", "250.0"],
&["bag", "add", "--roast-id", &roast_id, "--amount", "250.0"],
&[("BREWLOG_TOKEN", &token)],
);
let bag: Value = serde_json::from_slice(&bag_output.stdout).unwrap();
let bag_id = bag["id"].as_i64().unwrap();
// Test: List Bags (Authenticated)
let output = run_brewlog(&["list-bags"], &[("BREWLOG_TOKEN", &token)]);
let output = run_brewlog(&["bag", "list"], &[("BREWLOG_TOKEN", &token)]);
assert!(output.status.success());
let bags: Value = serde_json::from_slice(&output.stdout).unwrap();
@ -159,7 +167,7 @@ fn test_list_bags_shows_open_and_closed_bags() {
// Create Open Bag
let open_bag_output = run_brewlog(
&["add-bag", "--roast-id", &roast_id, "--amount", "250.0"],
&["bag", "add", "--roast-id", &roast_id, "--amount", "250.0"],
&[("BREWLOG_TOKEN", &token)],
);
let open_bag: Value = serde_json::from_slice(&open_bag_output.stdout).unwrap();
@ -167,7 +175,7 @@ fn test_list_bags_shows_open_and_closed_bags() {
// Create Closed Bag
let closed_bag_output = run_brewlog(
&["add-bag", "--roast-id", &roast_id, "--amount", "250.0"],
&["bag", "add", "--roast-id", &roast_id, "--amount", "250.0"],
&[("BREWLOG_TOKEN", &token)],
);
let closed_bag: Value = serde_json::from_slice(&closed_bag_output.stdout).unwrap();
@ -176,7 +184,8 @@ fn test_list_bags_shows_open_and_closed_bags() {
// Close the second bag
let _ = run_brewlog(
&[
"update-bag",
"bag",
"update",
"--id",
&closed_bag_id.to_string(),
"--closed",
@ -186,7 +195,7 @@ fn test_list_bags_shows_open_and_closed_bags() {
);
// Test: List Bags (Authenticated)
let output = run_brewlog(&["list-bags"], &[("BREWLOG_TOKEN", &token)]);
let output = run_brewlog(&["bag", "list"], &[("BREWLOG_TOKEN", &token)]);
assert!(output.status.success());
let bags: Value = serde_json::from_slice(&output.stdout).unwrap();
@ -210,7 +219,7 @@ fn test_list_bags_shows_open_and_closed_bags() {
#[test]
fn test_delete_bag_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["delete-bag", "--id", "123"], &[]);
let output = run_brewlog(&["bag", "delete", "--id", "123"], &[]);
assert!(!output.status.success());
}
@ -223,7 +232,7 @@ fn test_delete_bag_with_authentication() {
let roast_id = create_roast(&roaster_id, "Bag Delete Roast", &token);
let bag_output = run_brewlog(
&["add-bag", "--roast-id", &roast_id, "--amount", "250.0"],
&["bag", "add", "--roast-id", &roast_id, "--amount", "250.0"],
&[("BREWLOG_TOKEN", &token)],
);
let bag: Value = serde_json::from_slice(&bag_output.stdout).unwrap();
@ -231,12 +240,12 @@ fn test_delete_bag_with_authentication() {
// Test: Delete Bag
let output = run_brewlog(
&["delete-bag", "--id", &bag_id],
&["bag", "delete", "--id", &bag_id],
&[("BREWLOG_TOKEN", &token)],
);
assert!(output.status.success());
// Verify deletion
let get_output = run_brewlog(&["get-bag", "--id", &bag_id], &[]);
let get_output = run_brewlog(&["bag", "get", "--id", &bag_id], &[]);
assert!(!get_output.status.success());
}

View file

@ -7,7 +7,8 @@ fn test_add_gear_requires_authentication() {
let output = run_brewlog(
&[
"add-gear",
"gear",
"add",
"--category",
"grinder",
"--make",
@ -20,7 +21,7 @@ fn test_add_gear_requires_authentication() {
assert!(
!output.status.success(),
"add-gear without auth should fail"
"gear add without auth should fail"
);
}
@ -30,7 +31,8 @@ fn test_add_gear_with_authentication() {
let output = run_brewlog(
&[
"add-gear",
"gear",
"add",
"--category",
"grinder",
"--make",
@ -54,11 +56,11 @@ fn test_add_gear_with_authentication() {
fn test_list_gear_works_without_authentication() {
let _ = server_info();
let output = run_brewlog(&["list-gear"], &[]);
let output = run_brewlog(&["gear", "list"], &[]);
assert!(
output.status.success(),
"list-gear should work without auth"
"gear list should work without auth"
);
}
@ -69,7 +71,8 @@ fn test_list_gear_shows_added_gear() {
// Add gear
let add_output = run_brewlog(
&[
"add-gear",
"gear",
"add",
"--category",
"grinder",
"--make",
@ -82,7 +85,7 @@ fn test_list_gear_shows_added_gear() {
assert!(add_output.status.success());
// List gear
let list_output = run_brewlog(&["list-gear"], &[]);
let list_output = run_brewlog(&["gear", "list"], &[]);
assert!(list_output.status.success());
let gear_list: Value = serde_json::from_slice(&list_output.stdout).unwrap();
@ -103,7 +106,8 @@ fn test_list_gear_filtered_by_category() {
// Add grinder
run_brewlog(
&[
"add-gear",
"gear",
"add",
"--category",
"grinder",
"--make",
@ -117,7 +121,8 @@ fn test_list_gear_filtered_by_category() {
// Add brewer
run_brewlog(
&[
"add-gear",
"gear",
"add",
"--category",
"brewer",
"--make",
@ -129,7 +134,7 @@ fn test_list_gear_filtered_by_category() {
);
// List only grinders
let output = run_brewlog(&["list-gear", "--category", "grinder"], &[]);
let output = run_brewlog(&["gear", "list", "--category", "grinder"], &[]);
assert!(output.status.success());
let gear_list: Value = serde_json::from_slice(&output.stdout).unwrap();
@ -151,7 +156,8 @@ fn test_get_gear_by_id() {
// Add gear
let add_output = run_brewlog(
&[
"add-gear",
"gear",
"add",
"--category",
"grinder",
"--make",
@ -165,7 +171,7 @@ fn test_get_gear_by_id() {
let gear_id = gear["id"].as_i64().unwrap().to_string();
// Get gear by ID
let output = run_brewlog(&["get-gear", "--id", &gear_id], &[]);
let output = run_brewlog(&["gear", "get", "--id", &gear_id], &[]);
assert!(output.status.success());
let retrieved_gear: Value = serde_json::from_slice(&output.stdout).unwrap();
@ -177,11 +183,11 @@ fn test_get_gear_by_id() {
fn test_update_gear_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["update-gear", "--id", "123", "--make", "Updated"], &[]);
let output = run_brewlog(&["gear", "update", "--id", "123", "--make", "Updated"], &[]);
assert!(
!output.status.success(),
"update-gear without auth should fail"
"gear update without auth should fail"
);
}
@ -192,7 +198,8 @@ fn test_update_gear_with_authentication() {
// Add gear
let add_output = run_brewlog(
&[
"add-gear",
"gear",
"add",
"--category",
"grinder",
"--make",
@ -207,7 +214,7 @@ fn test_update_gear_with_authentication() {
// Update gear
let output = run_brewlog(
&["update-gear", "--id", &gear_id, "--model", "Mini II"],
&["gear", "update", "--id", &gear_id, "--model", "Mini II"],
&[("BREWLOG_TOKEN", &token)],
);
@ -221,11 +228,11 @@ fn test_update_gear_with_authentication() {
fn test_delete_gear_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["delete-gear", "--id", "123"], &[]);
let output = run_brewlog(&["gear", "delete", "--id", "123"], &[]);
assert!(
!output.status.success(),
"delete-gear without auth should fail"
"gear delete without auth should fail"
);
}
@ -236,7 +243,8 @@ fn test_delete_gear_with_authentication() {
// Add gear
let add_output = run_brewlog(
&[
"add-gear",
"gear",
"add",
"--category",
"grinder",
"--make",
@ -251,12 +259,12 @@ fn test_delete_gear_with_authentication() {
// Delete gear
let delete_output = run_brewlog(
&["delete-gear", "--id", &gear_id],
&["gear", "delete", "--id", &gear_id],
&[("BREWLOG_TOKEN", &token)],
);
assert!(delete_output.status.success());
// Verify deletion - get should fail
let get_output = run_brewlog(&["get-gear", "--id", &gear_id], &[]);
let get_output = run_brewlog(&["gear", "get", "--id", &gear_id], &[]);
assert!(!get_output.status.success());
}

View file

@ -119,7 +119,8 @@ pub fn create_token(name: &str) -> String {
let output = run_brewlog(
&[
"create-token",
"token",
"create",
"--name",
name,
"--username",
@ -165,7 +166,7 @@ pub fn run_brewlog(args: &[&str], env: &[(&str, &str)]) -> std::process::Output
/// Helper to create a roaster and return its ID
pub fn create_roaster(name: &str, token: &str) -> String {
let output = run_brewlog(
&["add-roaster", "--name", name, "--country", "UK"],
&["roaster", "add", "--name", name, "--country", "UK"],
&[("BREWLOG_TOKEN", token)],
);
@ -189,7 +190,8 @@ pub fn create_roaster(name: &str, token: &str) -> String {
pub fn create_roast(roaster_id: &str, name: &str, token: &str) -> String {
let output = run_brewlog(
&[
"add-roast",
"roast",
"add",
"--roaster-id",
roaster_id,
"--name",

View file

@ -6,13 +6,20 @@ fn test_add_roaster_requires_authentication() {
let _ = server_info(); // Ensure server is started
let output = run_brewlog(
&["add-roaster", "--name", "Test Roasters", "--country", "UK"],
&[
"roaster",
"add",
"--name",
"Test Roasters",
"--country",
"UK",
],
&[],
);
assert!(
!output.status.success(),
"add-roaster without auth should fail"
"roaster add without auth should fail"
);
}
@ -21,13 +28,20 @@ fn test_add_roaster_with_authentication() {
let token = create_token("test-add-roaster");
let output = run_brewlog(
&["add-roaster", "--name", "Test Roasters", "--country", "UK"],
&[
"roaster",
"add",
"--name",
"Test Roasters",
"--country",
"UK",
],
&[("BREWLOG_TOKEN", &token)],
);
assert!(
output.status.success(),
"add-roaster with auth should succeed: {}",
"roaster add with auth should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
@ -44,11 +58,11 @@ fn test_add_roaster_with_authentication() {
fn test_list_roasters_works_without_authentication() {
let _ = server_info();
let output = run_brewlog(&["list-roasters"], &[]);
let output = run_brewlog(&["roaster", "list"], &[]);
assert!(
output.status.success(),
"list-roasters should work without auth"
"roaster list should work without auth"
);
let stdout = String::from_utf8_lossy(&output.stdout);
@ -65,7 +79,7 @@ fn test_list_roasters_shows_added_roaster() {
let roaster_id = create_roaster("Example Roasters", &token);
// List roasters
let list_output = run_brewlog(&["list-roasters"], &[]);
let list_output = run_brewlog(&["roaster", "list"], &[]);
assert!(list_output.status.success());
@ -87,11 +101,11 @@ fn test_list_roasters_shows_added_roaster() {
fn test_delete_roaster_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["delete-roaster", "--id", "some-id"], &[]);
let output = run_brewlog(&["roaster", "delete", "--id", "some-id"], &[]);
assert!(
!output.status.success(),
"delete-roaster without auth should fail"
"roaster delete without auth should fail"
);
}
@ -101,7 +115,8 @@ fn test_update_roaster_requires_authentication() {
let output = run_brewlog(
&[
"update-roaster",
"roaster",
"update",
"--id",
"some-id",
"--name",
@ -112,6 +127,6 @@ fn test_update_roaster_requires_authentication() {
assert!(
!output.status.success(),
"update-roaster without auth should fail"
"roaster update without auth should fail"
);
}

View file

@ -7,7 +7,8 @@ fn test_add_roast_requires_authentication() {
let output = run_brewlog(
&[
"add-roast",
"roast",
"add",
"--roaster-id",
"some-id",
"--name",
@ -26,7 +27,7 @@ fn test_add_roast_requires_authentication() {
assert!(
!output.status.success(),
"add-roast without auth should fail"
"roast add without auth should fail"
);
}
@ -40,7 +41,8 @@ fn test_add_roast_with_authentication() {
// Now add a roast
let output = run_brewlog(
&[
"add-roast",
"roast",
"add",
"--roaster-id",
&roaster_id,
"--name",
@ -61,7 +63,7 @@ fn test_add_roast_with_authentication() {
assert!(
output.status.success(),
"add-roast with auth should succeed: {}",
"roast add with auth should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
@ -80,11 +82,11 @@ fn test_add_roast_with_authentication() {
fn test_list_roasts_works_without_authentication() {
let _ = server_info();
let output = run_brewlog(&["list-roasts"], &[]);
let output = run_brewlog(&["roast", "list"], &[]);
assert!(
output.status.success(),
"list-roasts should work without auth"
"roast list should work without auth"
);
let stdout = String::from_utf8_lossy(&output.stdout);
@ -103,7 +105,8 @@ fn test_list_roasts_shows_added_roast() {
// Add a roast
let add_output = run_brewlog(
&[
"add-roast",
"roast",
"add",
"--roaster-id",
&roaster_id,
"--name",
@ -131,7 +134,7 @@ fn test_list_roasts_shows_added_roast() {
.expect("roast id should be numeric");
// List roasts
let list_output = run_brewlog(&["list-roasts"], &[]);
let list_output = run_brewlog(&["roast", "list"], &[]);
assert!(list_output.status.success());
@ -157,10 +160,10 @@ fn test_list_roasts_shows_added_roast() {
fn test_delete_roast_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["delete-roast", "--id", "some-id"], &[]);
let output = run_brewlog(&["roast", "delete", "--id", "some-id"], &[]);
assert!(
!output.status.success(),
"delete-roast without auth should fail"
"roast delete without auth should fail"
);
}

View file

@ -4,11 +4,11 @@ use crate::helpers::{create_token, run_brewlog, server_info};
fn test_list_tokens_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["list-tokens"], &[]);
let output = run_brewlog(&["token", "list"], &[]);
assert!(
!output.status.success(),
"list-tokens without auth should fail"
"token list without auth should fail"
);
}
@ -16,11 +16,11 @@ fn test_list_tokens_requires_authentication() {
fn test_list_tokens_with_authentication() {
let token = create_token("test-list-tokens");
let output = run_brewlog(&["list-tokens"], &[("BREWLOG_TOKEN", &token)]);
let output = run_brewlog(&["token", "list"], &[("BREWLOG_TOKEN", &token)]);
assert!(
output.status.success(),
"list-tokens with auth should succeed"
"token list with auth should succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
@ -34,11 +34,11 @@ fn test_list_tokens_with_authentication() {
fn test_revoke_token_requires_authentication() {
let _ = server_info();
let output = run_brewlog(&["revoke-token", "--id", "1"], &[]);
let output = run_brewlog(&["token", "revoke", "--id", "1"], &[]);
assert!(
!output.status.success(),
"revoke-token without auth should fail"
"token revoke without auth should fail"
);
}
@ -47,7 +47,7 @@ fn test_revoke_token_with_authentication() {
let token = create_token("test-revoke-token");
// List tokens to get the ID
let list_output = run_brewlog(&["list-tokens"], &[("BREWLOG_TOKEN", &token)]);
let list_output = run_brewlog(&["token", "list"], &[("BREWLOG_TOKEN", &token)]);
assert!(list_output.status.success());
let list_stdout = String::from_utf8_lossy(&list_output.stdout);
@ -66,7 +66,7 @@ fn test_revoke_token_with_authentication() {
.expect("Token should have ID");
let revoke_output = run_brewlog(
&["revoke-token", "--id", &token_id.to_string()],
&["token", "revoke", "--id", &token_id.to_string()],
&[("BREWLOG_TOKEN", &token)],
);
@ -85,7 +85,7 @@ fn test_revoked_token_cannot_be_used() {
let admin_token = create_token("test-admin-token");
// List tokens to get the ID of the token we want to revoke
let list_output = run_brewlog(&["list-tokens"], &[("BREWLOG_TOKEN", &admin_token)]);
let list_output = run_brewlog(&["token", "list"], &[("BREWLOG_TOKEN", &admin_token)]);
assert!(list_output.status.success());
let list_stdout = String::from_utf8_lossy(&list_output.stdout);
@ -105,7 +105,7 @@ fn test_revoked_token_cannot_be_used() {
// Revoke the token
let revoke_output = run_brewlog(
&["revoke-token", "--id", &token_id.to_string()],
&["token", "revoke", "--id", &token_id.to_string()],
&[("BREWLOG_TOKEN", &admin_token)],
);
assert!(
@ -115,7 +115,7 @@ fn test_revoked_token_cannot_be_used() {
// Try to use the revoked token - it should fail
let list_with_revoked_output =
run_brewlog(&["list-tokens"], &[("BREWLOG_TOKEN", &token_to_revoke)]);
run_brewlog(&["token", "list"], &[("BREWLOG_TOKEN", &token_to_revoke)]);
assert!(
!list_with_revoked_output.status.success(),