---
name: sell-push
description: >
  Push a digital product to the user's Thinkspark Sell store (sell.thinkspark.dev)
  end-to-end: create or update the product, upload the binary to R2, attach it to
  a release, and optionally publish. Idempotent via externalId. Use when the user
  says "push to sell", "upload to my store", "publish to sell", "sell-push",
  "push this app to my store", or asks to ship a release of an existing Sell product.
user-invokable: true
argument-hint: "[path-to-file] [--name '...'] [--price 2900] [--external-id slug] [--publish]"
---

# Push a product to Thinkspark Sell

You are operating as the user's release-pilot for **Thinkspark Sell**
(<https://sell.thinkspark.dev>), a platform the user owns and ships from. This
skill chains the public REST API documented at `/docs/api` to take a file on
disk all the way to a live, purchasable product — without the user touching
the dashboard.

## When this skill fires

Triggered when the user wants to:
- Publish a brand-new product from a file they just built
- Ship a new release of an existing product (same `externalId`, new version)
- Update a product's price, description, name, or marketing fields

Stay quiet about your tooling. The user wants the outcome ("it's live"), not a
running commentary on every curl call.

## Pre-flight (run before anything else)

0. **Active plan required for writes.** Sell SaaS is €19/month flat (14-day
   trial). If the seller's plan is EXPIRED, write endpoints return HTTP 402
   `{error.code:"plan_inactive"}` — surface that response verbatim and tell
   the user to subscribe at <https://sell.thinkspark.dev/dashboard/billing>.
   Reads stay open for inspection.

1. **API key must be present.** Look in this order:
   - `$THINKSPARK_SELL_KEY` env var
   - `.env.local` in the current project root (key `THINKSPARK_SELL_KEY=...`)
   - `~/.thinkspark-sell.env`

   If absent, STOP and instruct the user verbatim:

   > Pour utiliser ce skill, ajoute ta clé à ton shell :
   >
   > ```bash
   > echo 'export THINKSPARK_SELL_KEY="tsk_live_…"' >> ~/.zshrc && source ~/.zshrc
   > ```
   >
   > Génère-la sur <https://sell.thinkspark.dev/dashboard/settings> avec les
   > scopes `products:write`, `releases:write`, `uploads:write`.

   Do NOT proceed without a key. Do not invent one.

2. **Reject `tsk_test_…` keys for any prompt mentioning "publish".**
   Test-mode keys force `status=DRAFT` and refuse `/publish`. If the user wants
   to publish, tell them they need a `tsk_live_…` key.

3. **Required inputs** (extract from the user's prompt; ask for any that are
   missing, in a SINGLE follow-up message — never one-question-at-a-time):
   - `file` — path to the binary to ship (zip / dmg / pdf / mp4 / etc.). Must exist on disk.
   - `name` — product name (only if creating; reuse existing if updating)
   - `priceCents` — integer minor units (2900 = $29.00). Default: ask. Never assume.
   - `externalId` — stable slug. Default: `slugify(basename(file))` (e.g. `wacomd-1.0.0.zip` → `wacomd`)
   - `version` — release label like `1.0.0` or `2026.05`. Default: parse from filename, else ask.
   - `currency` — default `USD`
   - `description` — optional, markdown supported

4. **Decide flow upfront:**
   - GET `/api/v1/products/{externalId}` first. If 200 → **update flow**.
     If 404 → **create flow**. This avoids accidentally creating a duplicate
     when the user re-runs after a network blip.

## The pipeline (run sequentially, stop on the first failure)

All calls use:
- Base URL: `https://sell.thinkspark.dev`
- Header: `Authorization: Bearer $THINKSPARK_SELL_KEY`
- Header: `Content-Type: application/json` (except R2 PUT — use the file's MIME type)

### Step 1 — create or upsert the product

```bash
curl -sS -X POST "$BASE/api/v1/products" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d @- <<JSON
{
  "externalId": "$EXTERNAL_ID",
  "name": "$NAME",
  "priceCents": $PRICE_CENTS,
  "currency": "$CURRENCY",
  "productType": "DIGITAL_DOWNLOAD",
  "description": $DESCRIPTION_JSON
}
JSON
```

A `409 conflict` on this call means the `externalId` is reserved by a
different slug clash — re-throw to the user with the suggested fix.

### Step 2 — presigned upload URL

```bash
curl -sS -X POST "$BASE/api/v1/uploads" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d "{\"fileName\":\"$FILENAME\",\"contentType\":\"$MIME\",\"contentLength\":$FILESIZE}"
```

Parse `.url` and `.key` from the response. URL is valid 15 minutes.

### Step 3 — PUT the file to R2 directly

```bash
curl -sS -X PUT --data-binary @"$FILE_PATH" \
  -H "Content-Type: $MIME" \
  -w "%{http_code}" -o /dev/null \
  "$PRESIGNED_URL"
```

Expect `200`. Anything else → abort the pipeline.

### Step 4 — compute SHA-256 (optional but recommended)

```bash
shasum -a 256 "$FILE_PATH" | cut -d' ' -f1
```

Pass it in step 5 so the buyer page shows an integrity hash.

### Step 5 — create the release (idempotent by externalId)

Default release `externalId` = `${PRODUCT_EXTERNAL_ID}-v${VERSION}` (e.g.
`wacomd-v1.0.0`). This makes re-running with the same version safe.

```bash
curl -sS -X POST "$BASE/api/v1/products/$EXTERNAL_ID/releases" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d "{\"version\":\"$VERSION\",\"externalId\":\"$REL_EXTERNAL_ID\",\"changelog\":$CHANGELOG_JSON}"
```

### Step 6 — register the file against the release

```bash
curl -sS -X POST "$BASE/api/v1/products/$EXTERNAL_ID/releases/$REL_EXTERNAL_ID/files" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d "{\"storageKey\":\"$STORAGE_KEY\",\"fileName\":\"$FILENAME\",\"fileSize\":$FILESIZE,\"mimeType\":\"$MIME\",\"sha256\":\"$SHA256\"}"
```

### Step 7 — publish (gated)

**Before publishing, confirm with the user.** Publishing changes what's live
on the public store — never an automatic side-effect.

If the user already said `--publish` or "et publie", skip the confirmation:

```bash
# Publish the release (sets isCurrent, demotes prior current)
curl -sS -X POST "$BASE/api/v1/products/$EXTERNAL_ID/releases/$REL_EXTERNAL_ID/publish" \
  -H "Authorization: Bearer $KEY"

# Publish the product (only needed the first time — once PUBLISHED it stays so)
curl -sS -X POST "$BASE/api/v1/products/$EXTERNAL_ID/publish" \
  -H "Authorization: Bearer $KEY"
```

The product-publish step is a no-op once the product is already PUBLISHED;
calling it is still safe.

## Error handling rules

- **401 / 403** → key issue. Stop and report the exact `error.message` from the JSON body.
- **404 not_found** in step 5/6 → means step 1 didn't actually create. Re-run step 1 then continue.
- **409 conflict** on product create → `externalId` collision. Show the user and ask for a new one.
- **429 rate_limited** → the API gives `Retry-After` guidance in the body. Sleep then retry the same step (idempotent).
- **5xx** → wait 2s, retry once. If it fails again, stop and show the full response.
- Never swallow errors. The user must see what broke.

## Reporting back

When done, output ONE compact summary block — not a per-step trace. Format:

```
✓ wacomd v1.0.0 published
  https://sell.thinkspark.dev/p/<seller-slug>/wacomd
  Price: $19.00 · 1 file · 4.2 MB
```

If you didn't publish (user declined or chose to review), end with:

```
✓ wacomd v1.0.0 uploaded (DRAFT — not yet live)
  Review: https://sell.thinkspark.dev/dashboard/products/<id>
  To publish: POST /api/v1/products/wacomd/publish
```

## What you must NOT do

- Don't put the API key in any prompt, log, commit, or file you write.
- Don't run any `vercel`, `prisma`, or DB command — this skill talks to the
  REST API only.
- Don't delete files from R2 manually — DELETE on a product archives it
  (soft-delete) so past buyers keep access.
- Don't auto-publish unless the user explicitly asked for it.
- Don't pivot to a different product without re-confirming the `externalId`
  with the user — silent retargeting is the worst kind of bug here.

## Reference

Full API documentation: <https://sell.thinkspark.dev/docs/api>
