shop

package module
v0.0.0-...-92dc408 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 28, 2026 License: MIT Imports: 7 Imported by: 0

README

shop

shop

Shopping from your terminal. Any store. One interface. Just shop.

Go License Platform

A multi-store shopping CLI with a unified interface. Search products, read reviews, manage your cart, and place real orders — all from your terminal with structured JSON.


Search · Product Details · Reviews · Variants · Offers · Cart · Checkout · Order


What is this?

shop is a single-binary CLI that gives you programmatic access to online stores through their internal APIs — the same APIs their own apps use. No scraping, no browser automation, no third-party API keys. Just direct HTTP calls, authenticated natively.

# Search from your terminal
$ shop search "sony wf-1000xm5" | jq '.products[0] | {title, price, rating}'
{
  "title": "Sony WF-1000XM5 Wireless Earbuds",
  "price": { "amount": 22800, "currency": "USD" },
  "rating": { "average": 4.5, "count": 12847 }
}

Every command outputs structured JSON. Same interface regardless of store. Pipe to jq, feed to scripts, build automations.


Features

🔍 Product Search 📦 Full Product Details 🌳 Variant Trees
Filters, sorting, pagination, price ranges, ratings Specs, features, images, descriptions, seller info Every color, size, and configuration mapped to ASINs
Customer Reviews 🏷️ Multi-Seller Offers 🛒 Cart Management
Sort by recent/helpful/rating, filter by stars All available sellers, conditions, and prices Add, remove, view, clear — full cart lifecycle
💳 Real Checkout 📍 Address & Payment 🌍 6 Amazon Regions
Preview orders, auto-select fastest shipping List saved addresses and payment methods US, UK, DE, JP, CA, AU — same binary

Quick Start

Install
go install github.com/saucesteals/shop/cmd/shop@latest
Authenticate
# Step 1: Start the auth flow
$ shop login amazon
{
  "status": "challenge",
  "challenge": {
    "type": "device_code",
    "code": "A4K7X2",
    "url": "https://www.amazon.com/code",
    "expiresIn": 600
  }
}

# Step 2: Complete the challenge, then run again
$ shop login amazon
{
  "status": "authenticated",
  "account": { "id": "A2F0K..." }
}
Use as an AI agent skill

shop skill prints the embedded SKILL.md to stdout — pipe it anywhere to enable autonomous shopping for your AI agent:

# OpenClaw
mkdir -p ~/.openclaw/workspace/skills/shop
shop skill > ~/.openclaw/workspace/skills/shop/SKILL.md

# Claude Code
shop skill > .claude/shop.md

The skill teaches the agent every command, flag, and workflow — search, cart, checkout, order — so it can shop on your behalf.

Set your default store
shop config set defaults.store amazon
Search → Cart → Buy
# Find what you want
shop search "usb-c cable" --sort price_low --min-rating 4.0

# Get the details
shop product B0D1XD1ZV3

# Check the variants
shop variants B0D1XD1ZV3

# Read the reviews
shop reviews B0D1XD1ZV3 --sort helpful

# Add to cart
shop cart add B0D1XD1ZV3

# Preview the order
shop checkout

# Place it (⚠️ real money, real order)
shop order place <checkout-id>

That's the full flow. Search to doorstep, never leaving the terminal.


How It Works

Provider Architecture

Each store is implemented as a provider that reverse-engineers the store's public APIs. This means structured JSON responses, stable endpoints, and full feature access without scraping.

Amazon Provider

The Amazon provider speaks TVSS (TV Shopping Service) — the internal API behind Amazon's Fire TV Shopping app at tvss.amazon.com.

┌──────────┐     device-code auth     ┌──────────────────┐
│          │ ◄──────────────────────► │  Amazon Auth API │
│   shop   │                          └──────────────────┘
│   CLI    │     TVSS JSON API        ┌──────────────────┐
│          │ ◄──────────────────────► │  tvss.amazon.com │
└──────────┘                          └──────────────────┘

Authentication uses Amazon's device-code pairing flow — the same mechanism Fire TV uses. You get a code, enter it at amazon.com/code, and the CLI registers as a device on your account. No passwords ever touch the CLI.

Product data comes from TVSS endpoints that return richly typed JSON — prices in minor units, structured variant dimensions, review distributions, full merchant info, and availability details.

Search uses Amazon's mobile search for ASIN discovery and ranking, then enriches results through TVSS for structured data.

The approach
  • No API keys — stores don't offer public product APIs. We don't need them.
  • No scraping — No HTML parsing, no CSS selectors, no breaking on redesigns.
  • No browser — No Puppeteer, no Playwright, no headless Chrome. Just HTTP.
  • Native auth — authenticated as a real device on your account, per-provider.

Command Reference

Auth
shop login <store>       # Start or complete auth flow
shop logout <store>      # Revoke credentials and clear tokens
shop whoami              # Check current auth state

Auth is provider-specific — each store uses its native auth mechanism (device code, OAuth, etc.). Run login twice: first call returns a challenge, second call (after completing it) finishes auth. State persists in ~/.config/shop/auth/ with 0600 permissions.

shop search "protein powder"
shop search "headphones" --sort price_low --min-price 2000 --max-price 15000
shop search "laptop" --min-rating 4.0 --category Electronics
shop search "keyboard" --filter brand=Keychron --page 2

Prices are in cents (minor units). --min-price 2000 = $20.00.

Search flags
  • --sortrelevance, price_low, price_high, rating, newest, best_seller
  • --page — Page number (default 1)
  • --page-size — Results per page
  • --min-price — Minimum price in minor units (cents)
  • --max-price — Maximum price in minor units (cents)
  • --min-rating — Minimum average rating (e.g. 4.0)
  • --category — Category filter (provider-specific)
  • --filter — Arbitrary key=value filter (repeatable)
Example output
{
  "products": [
    {
      "id": "B0D1XD1ZV3",
      "title": "Sony WF-1000XM5 Wireless Earbuds",
      "url": "https://www.amazon.com/dp/B0D1XD1ZV3",
      "price": { "amount": 22800, "currency": "USD" },
      "rating": { "average": 4.5, "count": 12847 },
      "availability": { "status": "in_stock" },
      "badge": "best_seller"
    }
  ],
  "total": 16,
  "page": 1,
  "hasMore": true
}
Product Details
shop product B0D1XD1ZV3

Returns the full product record: title, brand, price, list price, images, specs, features, description, seller info, availability, and variant metadata. Product IDs are Amazon ASINs (10-character alphanumeric).

Variants
shop variants B0D1XD1ZV3

Returns variant dimensions (Color, Size, etc.) with all options and a combinations array mapping dimension values to ASINs and prices. Every permutation the product page shows, structured as data.

Reviews
shop reviews B0D1XD1ZV3
shop reviews B0D1XD1ZV3 --sort recent --rating 5 --page 2
Review flags
  • --sortrecent, helpful, rating
  • --rating — Filter to specific star rating (1–5)
  • --page / --page-size — Pagination
Offers
shop offers B0D1XD1ZV3
shop offers B0D1XD1ZV3 --condition used_good

Returns seller offers for a product, including condition and pricing.

Offer flags
  • --conditionnew, used_like_new, used_good, used_fair, refurbished
  • --page / --page-size — Pagination
Cart
shop cart add B0D1XD1ZV3              # Add 1 unit
shop cart add B0D1XD1ZV3 --qty 3     # Add 3 units
shop cart view                        # View cart with subtotal
shop cart remove B0D1XD1ZV3           # Remove item
shop cart clear                       # Empty the cart

All cart commands return the full cart snapshot with items and subtotal.

Checkout & Order
# Preview — see totals, shipping, payment (does NOT charge)
shop checkout

# Place the order (⚠️ irreversible — real money)
shop order place <checkout-id>

The checkout-id comes from the checkout preview response. If the cart changes between preview and placement, the order is rejected — no surprise charges.

Checkout flags
  • --address — Shipping address ID
  • --payment — Payment method ID
  • --shipping — Shipping option ID
  • --coupon — Coupon/promo code
Account
shop addresses                        # List saved shipping addresses
shop payments                         # List saved payment methods
Stores
shop stores                           # List all known stores
shop stores --provider amazon         # Filter by provider
shop store info                       # Current store details + capabilities
shop capabilities                     # What the store supports

Global Flags

  • -s, --store — Target store (name or domain). Default: config value or SHOP_STORE env.
  • --json — Force compact JSON output
  • --pretty — Force pretty-printed JSON output
  • --config — Config directory path (default ~/.config/shop/)
  • --timeout — Request timeout (default 30s)

Output Format

All commands output JSON to stdout. Pretty-printed when interactive, compact when piped — override with --json or --pretty.

# Pretty in terminal
shop search "coffee"

# Compact for piping (auto-detected)
shop search "coffee" | jq '.products[] | {title, price}'

# Force compact
shop search "coffee" --json
Money

All prices use minor units (cents). Never floating point.

{ "amount": 2999, "currency": "USD" }

2999 = $29.99. For JPY, minor units = whole yen.

Errors

Structured JSON on stderr with typed error codes:

{
  "code": "auth_required",
  "message": "not authenticated; run 'shop login --store amazon' first"
}
Error code reference
  • auth_required (exit 10) — Not authenticated
  • auth_expired (exit 11) — Session expired
  • auth_failed (exit 12) — Authentication rejected
  • auth_timeout (exit 13) — Auth flow timed out
  • store_not_found (exit 20) — Unknown store
  • not_supported (exit 21) — Operation not supported
  • not_found (exit 30) — Product/resource not found
  • out_of_stock (exit 31) — Product unavailable
  • cart_empty (exit 40) — Cart has no items
  • cart_changed (exit 41) — Cart changed since checkout preview
  • quantity_limit (exit 42) — Exceeds store limit
  • rate_limited (exit 50) — Too many requests
  • store_error (exit 51) — Store-side error
  • network (exit 60) — Network failure
  • invalid_input (exit 2) — Bad arguments
  • config_error (exit 3) — Config read/write failure
  • internal (exit 1) — Unexpected error

Configuration

~/.config/shop/
├── config.json          # User settings
├── registry.json        # Store → provider mapping
└── auth/
    └── amazon.json      # Auth state (0600 permissions)
shop config set defaults.store amazon
shop config set defaults.timeout 60s
shop config get defaults.store
shop config list
Store Resolution

When you pass --store amazon, the CLI resolves it through:

  1. Registry — exact alias or domain match in registry.json
  2. Auto-discovery — providers probe the domain (sorted by cost, cheapest first)
  3. Cache — discovered stores are saved for future lookups
  4. Failstore_not_found error

Also accepts raw domains (amazon.co.uk, amazon.de), SHOP_STORE env var, or the config default.


Architecture

Provider Abstraction

shop is built on a provider interface. Each store is a self-contained provider that implements the same interface:

                    ┌─────────────────────────────────────────┐
                    │              shop.Store                 │
                    │                                         │
                    │  Login · Search · Product · Reviews     │
                    │  Offers · Variants · Cart · Checkout    │
                    │  PlaceOrder · Addresses · Payments      │
                    └──────────────────┬──────────────────────┘
                                       │
                    ┌──────────────────┴──────────────────────┐
                    │          amazon.Store                   │
                    │                                         │
                    │  TVSS API ←→ tvss.amazon.com            │
                    │  Device auth ←→ api.amazon.com          │
                    │                                         │
                    │  US · UK · DE · JP · CA · AU            │
                    └─────────────────────────────────────────┘

Providers self-register via init() and a shop.Register() call. The resolution layer discovers them automatically — zero wiring:

// internal/provider/amazon/amazon.go
func init() {
    shop.Register(&Provider{})
}

// cmd/shop/main.go
import _ "github.com/saucesteals/shop/internal/provider/amazon"
Adding a New Provider
  1. Create internal/provider/<name>/ implementing shop.Provider, shop.Store, and shop.Cart
  2. Call shop.Register(&Provider{}) in init()
  3. Add a blank import in cmd/shop/main.go

That's it. No config files, no factory registration, no dependency injection. The provider exists and the CLI finds it.


Supported Stores

Amazon

6 regions, full feature support:

Store Domain Currency
🇺🇸 Amazon US amazon.com USD
🇬🇧 Amazon UK amazon.co.uk GBP
🇩🇪 Amazon DE amazon.de EUR
🇯🇵 Amazon JP amazon.co.jp JPY
🇨🇦 Amazon CA amazon.ca CAD
🇦🇺 Amazon AU amazon.com.au AUD
shop search "coffee" --store amazon.co.jp
shop search "kaffee" --store amazon.de

More providers coming. The shop.Provider interface is all you need to add one.


License

MIT

Built with ⚡ Jarvis

Documentation

Overview

Package shop defines the core interfaces and types for a multi-platform shopping CLI. Providers implement these interfaces; the CLI consumes them.

Index

Constants

This section is empty.

Variables

ExitCodes maps error codes to CLI exit codes.

View Source
var SkillMD string

SkillMD is the embedded SKILL.md content for use with AI agents.

Functions

func ExitCode

func ExitCode(err error) int

ExitCode returns the CLI exit code for the given error. Returns 1 for unknown errors.

func IsAuthExpired

func IsAuthExpired(err error) bool

IsAuthExpired reports whether err is an auth_expired error.

func IsAuthRequired

func IsAuthRequired(err error) bool

IsAuthRequired reports whether err is an auth_required error.

func IsCartChanged

func IsCartChanged(err error) bool

IsCartChanged reports whether err is a cart_changed error.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err is a not_found error.

func IsNotSupported

func IsNotSupported(err error) bool

IsNotSupported reports whether err is a not_supported error.

func IsRateLimited

func IsRateLimited(err error) bool

IsRateLimited reports whether err is a rate_limited error.

func IsStoreNotFound

func IsStoreNotFound(err error) bool

IsStoreNotFound reports whether err is a store_not_found error.

func Register

func Register(p Provider)

Register makes a provider available for store resolution. Called in init() by each provider package.

func SetResolver

func SetResolver(fn func(ctx context.Context, storeValue string) (Store, error))

SetResolver registers the global store resolution function. Called by the app layer during initialization.

Types

type AccountInfo

type AccountInfo struct {
	Authenticated bool   `json:"authenticated"`
	AccountID     string `json:"accountId,omitempty"`
	AccountName   string `json:"accountName,omitempty"`
	Email         string `json:"email,omitempty"`
	ExpiresAt     string `json:"expiresAt,omitempty"`
}

AccountInfo is returned by Store.WhoAmI() and LoginResult.

type Address

type Address struct {
	ID         string `json:"id"`
	Label      string `json:"label,omitempty"`
	Name       string `json:"name"`
	Line1      string `json:"line1"`
	Line2      string `json:"line2,omitempty"`
	City       string `json:"city"`
	State      string `json:"state,omitempty"`
	PostalCode string `json:"postalCode"`
	Country    string `json:"country"`
	Phone      string `json:"phone,omitempty"`
	IsDefault  bool   `json:"isDefault"`
}

Address is a shipping/billing address.

type Availability

type Availability struct {
	Status  AvailabilityStatus `json:"status"`
	Message string             `json:"message,omitempty"`
}

Availability represents whether a product can be purchased.

type AvailabilityStatus

type AvailabilityStatus string

AvailabilityStatus is the purchase-readiness of a product.

const (
	AvailabilityInStock     AvailabilityStatus = "in_stock"
	AvailabilityLowStock    AvailabilityStatus = "low_stock"
	AvailabilityOutOfStock  AvailabilityStatus = "out_of_stock"
	AvailabilityPreorder    AvailabilityStatus = "preorder"
	AvailabilityUnavailable AvailabilityStatus = "unavailable"
)

type Capabilities

type Capabilities struct {
	Search          bool `json:"search"`
	Reviews         bool `json:"reviews"`
	Offers          bool `json:"offers"`
	Variants        bool `json:"variants"`
	Cart            bool `json:"cart"`
	Checkout        bool `json:"checkout"`
	Addresses       bool `json:"addresses"`
	PaymentMethods  bool `json:"paymentMethods"`
	ShippingOptions bool `json:"shippingOptions"`
	Coupons         bool `json:"coupons"`
}

Capabilities declares which optional features a store implementation supports.

type Cart

type Cart interface {
	// Add adds a product to the cart. quantity must be >= 1.
	Add(ctx context.Context, id string, quantity int) (*CartContents, error)

	// Remove removes a product entirely from the cart.
	Remove(ctx context.Context, id string) (*CartContents, error)

	// View returns the current cart snapshot.
	View(ctx context.Context) (*CartContents, error)

	// Clear empties the cart.
	Clear(ctx context.Context) (*CartContents, error)
}

Cart manages line items for a single store. Cart state is internal to the provider implementation — the CLI only reads it via View().

type CartContents

type CartContents struct {
	Items    []CartEntry `json:"items"`
	Subtotal Money       `json:"subtotal"`
}

CartContents is a snapshot of the current cart state.

type CartEntry

type CartEntry struct {
	Product  Product `json:"product"`
	Quantity int     `json:"quantity"`
}

CartEntry is a single line item in the cart.

type Challenge

type Challenge struct {
	URL       string `json:"url"`
	Code      string `json:"code,omitempty"`
	ExpiresAt string `json:"expiresAt,omitempty"`
	Message   string `json:"message,omitempty"`
}

Challenge describes an external action the consumer must complete to finish authentication.

type CheckoutOpts

type CheckoutOpts struct {
	AddressID       string `json:"addressId,omitempty"`
	PaymentMethodID string `json:"paymentMethodId,omitempty"`
	ShippingOption  string `json:"shippingOption,omitempty"`
	CouponCode      string `json:"couponCode,omitempty"`
}

CheckoutOpts controls checkout preview behavior.

type CheckoutResult

type CheckoutResult struct {
	CheckoutID        string           `json:"checkoutId"`
	Items             []CartEntry      `json:"items"`
	Subtotal          Money            `json:"subtotal"`
	Shipping          Money            `json:"shipping"`
	Tax               Money            `json:"tax"`
	Discount          Money            `json:"discount"`
	Total             Money            `json:"total"`
	ShippingAddress   *Address         `json:"shippingAddress,omitempty"`
	PaymentMethod     *PaymentMethod   `json:"paymentMethod,omitempty"`
	ShippingOptions   []ShippingOption `json:"shippingOptions,omitempty"`
	SelectedShipping  *ShippingOption  `json:"selectedShipping,omitempty"`
	EstimatedDelivery string           `json:"estimatedDelivery,omitempty"`
	Warnings          []string         `json:"warnings,omitempty"`
	Attributes        map[string]any   `json:"attributes,omitempty"`
}

CheckoutResult is the order preview returned by Store.Checkout().

type DetectCost

type DetectCost int

DetectCost indicates how expensive a provider's Detect call is.

const (
	// DetectCostFree is a pure string match, no network.
	DetectCostFree DetectCost = 0
	// DetectCostCheap is a single HTTP request.
	DetectCostCheap DetectCost = 1
	// DetectCostModerate is multiple requests or DNS probes.
	DetectCostModerate DetectCost = 2
)

type Error

type Error struct {
	Code    ErrorCode      `json:"code"`
	Message string         `json:"message"`
	Details map[string]any `json:"details,omitempty"`
}

Error is the structured error type. Every error from every provider gets normalized into this shape before the CLI outputs it.

func Errorf

func Errorf(code ErrorCode, format string, args ...any) *Error

Errorf creates a new Error with the given code and formatted message.

func NotImplemented

func NotImplemented(provider, op string) *Error

NotImplemented returns a standard not-supported error for the given provider and operation. Shared across all provider stubs.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

func (*Error) WithDetails

func (e *Error) WithDetails(details map[string]any) *Error

WithDetails returns a copy of the error with the given details merged into any existing details. New keys overwrite existing ones.

type ErrorCode

type ErrorCode string

ErrorCode identifies the category of an error.

const (
	// Auth errors.
	ErrAuthRequired ErrorCode = "auth_required"
	ErrAuthExpired  ErrorCode = "auth_expired"
	ErrAuthFailed   ErrorCode = "auth_failed"
	ErrAuthTimeout  ErrorCode = "auth_timeout"

	// Product/search errors.
	ErrNotFound   ErrorCode = "not_found"
	ErrOutOfStock ErrorCode = "out_of_stock"

	// Cart errors.
	ErrCartEmpty     ErrorCode = "cart_empty"
	ErrCartChanged   ErrorCode = "cart_changed"
	ErrQuantityLimit ErrorCode = "quantity_limit"

	// Store errors.
	ErrStoreNotFound ErrorCode = "store_not_found"
	ErrNotSupported  ErrorCode = "not_supported"
	ErrRateLimited   ErrorCode = "rate_limited"
	ErrStoreError    ErrorCode = "store_error"

	// Input errors.
	ErrInvalidInput ErrorCode = "invalid_input"

	// System errors.
	ErrInternal    ErrorCode = "internal"
	ErrNetwork     ErrorCode = "network"
	ErrConfigError ErrorCode = "config_error"
)

type Image

type Image struct {
	URL    string `json:"url"`
	Alt    string `json:"alt,omitempty"`
	Width  int    `json:"width,omitempty"`
	Height int    `json:"height,omitempty"`
}

Image is a product image with optional dimensions.

type LoginResult

type LoginResult struct {
	Authenticated bool         `json:"authenticated"`
	Account       *AccountInfo `json:"account,omitempty"`
	Challenge     *Challenge   `json:"challenge,omitempty"`
}

LoginResult is returned by Store.Login().

type Money

type Money struct {
	Amount   int64  `json:"amount"`
	Currency string `json:"currency"`
}

Money represents a monetary value in minor units (cents for USD, pence for GBP, etc.). Never a float.

type Offer

type Offer struct {
	ID           string         `json:"id"`
	Seller       Seller         `json:"seller"`
	Condition    OfferCondition `json:"condition"`
	Price        Money          `json:"price"`
	Shipping     *ShippingInfo  `json:"shipping,omitempty"`
	Availability Availability   `json:"availability"`
	IsBuyBox     bool           `json:"isBuyBox,omitempty"`
	IsPrime      bool           `json:"isPrime,omitempty"`
	DeliveryDate string         `json:"deliveryDate,omitempty"`
	Attributes   map[string]any `json:"attributes,omitempty"`
}

Offer represents a single seller's listing for a product.

type OfferCondition

type OfferCondition string

OfferCondition filters offers by item condition.

const (
	ConditionAny         OfferCondition = ""
	ConditionNew         OfferCondition = "new"
	ConditionUsedLikeNew OfferCondition = "used_like_new"
	ConditionUsedGood    OfferCondition = "used_good"
	ConditionUsedFair    OfferCondition = "used_fair"
	ConditionRefurbished OfferCondition = "refurbished"
)

type OffersQuery

type OffersQuery struct {
	Condition OfferCondition `json:"condition,omitempty"`
	Page      int            `json:"page,omitempty"`
	PageSize  int            `json:"pageSize,omitempty"`
}

OffersQuery controls offer listing.

type OffersResult

type OffersResult struct {
	Offers  []Offer `json:"offers"`
	Page    int     `json:"page"`
	HasMore bool    `json:"hasMore"`
}

OffersResult is a paginated list of offers for a product.

type Order

type Order struct {
	OrderID           string         `json:"orderId"`
	Status            string         `json:"status"`
	Items             []CartEntry    `json:"items"`
	Total             Money          `json:"total"`
	ShippingAddress   *Address       `json:"shippingAddress,omitempty"`
	PaymentMethod     *PaymentMethod `json:"paymentMethod,omitempty"`
	EstimatedDelivery string         `json:"estimatedDelivery,omitempty"`
	PlacedAt          string         `json:"placedAt"`
	Attributes        map[string]any `json:"attributes,omitempty"`
}

Order is the result of a successfully placed order.

type PaymentMethod

type PaymentMethod struct {
	ID        string `json:"id"`
	Type      string `json:"type"`
	Label     string `json:"label"`
	Last4     string `json:"last4,omitempty"`
	ExpMonth  int    `json:"expMonth,omitempty"`
	ExpYear   int    `json:"expYear,omitempty"`
	IsDefault bool   `json:"isDefault"`
}

PaymentMethod is a saved payment instrument. Sensitive details are masked.

type Product

type Product struct {
	ID           string         `json:"id"`
	Title        string         `json:"title"`
	Brand        string         `json:"brand,omitempty"`
	Description  string         `json:"description,omitempty"`
	URL          string         `json:"url"`
	Images       []Image        `json:"images"`
	Price        *Money         `json:"price,omitempty"`
	ListPrice    *Money         `json:"listPrice,omitempty"`
	Rating       *Rating        `json:"rating,omitempty"`
	Availability Availability   `json:"availability"`
	Seller       *Seller        `json:"seller,omitempty"`
	Categories   []string       `json:"categories,omitempty"`
	Features     []string       `json:"features,omitempty"`
	Specs        []Spec         `json:"specs,omitempty"`
	VariantInfo  *VariantInfo   `json:"variantInfo,omitempty"`
	Attributes   map[string]any `json:"attributes,omitempty"`
}

Product is the full representation of a product.

type ProductSummary

type ProductSummary struct {
	ID           string         `json:"id"`
	Title        string         `json:"title"`
	Brand        string         `json:"brand,omitempty"`
	URL          string         `json:"url"`
	ImageURL     string         `json:"imageUrl,omitempty"`
	Price        *Money         `json:"price,omitempty"`
	ListPrice    *Money         `json:"listPrice,omitempty"`
	Rating       *Rating        `json:"rating,omitempty"`
	Availability Availability   `json:"availability"`
	Sponsored    bool           `json:"sponsored,omitempty"`
	Badge        string         `json:"badge,omitempty"`
	Attributes   map[string]any `json:"attributes,omitempty"`
}

ProductSummary is the condensed representation returned in search results.

type Provider

type Provider interface {
	// Name returns the provider's unique identifier (e.g., "amazon").
	Name() string

	// Detect probes a handle/domain and returns true if this provider can
	// handle it. Called during auto-discovery for unknown domains.
	// Implementations should be fast and not require auth. Returns a
	// StoreInfo if detected, which gets cached in the registry.
	Detect(ctx context.Context, handle string) (*StoreInfo, error)

	// DetectCost indicates how expensive Detect is, allowing the resolution
	// layer to sort providers optimally (cheapest first).
	DetectCost() DetectCost

	// Store creates a Store instance for the given handle. The handle is
	// the canonical domain for the store. configDir is the path to the
	// CLI's config directory (e.g., ~/.config/shop) for auth file I/O.
	Store(ctx context.Context, handle string, configDir string) (Store, error)
}

Provider is a factory that creates Store instances for compatible domains. Each provider handles a family of stores (e.g., "amazon" handles amazon.com/co.uk/etc.).

func Providers

func Providers() []Provider

Providers returns all registered providers, sorted by DetectCost (cheapest first). The returned slice must not be modified.

type Rating

type Rating struct {
	Average float64        `json:"average"`
	Count   int            `json:"count"`
	Stars   *StarBreakdown `json:"stars,omitempty"`
}

Rating is the aggregate customer rating for a product.

type RegistryEntry

type RegistryEntry struct {
	Alias          string         `json:"alias"`
	Domain         string         `json:"domain"`
	Provider       string         `json:"provider"`
	Name           string         `json:"name"`
	Country        string         `json:"country,omitempty"`
	Currency       string         `json:"currency,omitempty"`
	BuiltIn        bool           `json:"builtIn"`
	DetectedAt     string         `json:"detectedAt,omitempty"`
	DetectedBy     string         `json:"detectedBy,omitempty"`
	ProviderConfig map[string]any `json:"providerConfig,omitempty"`
}

RegistryEntry is a single known store in the persistent registry.

type Review

type Review struct {
	ID         string         `json:"id"`
	Author     string         `json:"author"`
	Title      string         `json:"title,omitempty"`
	Body       string         `json:"body"`
	Rating     int            `json:"rating"`
	Date       string         `json:"date"`
	Verified   bool           `json:"verified"`
	Helpful    int            `json:"helpful,omitempty"`
	Images     []Image        `json:"images,omitempty"`
	Attributes map[string]any `json:"attributes,omitempty"`
}

Review is a single customer review.

type ReviewSort

type ReviewSort string

ReviewSort controls the ordering of reviews.

const (
	ReviewSortRecent  ReviewSort = "recent"
	ReviewSortHelpful ReviewSort = "helpful"
	ReviewSortRating  ReviewSort = "rating"
)

type ReviewsQuery

type ReviewsQuery struct {
	Page     int        `json:"page,omitempty"`
	PageSize int        `json:"pageSize,omitempty"`
	Sort     ReviewSort `json:"sort,omitempty"`
	Rating   *int       `json:"rating,omitempty"`
}

ReviewsQuery controls review pagination and filtering.

type ReviewsResult

type ReviewsResult struct {
	Rating  Rating   `json:"rating"`
	Reviews []Review `json:"reviews"`
	Page    int      `json:"page"`
	HasMore bool     `json:"hasMore"`
}

ReviewsResult is a paginated list of reviews with aggregate stats.

type SearchFilter

type SearchFilter struct {
	Name    string               `json:"name"`
	Key     string               `json:"key"`
	Options []SearchFilterOption `json:"options"`
}

SearchFilter describes a filterable facet returned by the store (e.g., brand, department, price range).

type SearchFilterOption

type SearchFilterOption struct {
	Value string `json:"value"`
	Label string `json:"label"`
	Count int    `json:"count,omitempty"`
}

SearchFilterOption is a single selectable value within a SearchFilter.

type SearchQuery

type SearchQuery struct {
	Query     string            `json:"query"`
	Page      int               `json:"page,omitempty"`
	PageSize  int               `json:"pageSize,omitempty"`
	Sort      SearchSort        `json:"sort,omitempty"`
	MinPrice  *int64            `json:"minPrice,omitempty"`
	MaxPrice  *int64            `json:"maxPrice,omitempty"`
	MinRating *float64          `json:"minRating,omitempty"`
	Category  string            `json:"category,omitempty"`
	Filters   map[string]string `json:"filters,omitempty"`
}

SearchQuery defines what to search for and how to filter/sort results.

type SearchResult

type SearchResult struct {
	Products []ProductSummary `json:"products"`
	Count    int              `json:"count"`
	Page     int              `json:"page"`
	HasMore  bool             `json:"hasMore"`
	Filters  []SearchFilter   `json:"filters,omitempty"`
	Warnings []string         `json:"warnings,omitempty"`
}

SearchResult is a paginated list of product summaries.

type SearchSort

type SearchSort string

SearchSort controls the ordering of search results.

const (
	SortRelevance  SearchSort = "relevance"
	SortPriceLow   SearchSort = "price_low"
	SortPriceHigh  SearchSort = "price_high"
	SortRating     SearchSort = "rating"
	SortNewest     SearchSort = "newest"
	SortBestSeller SearchSort = "best_seller"
)

type Seller

type Seller struct {
	ID   string `json:"id,omitempty"`
	Name string `json:"name"`
	URL  string `json:"url,omitempty"`
}

Seller represents the merchant selling a product.

type ShippingInfo

type ShippingInfo struct {
	Price       *Money `json:"price,omitempty"`
	Description string `json:"description,omitempty"`
	Speed       string `json:"speed,omitempty"`
}

ShippingInfo describes shipping cost and speed for an offer.

type ShippingOption

type ShippingOption struct {
	ID            string `json:"id"`
	Label         string `json:"label"`
	Price         Money  `json:"price"`
	EstimatedDays string `json:"estimatedDays,omitempty"`
	EstimatedDate string `json:"estimatedDate,omitempty"`
	IsDefault     bool   `json:"isDefault"`
}

ShippingOption is a selectable shipping speed/method during checkout.

type Spec

type Spec struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

Spec is a single key-value specification (e.g., "Weight" → "2.5 lbs").

type StarBreakdown

type StarBreakdown struct {
	Five  float64 `json:"five"`
	Four  float64 `json:"four"`
	Three float64 `json:"three"`
	Two   float64 `json:"two"`
	One   float64 `json:"one"`
}

StarBreakdown shows the distribution of ratings by star level. Values are percentages (0-100), not counts.

type Store

type Store interface {
	// Info returns metadata about this store.
	Info() StoreInfo

	// Login authenticates with the store. Handles the full lifecycle:
	//   - No existing state + no creds → starts device code/OAuth flow, returns challenge
	//   - No existing state + creds → authenticates directly, returns authenticated
	//   - Pending challenge exists → polls for completion
	//   - Already authenticated → returns authenticated (idempotent)
	Login(ctx context.Context, creds map[string]string) (*LoginResult, error)

	// Logout revokes credentials and clears stored tokens.
	Logout(ctx context.Context) error

	// WhoAmI returns the current auth state. Read-only — no side effects.
	WhoAmI(ctx context.Context) (*AccountInfo, error)

	// Search performs a product search with the given query and filters.
	Search(ctx context.Context, query *SearchQuery) (*SearchResult, error)

	// Product returns full details for a single product by its opaque ID.
	Product(ctx context.Context, productID string) (*Product, error)

	// Offers returns all available offers (sellers/conditions) for a product.
	Offers(ctx context.Context, productID string, opts *OffersQuery) (*OffersResult, error)

	// Reviews returns customer reviews for a product.
	Reviews(ctx context.Context, productID string, opts *ReviewsQuery) (*ReviewsResult, error)

	// Variants returns the full variant tree for a product.
	Variants(ctx context.Context, productID string) (*VariantsResult, error)

	// Cart returns the cart interface for this store. The cart is a singleton
	// per store instance.
	Cart() Cart

	// Checkout previews the current cart as an order. Returns a full cost
	// breakdown and a checkout ID (hash of cart state). Does not place the order.
	Checkout(ctx context.Context, opts *CheckoutOpts) (*CheckoutResult, error)

	// PlaceOrder commits the checkout. The checkoutID must match the hash
	// from a prior Checkout() call — if the cart changed, this fails with
	// ErrCartChanged.
	PlaceOrder(ctx context.Context, checkoutID string) (*Order, error)

	// Addresses returns saved shipping addresses for the authenticated account.
	Addresses(ctx context.Context) ([]Address, error)

	// PaymentMethods returns saved payment methods for the authenticated account.
	PaymentMethods(ctx context.Context) ([]PaymentMethod, error)

	// Capabilities returns which optional features this store supports.
	Capabilities() Capabilities
}

Store is the primary shopping interface. All operations are scoped to a single merchant/domain. Implementations handle their own HTTP clients, auth token refresh, and platform-specific API details.

func Resolve

func Resolve(ctx context.Context, storeValue string) (Store, error)

Resolve takes a --store value and returns a ready Store instance. Implements the full resolution chain: exact match → normalize → detect → fail. The resolver must be initialized via SetResolver before calling this function.

type StoreInfo

type StoreInfo struct {
	Name     string `json:"name"`
	Domain   string `json:"domain"`
	Provider string `json:"provider"`
	Country  string `json:"country,omitempty"`
	Currency string `json:"currency,omitempty"`
	LogoURL  string `json:"logoUrl,omitempty"`
}

StoreInfo is metadata about a store.

type VariantCombo

type VariantCombo struct {
	Values    map[string]string `json:"values"`
	ProductID string            `json:"productId"`
	Price     *Money            `json:"price,omitempty"`
	Available bool              `json:"available"`
}

VariantCombo maps a specific set of dimension values to a product ID.

type VariantDimension

type VariantDimension struct {
	Name    string          `json:"name"`
	Options []VariantOption `json:"options"`
}

VariantDimension is a single axis of variation (e.g., "Color", "Size").

type VariantInfo

type VariantInfo struct {
	ParentID string            `json:"parentId,omitempty"`
	Selected map[string]string `json:"selected,omitempty"` // e.g. {"Color": "Black"}
}

VariantInfo describes which variant this product is within its family. Use shop variants to get the full dimension/combination tree.

type VariantOption

type VariantOption struct {
	Value     string `json:"value"`
	ProductID string `json:"productId,omitempty"`
	Available bool   `json:"available"`
	ImageURL  string `json:"imageUrl,omitempty"`
}

VariantOption is a single value within a dimension.

type VariantsResult

type VariantsResult struct {
	ParentID     string             `json:"parentId"`
	Dimensions   []VariantDimension `json:"dimensions"`
	Combinations []VariantCombo     `json:"combinations"`
	Truncated    bool               `json:"truncated,omitempty"`
}

VariantsResult is the full variant tree returned by Store.Variants().

Directories

Path Synopsis
cmd
shop command
internal
app
Package app wires providers, config, and the store registry together.
Package app wires providers, config, and the store registry together.
cli
Package cli defines the cobra command tree for the shop CLI.
Package cli defines the cobra command tree for the shop CLI.
config
Package config handles loading, saving, and managing the shop CLI's persistent configuration files: config.json, registry.json, and auth state.
Package config handles loading, saving, and managing the shop CLI's persistent configuration files: config.json, registry.json, and auth state.
provider/amazon
Package amazon implements the shop.Provider interface for Amazon stores.
Package amazon implements the shop.Provider interface for Amazon stores.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL