Add gumroad upsells command for upsells and cross-sells#168
Conversation
Wraps the new /v2/upsells API with list, view, create, update, and delete subcommands. create/update send JSON bodies so nested upsell_variants and offer codes round-trip cleanly; update fetches the upsell first and merges flag overrides so partial edits don't drop associations the API replaces wholesale. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile SummaryAdds the
Confidence Score: 5/5New command group with no auth changes; the merge logic in update is the main behavioral surface and is thoroughly exercised by the test suite. After tracing every merge path in update.go — including the universal-to-targeted conversion, type-switch normalisation, and remove-offer handling — each scenario is covered by a corresponding test and behaves correctly. The previously flagged edge case around --selected-product on a universal upsell is correctly resolved by lines 129-131 setting body["universal"] = false before the guard at line 136 can fire. No logic errors or data-loss paths were found in the changed code. update.go warrants the closest read: it fetches the current upsell unconditionally, before the dry-run gate in runUpsellWrite, so --dry-run still issues a live GET. All other files are straightforward. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[gumroad upsells] --> B[list]
A --> C[view id]
A --> D[create]
A --> E[update id]
A --> F[delete id]
D --> D1{Validate flags}
D1 -->|cross-sell conflict / missing audience / bad offer-variant| D2[UsageError]
D1 -->|valid| D3[Build JSON body]
D3 --> D4[POST /upsells]
D4 --> D5[renderUpsellWriteResult]
E --> E1{RequireAnyFlagChanged?}
E1 -->|no| E2[UsageError]
E1 -->|yes| E3[fetchUpsell GET /upsells/id]
E3 --> E4[currentUpsellBody]
E4 --> E5[Merge changed flags]
E5 --> E6{opts.DryRun?}
E6 -->|yes| E7[printUpsellDryRun]
E6 -->|no| E8[PUT /upsells/id JSON]
E8 --> E9[renderUpsellWriteResult]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[gumroad upsells] --> B[list]
A --> C[view id]
A --> D[create]
A --> E[update id]
A --> F[delete id]
D --> D1{Validate flags}
D1 -->|cross-sell conflict / missing audience / bad offer-variant| D2[UsageError]
D1 -->|valid| D3[Build JSON body]
D3 --> D4[POST /upsells]
D4 --> D5[renderUpsellWriteResult]
E --> E1{RequireAnyFlagChanged?}
E1 -->|no| E2[UsageError]
E1 -->|yes| E3[fetchUpsell GET /upsells/id]
E3 --> E4[currentUpsellBody]
E4 --> E5[Merge changed flags]
E5 --> E6{opts.DryRun?}
E6 -->|yes| E7[printUpsellDryRun]
E6 -->|no| E8[PUT /upsells/id JSON]
E8 --> E9[renderUpsellWriteResult]
Reviews (7): Last reviewed commit: "Require an audience for a cross-sell" | Re-trigger Greptile |
Address Cursor Bugbot findings on the fetch-then-merge update path: - drop the inherited variant_id when --product changes without --variant, so a version ID from the old product is not paired with the new product - gate --selected-product and --offer-variant on flags.Changed so an explicit empty --selected-product clears the set - omit product_ids when the result is universal Add tests for these paths plus dry-run/plain/view rendering, raising package coverage above the 85% gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow the API change that preserves omitted associations on update: - send explicit empty values (variant_id, product_ids, offer_code) to clear, instead of dropping keys which now means "leave unchanged" - clear the offered version when the product changes without a new --variant - drop product_ids for a universal cross-sell unless --selected-product is given - drop version upgrades when an upsell becomes a cross-sell - skip product_ids on a universal create Drop the section-separator comments from the test file per the no-comments style rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the update path so a create with an empty --selected-product does not POST a product_ids array containing blank entries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- create: --selected-product requires --cross-sell and conflicts with --universal - update: --universal and --selected-product cannot be combined; passing --selected-product alone retargets a universal cross-sell (sets universal false) - create only sends product_ids for a targeted cross-sell Erroring on contradictory flags is clearer than silently dropping one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a single consistency check shared by create and update: cross-sell-only flags (--variant, --universal, --selected-product, --replace-selected-products) require a cross-sell, and --offer-variant is rejected on a cross-sell. For update the check runs against the resulting offer type (current upsell merged with the changed flags), so editing a version upsell with cross-sell-only flags is rejected and passing --selected-product alone retargets a universal cross-sell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Symmetric to clearing version upgrades on a cross-sell: when the resulting offer type is a version upsell, clear variant_id, product_ids, universal, and replace_selected_products so a converting PUT does not carry stale cross-sell targeting fields. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ee99747. Configure here.
upsells update --product on a version upsell kept the old product's upsell_variants in the merged body, so the API rejected them (the variants don't belong to the new product) and the product change failed. Clear the version mappings on a product change unless new ones are given via --offer-variant, mirroring how the offered version is cleared. Found by exercising the CLI against a local server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The create help says a cross-sell takes --universal or --selected-product, but neither was enforced, so a cross-sell could be created (or an upsell converted) with no audience -- it would never show to any buyer. Require --universal or at least one --selected-product for a cross-sell on create, and for the resulting cross-sell on update (including clearing the selected products of a non-universal cross-sell). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

What
Adds a
gumroad upsellscommand group wrapping the new/v2/upsellsAPI:list,view,create,update,delete. Previouslygumroad upsellsreturned "unknown command".create/updatesend JSON request bodies so nestedupsell_variantsand theoffer_coderound-trip correctly (form-encoding sorts keys and turns the variants array into integer-keyed hashes).updatefetches the upsell first and merges only the flags you pass, so a partial edit doesn't drop the associations the API replaces wholesale.--remove-offerclears the discount.--json/--jq,--plain,--quiet, and--dry-runlike the other commands. Man page and skill docs included.Why
Companion to the Rails API for upsells/cross-sells (
antiwork/gumroad#5551). Sellers driving their store from the CLI can now automate upsells and cross-sells alongside products, offer codes, and variants.Part of antiwork/gumroad#5551
AI disclosure: implemented with Claude Opus 4.8 via Claude Code.
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
New mutating seller APIs that change checkout upsell/cross-sell configuration; logic is complex but heavily validated and tested, with no auth or payment-path changes.
Overview
Adds
gumroad upsells(list,view,create,update,delete) wired to/upsells, registered on the root command so the group is no longer unknown.Create and update post JSON bodies (via
PostJSON/PutJSON) so nestedupsell_variantsandoffer_codeserialize correctly. Update GETs the current upsell, merges only changed flags into a full PUT payload, and handles type switches (version upsell vs cross-sell), audience rules, and--remove-offer. Shared validation covers cross-sell vs upsell flags, discounts, and audience (--universalvs--selected-product).Output modes match other commands (
--json,--plain,--quiet,--dry-run). SKILL.md documents agent usage; a large upsells_test.go covers endpoints, payloads, merge behavior, and flag errors.Reviewed by Cursor Bugbot for commit 8e12c21. Bugbot is set up for automated code reviews on this repo. Configure here.