Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
965697a
chore(spec): vendor the by-key fire and app-emails routes
thedogwiththedataonit Oct 16, 2026
9bec9c3
feat(cli): send by key, app-emails list|get|upsert|kit, trigger keys
thedogwiththedataonit Oct 13, 2026
b28b4f2
docs: send by key and app emails in README and CHANGELOG
thedogwiththedataonit Oct 13, 2026
0accebb
test(types): key registry over main's contract artifacts
thedogwiththedataonit Oct 16, 2026
d484392
chore: 0.17.0 on top of main's 0.16.0; app email SDK gate
thedogwiththedataonit Oct 16, 2026
3ac0547
fix(app-emails,send): check keys and vocabularies locally; stdin bodi…
thedogwiththedataonit Oct 16, 2026
f354529
fix(send,app-emails): SDK 13.1.0/13.2.0 gate; secret fields; not-star…
thedogwiththedataonit Oct 16, 2026
4c882ea
fix(app-emails): resumed `list --all` refuses its starting cursor; 0.…
thedogwiththedataonit Oct 20, 2026
cd0d493
chore(spec): vendor brew-v2 #2169 @ 6f8ef50544 whole
thedogwiththedataonit Oct 21, 2026
0a8dd76
fix(app-emails): adapt to #2169's refusals and the draft-over-live wa…
thedogwiththedataonit Oct 21, 2026
2795a37
chore(release): 0.19.0 after main's published 0.18.0; SDK gate 13.5.0…
thedogwiththedataonit Oct 25, 2026
d86ccf9
test(app-emails): map send and app-emails flags to their body keys; m…
thedogwiththedataonit Oct 31, 2026
ffe775d
chore(release): app-emails is 0.20.0 after main's published 0.19.0
thedogwiththedataonit Oct 31, 2026
781a53e
chore: sync API spec to brew-v2 #2169 5084a5202f
thedogwiththedataonit Nov 1, 2026
8186eb4
fix(app-emails): a kit key refuses a --domain of either other purpose
thedogwiththedataonit Nov 1, 2026
f1d30ff
fix(app-emails): kit checks a selection after --input; upsert reads s…
thedogwiththedataonit Nov 1, 2026
efeeba6
test(types): compile the send registry against the installed SDK
thedogwiththedataonit Nov 1, 2026
3cef01f
fix(app-emails): kit prints each key's warnings and a draft over a li…
thedogwiththedataonit Nov 1, 2026
232600a
chore: sync API spec to brew-v2 #2169 b884e78157
thedogwiththedataonit Nov 1, 2026
3d57b54
fix(app-emails): upsert --publish turns an email unpublished in Brew …
thedogwiththedataonit Nov 1, 2026
49015be
fix(app-emails): upsert confirms --publish over an inline publish: false
thedogwiththedataonit Nov 1, 2026
13c93af
chore: sync API spec to brew-v2 #2169 02a3946b18
thedogwiththedataonit Nov 1, 2026
4104a4c
docs(app-emails): the same upsert plus --publish turns an unpublished…
thedogwiththedataonit Nov 1, 2026
86accad
docs(app-emails): a bare --publish repeats a kit key's starter setup,…
thedogwiththedataonit Nov 1, 2026
f1d019a
chore: sync API spec to brew-v2 #2169 6e4accea4c
thedogwiththedataonit Nov 1, 2026
536a9b8
test(app-emails): the APP_EMAIL_NOT_PUBLISHED fixture quotes the curr…
thedogwiththedataonit Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,80 @@
# Changelog

## Unreleased
## 0.20.0

Needs `@brew.new/sdk` 13.6.0: `brew.send` and
`automations.triggers.fireByKey` (GetBrew/typescript-sdk#55, planned
13.5.0) and `appEmails` (GetBrew/typescript-sdk#64, planned 13.6.0), both
after the published 13.4.0 (email `lang`). npm already has CLI 0.19.0
(released from `main` without this work), so this is 0.20.0.
Neither PR is published yet; see the app email SDK gate in `RELEASING.md`.
The API side is GetBrew/brew-v2#2169, one PR for trigger keys, fire by key,
the team sandbox, app emails and browser sign-in.

### Added

- **`brew-cli send <key>`** sends the app email behind a trigger key: the
same call as `brew.send(key, payload)`
(`POST /v1/automations/triggers/by-key/{triggerKey}/fire`). `--to
<email>` sets `payload.email` (over `--data`), `--data <json|->` carries
the rest of the payload, `--idempotency-key` makes a retry replay.
Destructive like `automations triggers fire`: exit 4 without `--yes`.
A send with no recipient exits 2 before any request.
`automations.triggers.fireByKey` is the same route, so it is a reviewed
SDK skip (alias of `send`).
- **`brew-cli app-emails list | get <key> | upsert <key> | kit`** bind
`GET /v1/app-emails`, `GET|PUT /v1/app-emails/{triggerKey}` and
`POST /v1/app-emails/kit`. `list` and `get` show each key's sender
(`domain`, `sandbox`, `none`) and readiness. `upsert` sets up one app
email (starter with `--starter`, or your own `--source-file` with
`--format` taken from `.html`, `.mjml`, `.jsx` or `.tsx`; `--title`,
`--subject`, `--preview-text`, `--domain`, `--overwrite`; `--input`
carries the rest, such as `payloadSchema` fields marked `secret: true`
for a one-time code or sign-in link) and publishes it, so it confirms;
`--no-publish` saves without the gate. A re-run leaves an email
unpublished in Brew off (warning `APP_EMAIL_NOT_PUBLISHED`); the same
`upsert` command plus `--publish` turns it back on (a bare `--publish`
sets a kit key up from its starter again, replacing a design built from
`--source-file`, and any other key is refused). `kit` sets up
the auth and lifecycle starter kit (`--sets`, `--stack`, `--keys`,
`--domain`, `--overwrite`, `--idempotency-key`), confirms, prints
per-key results and the stack's code snippets, and exits 1 when a key
failed. Its human output says
what each failed key needs: run it again to finish a key that failed
part way, a `--domain` of the key's purpose (or none) for
`APP_EMAIL_DOMAIN_PURPOSE_MISMATCH` (a transactional domain on welcome
or win-back is refused too), unpublishing the automations it names
(`error.details.automationIds`) for `APP_EMAIL_TRIGGER_IN_USE`, and
`--overwrite` for `APP_EMAIL_EDITED_IN_BREW`; after an
`--idempotency-key` run, a new key, since the same one replays the
result for 24 hours. `upsert` exits 1 on the
same refusals, `APP_EMAIL_SETUP_IN_PROGRESS` and
`CONTRACT_LOCKED_BY_PUBLISHED_AUTOMATIONS`; when the published version
keeps sending under a newer draft (`DRAFT_SAVED_NOT_LIVE`), it says so
and still prints the `send` line.
Both use the SDK's 5-minute defaults as their whole-command deadline
and need a key with the `automations` and `emails` scopes. A key that
breaks the trigger key grammar, an unknown `--format`, `--sets`,
`--stack` or kit key, an empty list, `keys` with `sets`/`stack` (as
flags or in `--input`), or `--input -` with `--source-file -` exits 2
before any request; a body on stdin (`--input -`) always confirms.
- `automations triggers create --key <key>` and `automations triggers
update --key <key>` set the stable key code fires by; `triggers list`
gains a `KEY` column (`get` returns `key` and `appEmail` in the row).
- `brew-cli types` appends a `BrewTriggerKeyRegistry` augmentation for
every keyed trigger, so `brew.send('order.paid', …)` checks the key and
the payload at compile time. A workspace with no keyed triggers gets
the same bytes as before (no `--check` drift); `--no-registry` leaves
the block out.
- The vendored spec is the GetBrew/brew-v2#2169 head `6e4accea4c` (main's
spec, brew-v2 `373e900a9a`, plus the DX delta): the by-key fire and the
app-emails routes, `key` / `appEmail` on trigger rows, the sandbox error
codes (`DOMAIN_REQUIRED`, `SANDBOX_DAILY_LIMIT`), the app email refusals
above, `DKIM_SELECTOR_IN_USE` warnings with `suggestedNames` on
`domains add` (printed verbatim), and the hosted auth hook connection
routes, pending in the spec parity test until `integrations connect`
lands. Re-vendor from brew-v2 main once #2169 merges.

- **`brew-cli login` signs in through the browser.** On an interactive
terminal it starts a sign-in on brew.new, prints the link and a code
("Check the code in your browser matches: WDJB-MJHT"), opens the page,
Expand Down Expand Up @@ -33,7 +104,6 @@
login that stores a key drops an older pending sign-in. A sign-in
against a non-default `--api-url` stores that URL with the key. The
`Signed in as …` line shows even under `--quiet`.

- **`brew-cli mcp install`** adds the Brew MCP server to Cursor
(`.cursor/mcp.json`), VS Code (`.vscode/mcp.json`), Claude Code
(`.mcp.json`), Gemini CLI (`.gemini/settings.json`), and, when named,
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,30 @@ data on stdout and progress on stderr, and documents itself:
brew-cli emails send --help
```

## Send app email by key

An app email is a trigger with a stable key (`auth.password-reset`,
`order.paid`), wired to a design and a published automation. Your code
sends it with `brew.send(key, payload)` from `@brew.new/sdk`; the CLI does
the same from a terminal or an agent:

```bash
brew-cli app-emails kit --stack better-auth --yes # auth + lifecycle starter kit, published
brew-cli app-emails upsert order.paid --source-file emails/order-paid.tsx --yes
brew-cli app-emails list # sender and readiness per key
brew-cli send auth.password-reset --to you@example.com --data '{"confirmationUrl":"https://…"}' --yes
brew-cli types --out lib/brew-contracts.ts # payload types + the brew.send key registry
```

Until a sending domain verifies, app emails go out from the team-only
sandbox: members of your Brew organization receive them, under a daily
cap. `send`, `upsert` and `kit` are destructive (they send or publish) and
exit 4 without `--yes` off a terminal. `upsert` and `kit` need a key with
both the `automations` and `emails` scopes. A mistyped key, format, set or
stack exits 2 before any request. `types` adds a
`declare module '@brew.new/sdk'` block for keyed triggers; pass
`--no-registry` in a project that does not install the SDK.

## Connect your AI editor to Brew (MCP)

```bash
Expand Down
21 changes: 21 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,27 @@ lockfile checksums must never be invented. The canvas tests must preserve
`contacts.import` and the import response from SDK 13.3.0, the minimum this
CLI depends on (`^13.3.0`).

### App email SDK gate

`send`, `app-emails` and the `types` key registry call `brew.send` and
`brew.appEmails`, which no published SDK has yet:
GetBrew/typescript-sdk#55 (`send`, `automations.triggers.fireByKey`) and
GetBrew/typescript-sdk#64 (`appEmails`, the auth adapters), planned as 13.5.0
and 13.6.0, after the published 13.4.0 (email `lang`). Until 13.6.0 is on
npm, the dependency and `bun.lock` stay on the
published `^13.3.0`, and typecheck and parity fail in CI on these commands.
Publish the SDK first, then
`bun add @brew.new/sdk@^13.6.0` (it regenerates `bun.lock` from the
registry; never hand-write a lockfile entry), run a frozen install, the full
checks and `docs:commands:check`, and release the CLI.

The API side must be live first: GetBrew/brew-v2#2169 deployed (the by-key
fire, `/v1/app-emails`, the team sandbox), and the live spec
(`https://brew.new/openapi/public-api-v1.yaml`, what the spec-drift
sentinel reads) matching `openapi/public-api-v1.yaml`,
which is the #2169 head `6e4accea4c` (main's spec, brew-v2 `373e900a9a`,
plus the DX delta).

```bash
# on a clean main with CI green
npm version <patch|minor|major> --no-git-tag-version
Expand Down
113 changes: 110 additions & 3 deletions docs/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- GENERATED FILE — do not edit. Regenerate with `bun run docs:commands`. -->

150 commands. Classes: read (always safe), write
155 commands. Classes: read (always safe), write
(mutating, retry-safe), destructive (irreversible — the confirmation
protocol applies: interactive y/N on a TTY, exit 4 + JSON envelope with
a `confirmCommand` otherwise, `--yes` to proceed).
Expand Down Expand Up @@ -75,7 +75,7 @@ a `confirmCommand` otherwise, `--yes` to proceed).
| `brew-cli sends cancel` | destructive | `POST /v1/sends/{sendId}/cancel` | Cancel a scheduled or queued send before it goes out |
| `brew-cli sends pause` | write | `POST /v1/sends/{sendId}/pause` | Pause an in-flight or scheduled send (resumable) |
| `brew-cli sends resume` | write | `POST /v1/sends/{sendId}/resume` | Resume a paused gradual send (the unsent tail is re-spread); the send reports `running` again |
| `brew-cli types` | read | — | Generate TypeScript payload contracts for this workspace's triggers into your codebase; --check is the CI drift gate (exit 1 on drift). Needs the automations scope |
| `brew-cli types` | read | — | Generate TypeScript payload contracts for this workspace's triggers (plus the brew.send key registry for keyed triggers) into your codebase; --check is the CI drift gate (exit 1 on drift). Needs the automations scope |
| `brew-cli audiences list` | read | `GET /v1/audiences` | List audience segments; one segment is `audiences get` |
| `brew-cli audiences get` | read | `GET /v1/audiences/{audienceId}` | Fetch one audience segment by id — the bare row |
| `brew-cli audiences create` | write | `POST /v1/audiences` | Create an audience segment from a filter definition |
Expand Down Expand Up @@ -104,6 +104,11 @@ a `confirmCommand` otherwise, `--yes` to proceed).
| `brew-cli automations triggers update` | write | `PATCH /v1/automations/triggers/{triggerEventId}` | Update a trigger event (title, description, payload schema) |
| `brew-cli automations triggers delete` | destructive | `DELETE /v1/automations/triggers/{triggerEventId}` | Delete a trigger event (rejected while automations depend on it) |
| `brew-cli automations triggers fire` | destructive | `POST /v1/automations/triggers/{triggerEventId}/fire` | Fire a trigger event with a payload (starts LIVE runs) |
| `brew-cli send` | destructive | `POST /v1/automations/triggers/by-key/{triggerKey}/fire` | Send the app email behind a trigger key (fires the key: starts LIVE runs) |
| `brew-cli app-emails list` | read | `GET /v1/app-emails` | List the app emails your code sends by key, with sender and readiness |
| `brew-cli app-emails get` | read | `GET /v1/app-emails/{triggerKey}` | Fetch one app email by its trigger key (404 TRIGGER_KEY_NOT_FOUND when none is set up) |
| `brew-cli app-emails upsert` | destructive | `PUT /v1/app-emails/{triggerKey}` | Set up one app email by key in one idempotent call (trigger, contract, design, automation) and publish it. Needs the automations and emails scopes |
| `brew-cli app-emails kit` | destructive | `POST /v1/app-emails/kit` | Set up and publish the auth + lifecycle starter kit (per-key results; exits 1 when a key failed, re-run to finish). Needs the automations and emails scopes |
| `brew-cli automations runs list` | read | `GET /v1/automations/runs` | List automation runs (live + test history); one run is `automations runs get` |
| `brew-cli automations runs get` | read | `GET /v1/automations/runs/{automationRunId}` | Fetch one automation run by id — the bare row |
| `brew-cli automations runs cancel` | destructive | `POST /v1/automations/runs/{automationRunId}/cancel` | Cancel one in-flight automation run (event execution or test run) — nothing further is sent, and it can never be resumed |
Expand Down Expand Up @@ -1128,11 +1133,12 @@ brew-cli sends resume snd_123

### brew-cli types

Generate TypeScript payload contracts for this workspace's triggers into your codebase; --check is the CI drift gate (exit 1 on drift). Needs the automations scope
Generate TypeScript payload contracts for this workspace's triggers (plus the brew.send key registry for keyed triggers) into your codebase; --check is the CI drift gate (exit 1 on drift). Needs the automations scope

- Class: read
- Derived from `brew.automations.triggers.list(...)`, `brew.automations.triggers.getContract(...)`
- `--out <file>` — Output file (default brew-contracts.ts)
- `--no-registry` — Leave out the BrewTriggerKeyRegistry augmentation for keyed triggers (for code that does not use @brew.new/sdk's send)
- `--check` — Verify the output file is up to date instead of writing; exits 1 on drift

```bash
Expand Down Expand Up @@ -1526,10 +1532,12 @@ Create a trigger event (title + typed payload schema)
- SDK: `brew.automations.triggers.create(...)`
- `--title <title>` — Trigger event title
- `--description <text>` — Trigger event description
- `--key <key>` — Stable trigger key your code fires by (`send <key>`), e.g. order.paid; unique per brand
- `--input <json>` — Full JSON request body, or - to read stdin (flags override it)
- `--idempotency-key <key>` — Idempotency-Key for safe retries (auto-generated otherwise)

```bash
brew-cli automations triggers create --title "Order paid" --key order.paid --input '{"payloadSchema":{"type":"object","fields":[{"key":"email","type":"string","required":true}]}}'
brew-cli automations triggers create --title user.signup --input '{"payloadSchema":{"type":"object","fields":[{"key":"userId","type":"string","required":true}]}}'
```

Expand Down Expand Up @@ -1560,10 +1568,12 @@ Update a trigger event (title, description, payload schema)
- Argument `triggerEventId` — Id of the trigger event to update
- `--title <title>` — New title
- `--description <text>` — New description
- `--key <key>` — Set the trigger key your code fires by (clear it with --input '{"key":null}')
- `--input <json>` — Full JSON request body, or - to read stdin (flags override it)

```bash
brew-cli automations triggers update tev_123 --title user.signup.v2
brew-cli automations triggers update tev_123 --key user.signup
brew-cli automations triggers update tev_123 --input '{"payloadSchema":{"type":"object","fields":[]}}'
```

Expand Down Expand Up @@ -1595,6 +1605,102 @@ Fire a trigger event with a payload (starts LIVE runs)
brew-cli automations triggers fire tri_signup --input '{"payload":{"email":"jane@example.com"}}' --yes
```

### brew-cli send

Send the app email behind a trigger key (fires the key: starts LIVE runs)

- Route: `POST /v1/automations/triggers/by-key/{triggerKey}/fire`
- Class: destructive
- SDK: `brew.send(...)`
- Argument `key` — Trigger key, e.g. auth.password-reset or order.paid
- `--to <email>` — Recipient address (sets payload.email; overrides --data)
- `--data <json>` — Payload JSON object (the values the email reads as {{ trigger.* }}), or - to read stdin
- `--idempotency-key <key>` — Idempotency-Key for safe retries (auto-generated otherwise)

```bash
brew-cli send lifecycle.welcome --to jane@example.com --yes
brew-cli send auth.password-reset --to jane@example.com --data '{"userName":"Jane","confirmationUrl":"https://app.example.com/reset?token=…"}' --yes
echo '{"email":"jane@example.com","orderId":"ord_1"}' | brew-cli send order.paid --data - --idempotency-key order.paid:ord_1 --yes
```

### brew-cli app-emails list

List the app emails your code sends by key, with sender and readiness

- Route: `GET /v1/app-emails`
- Class: read
- SDK: `brew.appEmails.list(...)`
- `--limit <n>` — Page size, 1-100 (default 100)
- `--cursor <cursor>` — Opaque pagination cursor from a previous page
- `--all` — Follow the cursor and return every page as one result

```bash
brew-cli app-emails list
brew-cli app-emails list --all --json
```

### brew-cli app-emails get

Fetch one app email by its trigger key (404 TRIGGER_KEY_NOT_FOUND when none is set up)

- Route: `GET /v1/app-emails/{triggerKey}`
- Class: read
- SDK: `brew.appEmails.get(...)`
- Argument `triggerKey` — Trigger key, e.g. auth.password-reset

```bash
brew-cli app-emails get auth.password-reset
brew-cli app-emails get order.paid --json
```

### brew-cli app-emails upsert

Set up one app email by key in one idempotent call (trigger, contract, design, automation) and publish it. Needs the automations and emails scopes

- Route: `PUT /v1/app-emails/{triggerKey}`
- Class: destructive
- SDK: `brew.appEmails.upsert(...)`
- Argument `triggerKey` — Trigger key your code sends by: a kit key (auth.password-reset) or your own (order.paid)
- `--starter <id>` — Starter-kit template to use (a kit key alone uses its own starter)
- `--source-file <path>` — Your own design source (HTML, MJML or React Email JSX), or - for stdin
- `--format <format>` — Source format: html | mjml | jsx (default: from the file extension)
- `--title <title>` — App email title in Brew
- `--subject <text>` — Subject line
- `--preview-text <text>` — Inbox preview text
- `--domain <domainId>` — Sending domain id (default: a verified domain for the purpose, else the team-only sandbox). A kit key refuses a domain of the other purpose (transactional: auth, trial ending, payment failed; marketing: welcome, win-back)
- `--publish` — Publish it even if it was unpublished in Brew since setup, which a re-run otherwise leaves off. Pass the same --starter or --source-file the setup used
- `--no-publish` — Save the setup without publishing the automation
- `--overwrite` — Replace a design that was edited in Brew since setup
- `--input <json>` — Full JSON request body, or - to read stdin (flags override it)

```bash
brew-cli app-emails upsert auth.password-reset --yes
brew-cli app-emails upsert order.paid --source-file emails/order-paid.tsx --subject "Your order is confirmed" --yes
brew-cli app-emails upsert lifecycle.welcome --overwrite --yes
brew-cli app-emails upsert account.sign-in-code --source-file emails/sign-in-code.tsx --input '{"payloadSchema":{"fields":[{"key":"code","type":"string","required":true,"secret":true}]}}' --yes
```

### brew-cli app-emails kit

Set up and publish the auth + lifecycle starter kit (per-key results; exits 1 when a key failed, re-run to finish). Needs the automations and emails scopes

- Route: `POST /v1/app-emails/kit`
- Class: destructive
- SDK: `brew.appEmails.setUpKit(...)`
- `--sets <sets>` — Comma-separated kit sets: auth, lifecycle (default both)
- `--stack <provider>` — Auth library: better-auth | authjs | supabase | clerk | firebase | custom (narrows the auth keys, picks the snippets)
- `--keys <keys>` — Comma-separated exact kit keys instead of sets (auth.password-reset,lifecycle.welcome)
- `--domain <domainId>` — Sending domain id for every key (default: a verified domain per purpose, else the team-only sandbox). Keys of the other purpose fail with APP_EMAIL_DOMAIN_PURPOSE_MISMATCH
- `--overwrite` — Replace designs that were edited in Brew since setup
- `--input <json>` — Full JSON request body, or - to read stdin (flags override it)
- `--idempotency-key <key>` — Idempotency-Key for safe retries (auto-generated otherwise)

```bash
brew-cli app-emails kit --stack better-auth --yes
brew-cli app-emails kit --sets lifecycle --yes --json
brew-cli app-emails kit --keys auth.password-reset,auth.verify-email --domain dom_123 --yes
```

### brew-cli automations runs list

List automation runs (live + test history); one run is `automations runs get`
Expand Down Expand Up @@ -2752,6 +2858,7 @@ SDK methods intentionally without a dedicated command:
- `chats.listAll` — auto-pager covered by `chats list --all`
- `emails.comments.listAllMessages` — auto-pager covered by `emails comments list <emailId> --comment-id <id> --all`
- `notifications.listAll` — auto-pager covered by `notifications list --all`
- `automations.triggers.fireByKey` — alias of `send` (the same by-key fire route), exposed as `send <key>`
- `brand.update` — SDK alias of brand.patch, exposed as `brand update`
- `brands.waitForExtraction` — read-only SDK polling helper over brands.get; lifecycle reads are exposed by `brands get`
- `withBrand` — client scoping helper activated by the global `--brand`; not an API command
Expand Down
Loading