@getbrevo/cli 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @getbrevo/cli
2
2
 
3
+ ## 2.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f3eefcc: **UI apps are GA** (BEX-290). `brevo app create` offers the _UI app_ type in every build (interactive-only — no `--type` flag), and `brevo app install [account-id]` / `brevo app uninstall [account-id]` now ship in the published CLI. Public app distribution (`--distribution public`, `app submit` / `app status` / `app withdraw`) remains pre-GA and excluded from published builds.
8
+
9
+ **Per-entry card `size`** (BEX-416). Each `ui_app.surface_point_list` entry accepts an optional `size` with `width` / `height` as CSS lengths — `"<positive integer>px"` or `"<1-100>%"` — overriding the host page's default card size for that placement. Both axes are optional; an omitted axis keeps the host default.
10
+
11
+ **CTA fields moved into each entry** (BEX-426). `label`, `more_info`, `redirect_link` and `modal_iframe_url` now live on each `surface_point_list` entry instead of the `ui_app` root, so every placement can carry its own label and destination. The root spellings are refused with a migration hint, and the per-type rules run per entry (`actionLink` requires `redirect_link` and refuses `modal_iframe_url`; `iframeExtension` the reverse). `brevo app create` authors exactly one placement — add more by editing `app-config.json` and running `brevo app upload`.
12
+
13
+ **Install/uninstall improvements.** Both commands refuse an app that is not a UI app (previously an OAuth app "installed" with nothing to render), name the resolved target account — `installed into Acme Retail (account 4043630)` — with an additive `accountName` in `--json`, and `uninstall`'s corporate sub-account picker asks "uninstall from" instead of "install into". Their interactive app picker now offers **only UI apps** — an OAuth app in the list was a choice whose only outcome was that refusal one step later — and errors with a pointer at `brevo app create` when there is no UI app to offer.
14
+
15
+ **`brevo app upload` diffs the `ui_app` block, and warns before it changes an installed app.** The summary used to print the block's desired state with a bare `(changed)` marker; it now diffs it against the server placement by placement — every changed value as `before → after`, added placements tagged `(new)`, dropped ones trailing `(removed)`, matched by slot slug so a reordered `surface_point_list` is not a change. For a UI app the command then warns that the app may already be installed in Brevo accounts (the configuration is shared by every install, and an upload is live there immediately, with no re-install) and asks _"Proceed with upload and update every account this app is installed in?"_. `--yes` skips the question but still prints the warning; `--json` prints neither and stays a single parseable document.
16
+
17
+ **`brevo app install` shows what it is about to install.** Before the confirmation it prints the app's configuration **as stored on the server** — app ID, name, `version`, `extension_type` and every placement — because that snapshot, not the local `app-config.json`, is what the account will render. `--json` gains additive `version` and `ui_app` keys carrying the same facts. When the app came from a linked project whose `ui_app` block has drifted from the stored one, the command says so and points at `brevo app upload`, then installs anyway (exit `0`) — installing an already-uploaded configuration is legitimate, so this is a notice rather than a refusal.
18
+
19
+ **`brevo app credentials` refuses a UI app.** A UI app has no OAuth credentials, so the command now exits `1` with a typed message pointing at `brevo app list`, instead of printing an empty credential form (blank client ID, "(none)" scopes and URLs) and caching the emptiness locally.
20
+
21
+ **`brevo app delete` warns before the confirm** that deletion also removes the app from every account where it is installed or published, and that installs and credentials cannot be recovered. `--force` still deletes without prompting, but now prints the same consequence line so a scripted delete leaves a record; it is kept out of `--json` output, which stays parseable JSON only.
22
+
23
+ **A `502 Bad Gateway` is now retried only for idempotent methods.** The automatic retry replays GET, PUT and DELETE; POST and PATCH surface the 502 as an error instead. A 502 comes from a gateway, so the origin may already have processed the request — blindly replaying a create could duplicate the resource (observed: `brevo app create` producing two identical apps). If it happens on `app create`, check `brevo app list` before retrying, since the app may already exist.
24
+
25
+ **The `app install` / `app uninstall` app picker is refused when it cannot be drawn.** Omitting `--app-id` outside a linked project under `--json` (or off a TTY) now exits `1` naming the `--app-id` form, instead of rendering the picker's choice list into what a script is parsing and then aborting on a raw readline error. Matches the guard `app delete`, `app credentials` and `app withdraw` already carry.
26
+
27
+ **A scaffolded UI app's project docs now describe a UI app.** `CLAUDE.md`, `AGENTS.md` and `README.md` branch on the app type, so a UI-app project documents the `ui_app` block, the per-entry placement fields and the `brevo app upload` → `brevo app install` flow. Previously all three were written from the OAuth templates regardless of type, which told the partner to `yarn --cwd src/oauth install` and `brevo app start oauth` against a `src/oauth/` directory a UI app never receives. OAuth projects get byte-identical files to before.
28
+
29
+ Smaller changes: `app create`'s placement prompt is labelled from the registry's own names; the UI integration-type prompt offers Link only until iframe authoring is ready (a hand-authored `iframeExtension` block still uploads); `--help` examples for `--app-id` show a UUID instead of `42`; a `surface_point_list` entry missing its `surface_point_name` key is reported as a missing key — with a rename hint when the pre-rename `surface_point` spelling is present — instead of as a blank slot name.
30
+
3
31
  ## 2.1.0
4
32
 
5
33
  ### Minor Changes
package/README.md CHANGED
@@ -79,7 +79,7 @@ Or step by step:
79
79
  ```bash
80
80
  brevo app list
81
81
  brevo app create --name "My App" --distribution private
82
- brevo app scaffold --app-id 42
82
+ brevo app scaffold --app-id 3f8c1a2e-5b47-4d9c-8e10-6a2b7d4f0c93
83
83
  brevo app start oauth --port 3000
84
84
  ```
85
85
 
@@ -93,19 +93,42 @@ Run `brevo --help` or `brevo <command> --help` for full command and option lists
93
93
  | `brevo logout` | Clear stored credentials (`--force` to skip confirmation) |
94
94
  | `brevo whoami` | Show the authenticated user |
95
95
  | `brevo app init` | Guided setup — login, create app, and scaffold in one go |
96
- | `brevo app create` | Create an OAuth app (`--name`, `--distribution private`, repeatable `--redirect-uri`, `--logo-uri`) |
97
- | `brevo app list` | List apps in your account |
96
+ | `brevo app create` | Create an app — an OAuth app (`--name`, `--distribution private`, repeatable `--redirect-uri`, `--logo-uri`), or a UI app via the interactive prompts (there is no `--type` flag; non-interactive runs always create an OAuth app) |
97
+ | `brevo app list` | List apps in your account (each row names its type) |
98
98
  | `brevo app credentials` | Show client ID and secret (`--app-id`, `--reveal-secret`) |
99
- | `brevo app upload` | Push `app-config.json` to Brevo after showing a local-vs-server diff (`--yes`) |
99
+ | `brevo app upload` | Push `app-config.json` to Brevo after showing a local-vs-server diff — field by field, including every `ui_app` placement (`--yes`) |
100
100
  | `brevo app delete` | Delete an app (`--app-id`, `--force`) |
101
101
  | `brevo app scaffold` | Add a feature to the app in the current directory, or set an empty directory up for an app you already have — picked interactively, or named with `--app-id` (`--overwrite`, `--json`) |
102
102
  | `brevo app start` | Run a scaffolded feature locally (e.g. `brevo app start oauth --port 3000`) |
103
+ | `brevo app install` | Install a UI app into a Brevo account, after showing the configuration and version it will install (`[account-id]` optional — a regular account installs into itself; a corporate account is prompted to pick a sub-account, so pass the ID explicitly in scripts; `--app-id`, `--force`) |
104
+ | `brevo app uninstall` | Uninstall a UI app from a Brevo account (same arguments as `install`) |
103
105
  | `brevo app available-scopes` | List the OAuth scopes the IdP supports (`--web` opens the catalog in a browser) |
104
106
 
105
107
  Most commands require a successful `brevo login` first, except authentication/help flows (`brevo login`, `brevo logout`, `brevo app init`, `--help`). Every command accepts `--json` for machine-readable output.
106
108
 
107
109
  The table above is the complete command surface of a published release. Features that aren't live on the Brevo platform yet aren't built into the package — `brevo --help` always lists everything the binary can do, so there is nothing hidden behind a flag or an environment variable.
108
110
 
111
+ ### Uploading a UI app that is already installed
112
+
113
+ A UI app's `ui_app` block is what every account it is installed in renders, and there is no
114
+ separate publish step — an upload is live in those accounts as soon as it succeeds. So the two
115
+ commands show you what you are about to change:
116
+
117
+ - **`brevo app upload`** diffs the block against the server placement by placement, printing
118
+ each changed value as `before → after` and tagging placements as `(new)` / `(removed)`. It
119
+ then warns that the app may already be installed in Brevo accounts and asks for confirmation
120
+ naming that consequence. `--yes` skips the question, not the warning; `--json` prints neither
121
+ and stays a single parseable document.
122
+ - **`brevo app install`** prints the configuration it is about to install — **as stored on the
123
+ server**, since that is what the install makes visible — with the app's version, extension
124
+ type and every placement, before asking to confirm. If the `app-config.json` in the current
125
+ directory has drifted from it, the command says so and points at `brevo app upload`; the
126
+ install still proceeds, because the stored configuration is a legitimate thing to install.
127
+ Under `--json` the same information comes back as `version` and `ui_app`.
128
+
129
+ To change what an installed app renders: edit `app-config.json`, run `brevo app upload`, and the
130
+ accounts it is installed in pick the change up — no re-install needed.
131
+
109
132
  ### Browser login
110
133
 
111
134
  `brevo login` defaults to a browser-based sign-in. The CLI starts a temporary loopback server, opens your browser to the Brevo CLI login service, and stores the returned tokens in `~/.brevo/credentials.json`. Access tokens refresh automatically on expiry.
@@ -62,10 +62,11 @@ Don't fall back to raw HTTP against `api.brevo.com` — the `brevo` binary is th
62
62
 
63
63
  ## When to use it
64
64
 
65
- - Create, list, upload changes to, or delete Brevo OAuth apps
65
+ - Create, list, upload changes to, or delete Brevo apps — OAuth apps, or UI apps (action links rendering inside Brevo CRM records)
66
66
  - Scaffold a starter OAuth integration
67
67
  - Run a local OAuth test server (`brevo app start oauth`)
68
68
  - Inspect or rotate app credentials
69
+ - Install or uninstall a UI app in a Brevo account (`brevo app install` / `brevo app uninstall`)
69
70
 
70
71
  ## Common commands
71
72
 
@@ -76,12 +77,14 @@ Don't fall back to raw HTTP against `api.brevo.com` — the `brevo` binary is th
76
77
  | `brevo whoami` | Show the authenticated account (`--json`) |
77
78
  | `brevo app init` | Guided setup (login, create, scaffold) |
78
79
  | `brevo app list` | List apps (`--json`). Each row names its type. |
79
- | `brevo app create` | Create an app (`--name`, `--distribution private`, `--redirect-uri`, `--logo-uri`, `--json`). **Pass `--distribution private`** — run `brevo app create --help` for the values this build accepts and don't pass one it doesn't list. Defaults to scopes `contacts:read`, `contacts:write`, `crm:read`, `crm:write`. Interactively it asks for the name before the OAuth prompts. **Errors immediately if `app-config.json` already exists in the working directory** — move elsewhere or use `brevo app scaffold` there instead. Otherwise resolves (creates/`cd`s into) a target directory, creates the app, and writes the basic project structure (`app-config.json` + `.gitignore`/`AGENTS.md`/`CLAUDE.md`/`README.md`). It scaffolds a feature (OAuth test server) only when the interactive confirm (*"Scaffold the Test OAuth App?"*, default yes) is answered yes; non-interactive runs (`--json` or piped) stay base-only — add the feature afterward with `brevo app scaffold`. |
80
- | `brevo app upload` | Push `app-config.json` to Brevo (`--yes`, `--json`). No edit flags — change name/redirect URLs/scopes/logo/version by editing `app-config.json` directly, then run `upload`. **`distribution_type` is immutable** — set at `app create` time; if the local value differs from the server, `upload` errors before pushing (restore the local value, or create a new app). Always fetches the remote app first and shows a local-vs-server diff (even under `--yes`/`--json`); exits 0 with no network push if nothing differs. |
81
- | `brevo app credentials` | Show client ID / secret (`--app-id`, `--reveal-secret`, `--json`). **`--app-id` is required when scripting** — see the picker note below. Also backfills a missing top-level `version` / `distribution_type` into cwd's `app-config.json` when its `appId` matches (fill-only-when-missing, silent). |
80
+ | `brevo app create` | Create an app (`--name`, `--distribution private`, `--redirect-uri`, `--logo-uri`, `--json`). **Pass `--distribution private`** — run `brevo app create --help` for the values this build accepts and don't pass one it doesn't list. Defaults to scopes `contacts:read`, `contacts:write`, `crm:read`, `crm:write`. Interactively it asks for the name before the OAuth prompts, and asks the **app type** (OAuth vs UI app) as the last question before the flow splits — there is no `--type` flag, so a UI app is only reachable interactively and every non-interactive run creates an OAuth app. **Errors immediately if `app-config.json` already exists in the working directory** — move elsewhere or use `brevo app scaffold` there instead. Otherwise resolves (creates/`cd`s into) a target directory, creates the app, and writes the basic project structure (`app-config.json` + `.gitignore`/`AGENTS.md`/`CLAUDE.md`/`README.md`). It scaffolds a feature (OAuth test server) only when the interactive confirm (*"Scaffold the Test OAuth App?"*, default yes) is answered yes; non-interactive runs (`--json` or piped) stay base-only — add the feature afterward with `brevo app scaffold`. |
81
+ | `brevo app upload` | Push `app-config.json` to Brevo (`--yes`, `--json`). No edit flags — change name/redirect URLs/scopes/logo/version by editing `app-config.json` directly, then run `upload`. **`distribution_type` is immutable** — set at `app create` time; if the local value differs from the server, `upload` errors before pushing (restore the local value, or create a new app). Always fetches the remote app first and shows a local-vs-server diff (even under `--yes`/`--json`); exits 0 with no network push if nothing differs. **For a UI app the diff covers the `ui_app` block placement by placement** — every changed value as `before → after`, added placements tagged `(new)`, dropped ones trailing `(removed)`, matched by slot slug so a reordered `surface_point_list` is not a change — and the command then warns that the app may already be installed in Brevo accounts and asks *"Proceed with upload and update every account this app is installed in?"*. There is no separate publish step: a successful upload changes what every account the app is installed in renders, immediately and with no re-install. `--yes` skips the question but still prints the warning; `--json` prints neither and stays a single parseable document. |
82
+ | `brevo app credentials` | Show client ID / secret (`--app-id`, `--reveal-secret`, `--json`). **`--app-id` is required when scripting** — see the picker note below. **OAuth apps only** — a UI app has no OAuth credentials, so the command refuses it with exit `1` and points at `brevo app list` for the app's type. Also backfills a missing top-level `version` / `distribution_type` into cwd's `app-config.json` when its `appId` matches (fill-only-when-missing, silent). |
82
83
  | `brevo app delete` | Delete an app (`--app-id`, `--force`, `--json`). **`--app-id` is required when scripting** — see the picker note below. |
83
84
  | `brevo app scaffold` | Add a feature to the app in the current directory (`--app-id`, `--overwrite`, `--json`). Requires an `app-config.json` in cwd **unless `--app-id <id>` is passed or you accept its bootstrap offer**; reads the linked app from it. `--app-id` in a directory with no config fetches that app and writes `app-config.json` + the base files first — the only way to get a config for an app that already exists, and the migration path off the removed `brevo app update --app-id`. Interactively, omitting `--app-id` in a config-less directory prints *"No app-config.json in this directory…"*, asks **"Set up a project for an app you already have?"** (default yes) and on yes shows an app picker; answering no exits `0` with the remaining routes on screen. Every interactive bootstrap (picker or `--app-id`) then asks `Output directory:` defaulted to `./<slugified app name>`, creates it and `cd`s into it — answer `.` to stay in the current directory — and the *Next steps* box opens with `cd <dir>`. Under `--json` or off a TTY there is no offer — it errors, listing the three ways out. It refuses, before any network call: if the directory is already linked to a different app — either the one the command ran in or the one it was pointed at (naming the app it is already linked to is a no-op), if the directory is **inside** an existing app project (a nested second config would make a later `brevo app upload` from there push the wrong app). Diffs the local config against the server and, on drift, updates `app-config.json` to match (on consent) before writing the feature files — including when a bootstrap is pointed at a directory that already holds a project, where answering **Merge** to the directory question does *not* skip that refresh. When feature files already exist, prompts Overwrite / Merge / Cancel (default Merge — existing files kept); `--overwrite` forces overwrite and skips the prompt. The scaffolded OAuth flow is the confidential-client flow: the token exchange is authenticated with the `CLIENT_SECRET` written into the generated `.env.local`. |
84
85
  | `brevo app start oauth` | Run the scaffolded OAuth test server (`--port`) |
86
+ | `brevo app install` | Install a **UI app** into a Brevo account (`[account-id]` positional, `--app-id`, `--force`, `--json`). UI apps only — an OAuth app has nothing to install and the CLI refuses with exit `1`. `[account-id]` is optional: omitted, a plain account installs into itself (no prompt, `--json`/CI safe) and a corporate account picks a sub-account interactively — non-interactive corporate runs must pass it. Refused locally until the app has been validated by a `brevo app upload` (the `version` field is the signal). Interactively, omitting `--app-id` outside a linked project opens an app picker that lists **only UI apps**; with no UI app to offer it errors (exit `1`) naming `brevo app create`. That picker needs a terminal: under `--json` or off a TTY, omitting `--app-id` outside a linked project is refused with exit `1`. **Prints the configuration it will install before acting** — app ID, name, `version`, extension type and every placement, read from the server, because the account renders the stored snapshot and not the local `app-config.json`; under `--json` the same facts come back as additive `version` / `ui_app` keys on the result. When the linked project's block has drifted from the stored one it says so and names `brevo app upload`, then installs anyway (a notice, not a refusal — the stored configuration is a legitimate thing to install). |
87
+ | `brevo app uninstall` | Uninstall a UI app from a Brevo account (same arguments and target resolution as `install`). Uninstalling an app that isn't installed is informational, exit `0` — not an error. |
85
88
  | `brevo app available-scopes` | List OAuth scopes supported by the IdP (`--json`, `--web`) |
86
89
  | `brevo skill:cli install` | Install the brevo-cli Claude Code skill (Claude-only; auto-refreshes on every `brevo` run) |
87
90
  | `brevo skill:cli uninstall` | Remove the brevo-cli skill from `~/.claude/skills/` (Claude-only) |
@@ -91,7 +94,7 @@ Run `brevo --help` or `brevo <command> --help` for the full set.
91
94
  ## Conventions
92
95
 
93
96
  - **Every command supports `--json`** — prefer this when parsing output programmatically. It applies to failures too: a failing `--json` run writes a single `{"error": {...}}` document to stdout (see *JSON errors* below) while the human message goes to stderr.
94
- - **`app-config.json` describes an OAuth app.** It carries `auth.scopes` / `auth.redirectUris` alongside the top-level `appId` / `appName` / `logoUri` / `version` / `distribution_type`. Write only the keys documented here — `brevo app upload` validates the whole file and rejects anything it doesn't recognise.
97
+ - **Two app types, one command surface.** `app-config.json` describes either an **OAuth app** a populated `auth` block (`auth.scopes` / `auth.redirectUris`) and no `ui_app` — or a **UI app** — a `ui_app` block and an **empty** `auth: {}` (no callbacks, scopes, or credentials). The presence of `ui_app` is the discriminator; never mix the two in one file. Both types share the top-level `appId` / `appName` / `logoUri` / `version` / `distribution_type` and the same `create`/`upload`/`list`/`delete` commands; only UI apps take `install`/`uninstall`. **A UI app has one stored configuration, shared by every account it is installed in** — `brevo app upload` is therefore how you change what an installed app renders (edit `app-config.json`, upload, done), never uninstall-then-reinstall; both commands show what is about to change for that reason. The `ui_app` block holds `extension_type` at its root (`actionLink`, `iframeExtension`, `legacyComponent` — camelCase only) and a `surface_point_list` of placement entries, each carrying `surface_point_name` (the dot-notation slug from the platform's registry, e.g. `contactDetails.header.menu` — not the `<location>.<place>.<kind>` extension-point name like `contactDetails.headerMenu.action`, which is dotted too but a different string), `label`, optional `more_info`, `redirect_link` (record context arrives as query parameters), optional `context`, optional `size` (e.g. `{ "width": "280px", "height": "160px" }` — each axis a positive-integer `px` length or `1%`–`100%` of the host slot, shrink-only, both axes optional), and — `iframeExtension` only — `modal_iframe_url`. Do **not** write `link_target` or `extension_point_name` into the file: both are wire/server-stamped (`app upload` injects `link_target` itself). Write only the keys documented here — `brevo app upload` validates the whole file and rejects anything it doesn't recognise, including the pre-GA `heading`/`subheading` names (now `label`/`more_info`, per entry). A UI-app project is **configuration only** — no feature to scaffold and no `src/oauth/`, since an action link has no local server (`brevo app scaffold` inside one says so and exits `0`; `brevo app start` does not apply) — and the base docs the scaffold writes (`AGENTS.md` / `CLAUDE.md` / `README.md`) describe whichever type the app is, so a UI app's copies cover the `ui_app` block and the `upload` → `install` flow instead of an OAuth server.
95
98
  - **`brevo app create` refuses to run inside an already-linked directory.** If `app-config.json` exists in cwd, it throws immediately (no confirm, no override) — the error points at moving elsewhere or running `brevo app scaffold` there.
96
99
  - **`brevo app create` resolves its target directory before creating the app**, then writes the **basic project structure only** (`app-config.json` + `.gitignore`/`AGENTS.md`/`CLAUDE.md`/`README.md`) — the OAuth server code is a *feature*, not part of the base. Interactive mode prompts for the target directory (default `./<slugified-app-name>`, `cd`s into it) before the API call, how to handle an existing one (overwrite / merge / choose a different path), and — after the app is created — whether to scaffold a feature (*"Scaffold the Test OAuth App?"*, default **yes**). There is no follow-up "which feature?" question while the CLI ships one: a list of one is not asked, and the confirm names it instead. A second feature would bring the picker back. Non-interactive runs stay base-only: `--json` (and piped, non-TTY) create the app and write the base files but never scaffold a feature — run `brevo app scaffold` afterward for the OAuth code. Under `--json` the same default directory is used and `cd`d into if it doesn't already exist; if it already exists, both directory setup and scaffolding are skipped (the app is still created). The JSON response always includes `directory` (absolute path) alongside the app fields, plus either `scaffolded` (base file count, on success) or `scaffoldSkipped` (a message, when the directory already existed).
97
100
  - **`brevo app scaffold` adds a feature to an already-created project, or sets an empty directory up for an app that already exists.** It **requires** an `app-config.json` in cwd unless `--app-id` is passed or its bootstrap offer is accepted, and only the bootstrap mode ever creates a directory (the feature-add mode always writes into the project it was run in). **`--app-id <id>` bootstraps a project for an app that already exists**: it fetches the app, writes `app-config.json` + the base files, and then continues into the feature flow. That is the only command that produces a config for an existing app (`app create` creates a new one, `app upload` only reads the linked project), which makes it the migration path off the removed `brevo app update --app-id`. **Interactively, `--app-id` is optional**: in a config-less directory the command explains there is no app here, asks *"Set up a project for an app you already have?"* (default **yes**), and on yes runs the same app picker `app delete` uses — because a user who has lost their project folder has the app but not necessarily its ID. Declining is a normal outcome that exits `0` after printing the remaining routes; the offer is skipped entirely under `--json` or off a TTY, where the no-config error (naming all three ways out: `cd` into a project, `--app-id`, `brevo app create`) is raised instead, so scripts behave exactly as before. **An interactive bootstrap also asks where to put the project** — `Output directory:`, defaulted to `./<slugified app name>`, the same prompt (and the same overwrite / merge / choose-a-different-path follow-up on an existing directory) `app create` uses; it creates the directory, `cd`s the CLI process into it, writes and reports the project, then asks *"Scaffold the Test OAuth App?"* (default yes; declining leaves the project and exits `0`), and opens *Next steps* with `cd <dir>` since the user's shell stayed behind. Answering `.` keeps the current directory and drops that step. This too is interactive-only: under `--json` or off a TTY the files go into the current directory as they always have, which is what makes `scaffold --app-id` safe to script. In bootstrap mode the config is written from the server's copy of the app, since there is nothing local to read it from. Bootstrapping is refused, before any network call or write, in two cases: a directory already linked to a **different** app (passing the app it is already linked to changes nothing), and a directory **inside** an existing app project — `readProjectConfig` reads cwd only and never walks up, so without that check a stray `cd` would nest a second `app-config.json` inside the first and a later `app upload` from there would push the wrong app silently. The different-app check applies to the answer to `Output directory:` as well as to cwd, and there it is the only thing standing between you and a project whose `app-config.json` and `src/oauth/.env.local` name two different apps. **A target directory that already holds a project for the same app makes the bootstrap a refresh**: its config is diffed against the server and rewritten only on consent, and the directory question's **Merge** answer does not suppress that. The two answers address different things — Merge means "don't clobber my own files" and is implemented by skipping any path that already exists, which `app-config.json` always does here, so letting it govern the base write meant the command fetched the app, discarded every field, wrote nothing, and still printed its success box. No drift leaves `app-config.json` as it is with a one-line notice; the feature is still offered either way. It otherwise reads the linked app id from that config (no picker — the picker is only for the config-less bootstrap), diffs the local config against the server, and if fields drifted it shows them and asks consent to update `app-config.json` (and the other base files) to match before writing the feature files. When any feature file already exists it prompts **Overwrite / Merge / Cancel** (default **Merge** — existing, e.g. hand-edited, files are kept and only missing files added; Cancel aborts without writing). The `--overwrite` flag forces a full overwrite of feature files and skips that prompt (works interactively and under `--json`). **Under `--json` it never prompts**: a config diff comes back as `{ "cancelled": true, "reason": "...", "diffs": [...] }`; otherwise it writes the feature (merging existing files unless `--overwrite` is passed) and returns `{ "scaffolded": <n>, "directory": "..." }`.
@@ -100,7 +103,7 @@ Run `brevo --help` or `brevo <command> --help` for the full set.
100
103
  - **There is no `brevo app update`.** It was removed and replaced by `brevo app upload`, with no shim and no flag-for-flag equivalent — change an app's name, redirect URLs, scopes or logo by editing `app-config.json`, then run `brevo app upload`. That is what to replace it with wherever you find it: a user's script, a CI job, a README, or your own recollection. Invoking it — with any of the old flags (`--name`, `--redirect-uri`, `--scope`, `--logo-uri`, `--app-id`), with `--help`, or as `brevo app help update` — prints a message naming `upload` and exits `1` **without uploading anything**, so a `1` from `brevo app update` means the command is gone, not that an upload failed. It is absent from every help screen, and needs no login to reach.
101
104
  - **Credentials** live at `~/.brevo/credentials.json`. Never commit this file or any `.env.local`.
102
105
  - **Non-interactive auth:** `BREVO_API_KEY=xkeysib-... brevo login`. The legacy `--api-key` flag was removed because it leaks into shell history.
103
- - **Skip prompts:** `--force` for `app delete` and `logout`; `--yes` for `app upload`.
106
+ - **Skip prompts:** `--force` for `app delete`, `app install`, `app uninstall` and `logout`; `--yes` for `app upload`. `app delete --force` still prints the install-loss warning line (kept out of `--json` output, which stays parseable JSON only). `app upload --yes` behaves the same way for a UI app: it skips the confirmation but still prints the line warning that the app may already be installed.
104
107
  - **Forced update:** when the installed CLI is a full **major** version behind the latest npm release, every command except `--help`/`--version` prints a blocking update banner to stderr and exits `1` without running. Update with `npm install -g @getbrevo/cli` (or `yarn global add`). The gate honors the same opt-outs as the soft update notice (`BREVO_NO_UPDATE_NOTIFIER=1`, `--no-update-notifier`, CI, non-TTY), so it never fires in those contexts.
105
108
  - **Update notice wording:** the update/force-update banners take their first line from the app-store service (`GET /cli/info`). It is called directly, not through the v3 API gateway, and needs no API key — so it works while logged out or with expired credentials. It runs once per invocation, **before** the command, and the response is cached at `~/.brevo/cli-info-cache.json` for **15 minutes**, keyed to the installed `cliVersion` — so reworded text or a new block reaches the CLI within minutes rather than after the old 12h npm-style TTL. Whether an update banner appears is still decided from the npm registry, and if the call fails the banner still appears with local wording (a failed call never overwrites a good cache entry).
106
109
  - **Soft update notice on failures:** the non-blocking update banner also prints after a command *fails*, not just after it succeeds — so stderr may hold the error message followed by the update box. Exit codes are unchanged (still the command's own), the box never appears twice in one run, and a Ctrl-C abort skips it. Parse stderr accordingly, or set `BREVO_NO_UPDATE_NOTIFIER=1` / pass `--no-update-notifier`.
@@ -53,12 +53,15 @@ Don't fall back to raw HTTP against `api.brevo.com` — the `brevo` binary is th
53
53
  - "Who am I logged in as?" → `brevo whoami --json`
54
54
  - "Show / pick an app" → `brevo app list --json`
55
55
  - "Create an app" → `brevo app create --name "<name>" --distribution private --redirect-uri <url> --json` (add `--logo-uri <https://…>` to set the app logo at creation time; new apps default to scopes `contacts:read`, `contacts:write`, `crm:read`, `crm:write`). **Use `--distribution private`** — check `brevo app create --help` for the values your account accepts, and don't pass one it doesn't list. **Fails immediately if run from a directory that already has `app-config.json`** — `cd` elsewhere first, or use `brevo app scaffold` in that directory instead. Otherwise resolves (creates/`cd`s into) its target directory, creates the app, and writes the **basic project structure** (`app-config.json` + `.gitignore`/`AGENTS.md`/`CLAUDE.md`/`README.md`). It scaffolds a feature (the OAuth test server) **only** when the interactive confirm (*"Scaffold the Test OAuth App?"*, default yes) is answered yes — there is no separate "which feature?" question while the CLI ships one; **non-interactive runs (`--json` or piped) stay base-only** — run `brevo app scaffold` afterward to add the OAuth code. Under `--json`, the response's `directory` field is where it landed and `scaffolded` is the base file count; check for `scaffoldSkipped` instead of `scaffolded` if that directory already existed (both directory setup and scaffolding are skipped together in that case, but the app is still created).
56
- - "Update app metadata" → edit the relevant field(s) in `app-config.json` (`appName`, `auth.redirectUris`, `auth.scopes`, `logoUri`, `version`) (older projects may still say `auth.redirectUrls` — the CLI reads it and migrates the file to `redirectUris` on its next write), then run `brevo app upload --json` (no `--app-id`/`--name`/`--redirect-uri`/`--scope`/`--logo-uri` flags exist `upload` always pushes the whole file, resolved only from cwd's `app-config.json`). **`distribution_type` is immutable** — it's set at `app create` time and cannot be changed via `upload`; if the local value differs from the server, `upload` errors and tells you to restore it (create a new app to get a different distribution). **There is no `brevo app update`** it was removed, with no shim and no flag-for-flag equivalent; if you find it in a user's script, CI job, README, or your own recollection, replace it with the edit-then-`upload` flow above. Invoking it with any of the old flags, with `--help`, or as `brevo app help update` prints a message naming `brevo app upload` and exits `1` **without uploading anything**, so a `1` from `brevo app update` means the command is gone, not that an upload failed.
57
- - "Get client credentials" → `brevo app credentials --app-id <id> --json` (add `--reveal-secret` to print the secret). **`--app-id` is not optional here** without it the command wants an interactive app picker, and under `--json`/off a TTY it refuses with exit `1` rather than prompting.
56
+ - "Create a UI app" (an action link that renders inside Brevo CRM records) run `brevo app create` **interactively** and pick *UI app* at the *"What type of app are you building?"* prompt. There is **no `--type` flag and no per-field flags** a UI app can only be authored from an interactive terminal, and every non-interactive run (`--json` or piped stdin) creates an OAuth app. The flow then asks for the integration type, one record page, one placement on it, a label, optional supporting text, and the destination URL it authors exactly **one** placement; add more by hand as further `surface_point_list` entries in `app-config.json` (each with its own `label` and `redirect_link`), then `brevo app upload`.
57
+ - "Install a UI app into an account" → `brevo app install [account-id] --app-id <id> --force --json`. **UI apps only** — an OAuth app has nothing to install (it becomes usable when a user authorizes it) and the CLI refuses with exit `1`. The `[account-id]` positional is optional: omitted, a plain account installs into itself (no prompt, so `--json`/CI works) and a corporate account picks a sub-account interactively (non-interactive corporate runs must pass it explicitly). The app must have been validated with `brevo app upload` first — installing before an upload is refused locally. Interactively, omitting `--app-id` outside a linked project opens an app picker that lists **only UI apps**; with no UI app to offer, the command errors (exit `1`) naming `brevo app create`. That picker needs a terminal: under `--json` or off a TTY, omitting `--app-id` outside a linked project is refused with exit `1` so always pass `--app-id` when scripting from an unlinked directory. Before it acts, `install` prints the configuration it will install **as stored on the server** — app ID, name, `version`, extension type and every placement — because that is what the account will render, not whatever the local `app-config.json` now says; under `--json` the same facts come back as `version` and `ui_app` on the result. If the linked project's `ui_app` block has drifted from the stored one, it warns and names `brevo app upload`, then installs anyway (exit `0`) — the stored configuration is a legitimate thing to install, so this is a notice, not a refusal.
58
+ - "Uninstall a UI app from an account" → `brevo app uninstall [account-id] --app-id <id> --force --json`. Same target resolution as `install`. Uninstalling an app that isn't installed is **informational, exit `0`** — not an error.
59
+ - "Update app metadata" → edit the relevant field(s) in `app-config.json` (`appName`, `auth.redirectUris`, `auth.scopes`, `logoUri`, `version`) (older projects may still say `auth.redirectUrls` — the CLI reads it and migrates the file to `redirectUris` on its next write), then run `brevo app upload --json` (no `--app-id`/`--name`/`--redirect-uri`/`--scope`/`--logo-uri` flags exist — `upload` always pushes the whole file, resolved only from cwd's `app-config.json`). **`distribution_type` is immutable** — it's set at `app create` time and cannot be changed via `upload`; if the local value differs from the server, `upload` errors and tells you to restore it (create a new app to get a different distribution). For a **UI app**, `upload` is also the way to change what an installed app renders — there is no re-install and no publish step: the diff prints every `ui_app` placement field by field (`before → after`, plus `(new)` / `(removed)` placements), then warns that the app may already be installed in Brevo accounts and asks *"Proceed with upload and update every account this app is installed in?"*. `--yes` skips the question but still prints the warning; `--json` prints neither and stays a single parseable document. **There is no `brevo app update`** — it was removed, with no shim and no flag-for-flag equivalent; if you find it in a user's script, CI job, README, or your own recollection, replace it with the edit-then-`upload` flow above. Invoking it — with any of the old flags, with `--help`, or as `brevo app help update` — prints a message naming `brevo app upload` and exits `1` **without uploading anything**, so a `1` from `brevo app update` means the command is gone, not that an upload failed.
60
+ - "Get client credentials" → `brevo app credentials --app-id <id> --json` (add `--reveal-secret` to print the secret). **`--app-id` is not optional here** — without it the command wants an interactive app picker, and under `--json`/off a TTY it refuses with exit `1` rather than prompting. **OAuth apps only** — a UI app has no OAuth credentials (no client ID, secret, scopes, or callbacks), so the command refuses it with exit `1` and points at `brevo app list` for the app's type.
58
61
  - "Set up a project for an app that already exists" → `brevo app scaffold --app-id <id>` (`brevo app list` gives the IDs). Fetches the app and writes `app-config.json` + the base files, then adds a feature as below. Interactively it asks which directory to write into first (default `./<slugified app name>`); under `--json`/off a TTY it uses the current one. **This is the only way to get an `app-config.json` for an existing app** — `brevo app create` makes a *new* app, and `brevo app upload` only ever reads the project in the current directory. It refuses if the directory is already linked to a *different* app; pointing it at the app the directory is already linked to is a no-op. **Interactively you can omit `--app-id`**: plain `brevo app scaffold` in a directory with no `app-config.json` says so, asks *"Set up a project for an app you already have?"* (default yes), and on yes lists the account's apps so you can pick one — no need to look the ID up first. **Every interactive bootstrap (picker or `--app-id`) then asks `Output directory:`, defaulted to `./<the app's name slugified>`** — the same question `brevo app create` asks — creates that directory and `cd`s into it, so the files don't land in whatever folder you happened to be standing in; answer `.` to use the current directory instead. It then writes the project (`app-config.json` + the base files), shows what it wrote, and asks *"Scaffold the Test OAuth App?"* (default yes) — declining is normal and leaves the project in place, exit `0`. The *Next steps* box opens with `cd <dir>`, because the CLI can only move its own process, never your shell. **Under `--json` or off a TTY there is no directory question and the files go into the current directory**, so scripted `brevo app scaffold --app-id <id>` runs are unchanged — `mkdir` and `cd` yourself first if you want them somewhere specific. Answering **no** is a normal outcome, not an error: it exits `0` after printing the remaining routes (`brevo app create` here, or `cd` into an existing project). **Always pass `--app-id` when scripting** — the offer needs a terminal, so under `--json` or off a TTY the command errors instead of prompting. **If the directory you point it at already holds a project, the bootstrap is a refresh, not a fresh write**: the config found there is diffed against the server and rewritten only on consent (*"…will update app-config.json to match the server. Continue?"*, default yes) — answering **Merge** at the directory question does not suppress that, because merging keeps the file that exists and so would skip the very file a bootstrap is for. No drift means `app-config.json` is left alone with a one-line notice, and the feature still gets added. A directory holding a project for a **different** app is refused outright, naming both apps. Two refusals apply to both forms, before any write: that different-app case, and the directory must not be **inside** an existing project (a nested second `app-config.json` would make a later `brevo app upload` push the wrong app).
59
62
  - "Add a feature (e.g. the OAuth test server) to an existing project" → `brevo app scaffold` (run **inside** the project directory; it reads the linked app from `app-config.json`, so `--app-id` is only needed to bootstrap a directory that has none). Not needed right after `app create` if you already accepted the feature prompt there. If feature files already exist it prompts Overwrite / Merge / Cancel (default Merge); pass `--overwrite` to force a full overwrite without prompting. **The scaffolded OAuth flow is the confidential-client flow:** `/auth/callback` authenticates the token exchange with the `CLIENT_SECRET` written into the scaffolded `.env.local`.
60
63
  - "Run the OAuth test server" → `brevo app start oauth --port 3009` (must be inside the scaffolded directory)
61
- - "Delete an app" → `brevo app delete --app-id <id> --force`. **`--app-id` is not optional here either** — omitting it means an interactive picker, which under `--json`/off a TTY is refused with exit `1`. Never script a delete without naming the app.
64
+ - "Delete an app" → `brevo app delete --app-id <id> --force`. **`--app-id` is not optional here either** — omitting it means an interactive picker, which under `--json`/off a TTY is refused with exit `1`. Never script a delete without naming the app. `--force` skips the prompt but still prints the install-loss warning line; under `--json` stdout stays JSON only.
62
65
  - "List supported OAuth scopes" → `brevo app available-scopes --json`
63
66
  - "Sign out" → `brevo logout --force`
64
67
 
@@ -70,6 +73,31 @@ Don't fall back to raw HTTP against `api.brevo.com` — the `brevo` binary is th
70
73
  4. **`brevo app create` refuses to run inside an already-linked directory** (`app-config.json` present) — `cd` elsewhere or use `brevo app scaffold` there instead. **`brevo app scaffold` requires an `app-config.json` in the current directory unless you pass `--app-id` or answer its bootstrap offer** (it adds a feature to an already-created project); with none present and no `--app-id` it offers to set the directory up for an existing app when interactive, and otherwise errors, listing the three ways out (`cd` into a project, `--app-id` to set this directory up for an app you already have, or `brevo app create` for a new one). With `--app-id <id>` in a directory that has no config it fetches that app, asks (interactively only) which directory to write into — default `./<slugified app name>`, `.` to stay put — writes `app-config.json` + the base files there, and continues; that is the migration path off the removed `brevo app update --app-id`, and off a TTY or under `--json` it writes into the current directory with no prompt. If the directory it is told to write into already holds a project for the **same** app, it diffs that config against the server and rewrites it only on consent — the directory prompt's **Merge** answer does not silently skip the refresh, and no drift leaves `app-config.json` untouched with a notice. It refuses (before any network call) if the directory is already linked to a *different* app — whether that is the directory the command ran in or the one it was pointed at — or if the directory is **inside** an existing app project (a nested second `app-config.json` would make a later `brevo app upload` from there push the wrong app — `cd` to the project root or outside it). It reads the linked app from that config, diffs the config against the server, and if fields drifted it tells you and (on consent) rewrites `app-config.json` to match before writing the feature files. When any feature file already exists it prompts **Overwrite / Merge / Cancel** (default **Merge** — existing, e.g. hand-edited, files are kept; Cancel aborts). Pass `--overwrite` to force a full overwrite and skip that prompt (works interactively and under `--json`). **Under `--json` it never prompts**: a config diff comes back as `{ "cancelled": true, "reason": "...", "diffs": [...] }`; otherwise it scaffolds the feature (merging existing files unless `--overwrite` is passed) and returns `{ "scaffolded": <n>, "directory": "..." }`.
71
74
  5. **Prefer flag-driven over interactive** in agent contexts: `--name`, `--app-id`, `--force`, `--yes` so the command doesn't block on prompts.
72
75
  6. **Write only the `app-config.json` keys this file documents.** `brevo app upload` validates the whole file and rejects keys it doesn't recognise, so an invented block fails at upload rather than doing anything useful.
76
+ 7. **Never mix the two app types in one `app-config.json`.** The presence of the `ui_app` block is the app-type discriminator: an OAuth app has a populated `auth` block and no `ui_app`; a UI app has a `ui_app` block and an **empty** `auth: {}` (it has no OAuth callback, scopes, or credentials). See *UI apps* below for the block's shape.
77
+
78
+ ## UI apps
79
+
80
+ A **UI app** is an action link that renders inside Brevo CRM record pages (contact, company, deal). It is the second app type next to OAuth apps — created by the same `brevo app create` (interactive only, see the decision tree), pushed by the same `brevo app upload`, and made visible in an account with `brevo app install`.
81
+
82
+ A UI-app project is **configuration only**: there is no feature to scaffold and no `src/oauth/`, because an action link has no local server to run — `brevo app scaffold` inside one says so and exits `0`, and `brevo app start` does not apply. The base docs the scaffold writes (`AGENTS.md` / `CLAUDE.md` / `README.md`) describe whichever type the app is, so a UI app's copies document the `ui_app` block and the `upload` → `install` flow rather than an OAuth server.
83
+
84
+ The `ui_app` block in `app-config.json`:
85
+
86
+ - `extension_type` (root) — one of `actionLink`, `iframeExtension`, `legacyComponent` (camelCase; snake_case spellings like `action_link` are rejected).
87
+ - `surface_point_list` — one entry per placement. Each entry carries:
88
+ - `surface_point_name` — the placement's dot-notation slug from the platform's registry (e.g. `contactDetails.header.menu`). The valid names come from the registry — the CLI prompts from it at create time, and `brevo app upload` validates against it (an unregistered name is a 400 naming the offender). Do **not** author the extension-point name (`contactDetails.headerMenu.action`, the `<location>.<place>.<kind>` grammar) — it is dotted too and looks right because specs quote it, but it is a different string and upload rejects it.
89
+ - `label` — the menu entry's text (and the CTA button on a card). Required.
90
+ - `more_info` — supporting text under the menu entry / a card's description. Optional.
91
+ - `redirect_link` — the destination URL that entry opens; record context arrives as **query parameters** (the path is never templated).
92
+ - `context` — optional narrowing of the record fields passed along; it can only narrow what the platform allows for that slot.
93
+ - `size` — optional card size for the widget card this placement renders, e.g. `{ "width": "280px", "height": "160px" }`. Each axis is a CSS length string — a positive integer with an explicit `px` unit, or `1%`–`100%` of the host slot's box (shrink-only; >100% is rejected). Both axes are optional; an omitted axis (or the whole key) stays on the host slot's default.
94
+ - `modal_iframe_url` — `iframeExtension` entries only; rejected on an `actionLink`.
95
+ - Do **not** write `link_target` or `extension_point_name` anywhere in the file — both are wire/server-stamped values (`app upload` injects `link_target: "_blank"` itself) and the CLI strips them from server echoes.
96
+ - The old `heading`/`subheading` names are rejected with a migration hint — they are `label`/`more_info` now, and they live **per entry**, not at the `ui_app` root.
97
+
98
+ Install semantics worth knowing: only UI apps install into an account; the app must have a `version` (written by a successful `brevo app upload`) before `install` will proceed; `uninstall` of a not-installed app is informational (exit `0`).
99
+
100
+ **An installed UI app tracks the server's configuration, not the account's copy of it.** There is one stored snapshot per app, so `brevo app upload` changes what every account it is installed in renders, immediately and with no re-install — which is why `upload` warns and asks before pushing a UI app, and why `install` shows the stored configuration it is about to make visible. Tell a user to edit `app-config.json` and run `brevo app upload`; never tell them to uninstall and re-install to pick up a change.
73
101
 
74
102
  ## Locating the linked app
75
103
 
@@ -1,4 +1,5 @@
1
1
  # {{APP_NAME}} — Agent Context
2
+ {{#if oauth}}
2
3
 
3
4
  This project is a Brevo OAuth 2.0 integration scaffolded with the Brevo CLI.
4
5
 
@@ -60,3 +61,59 @@ brevo app start oauth # port is read from app-config.json; override with
60
61
  - [Brevo API Documentation](https://developers.brevo.com)
61
62
  - [Brevo CLI reference](https://developers.brevo.com/docs/cli-reference) — full command and option list
62
63
  - [Brevo CLI repo](https://github.com/getbrevo/brevo-cli)
64
+ {{/if}}
65
+ {{#if ui_app}}
66
+
67
+ This project is a Brevo **UI app** scaffolded with the Brevo CLI — an action link that renders inside Brevo CRM records.
68
+
69
+ > **Nothing runs locally.** A UI app has no OAuth callback and no scaffolded server code. Brevo opens a URL you already host; this project is the app's configuration.
70
+
71
+ ## What this does
72
+ Declares where the app appears inside Brevo's CRM (which record page, which slot) and where Brevo sends the user when they click it. The whole app is the `ui_app` block in `app-config.json`.
73
+
74
+ ## Project structure
75
+ ```
76
+ .
77
+ ├── app-config.json App metadata + the `ui_app` block (the whole app)
78
+ ├── .gitignore Ignores .env.local and build artifacts
79
+ ├── AGENTS.md AI agent guidance (this file)
80
+ ├── CLAUDE.md Claude Code guidance
81
+ └── README.md Human-facing setup guide
82
+ ```
83
+
84
+ ## The `ui_app` contract
85
+ - The presence of `ui_app` is the app-type discriminator. There is no `appType` key.
86
+ - `auth` must be exactly `{}` — a UI app uses no OAuth, and `brevo app upload` refuses one carrying `scopes` or `redirectUris`.
87
+ - `extension_type` is the only field at the `ui_app` root: `actionLink` (opens `redirect_link` in a new tab) or `iframeExtension` (embeds `modal_iframe_url` in a modal). camelCase only — `action_link` is rejected, not aliased.
88
+ - Everything else lives **per entry** in `surface_point_list`, so each placement carries its own text and destination:
89
+ - `surface_point_name` (required) — the registry slug in dot notation, e.g. `contactDetails.header.menu`. Not the dotted extension-point name a spec quotes (`contactDetails.headerMenu.action`).
90
+ - `label` (required, max 48) — the menu entry's text on an `.action` slot; a card's CTA button on a `.widget` slot.
91
+ - `more_info` (optional, max 255) — the menu entry's second line; a card's description.
92
+ - `redirect_link` (`actionLink`) / `modal_iframe_url` (`iframeExtension`) — `https://`, or `http://localhost`. Each is rejected on the other extension type.
93
+ - `context` (optional) — narrows the record context Brevo sends; omitted, the slot's full allow-list applies.
94
+ - `size` (optional) — `{ "width": "280px", "height": "160px" }`; a positive integer with an explicit `px` or `%` unit, `%` capped at 100.
95
+ - A card's title is the app name. There is no field for it.
96
+ - Never author `link_target`, `version` or `extension_point_name` — the platform owns them, `brevo app upload` injects or strips them, and a copy in the file reads as permanent drift in the upload diff.
97
+ - Root-level `label` / `more_info` / `redirect_link` / `modal_iframe_url` / `context` / `link_target`, and the older `heading` / `subheading`, are all refused by name with a migration hint.
98
+
99
+ ## Record context
100
+ Brevo appends the record context to the destination URL as **query parameters** — the path is never templated. Typical fields: `recordId`, `recordType`, `accountId`, `userId`, `clientId`, `extId`, `locale`. They arrive via the browser, so treat them as untrusted input.
101
+
102
+ ## Placements
103
+ `brevo app create` authors exactly one placement. Add more by hand as extra `surface_point_list` entries — each with its own `label` and destination — then `brevo app upload`.
104
+
105
+ Slot names are **not** validated locally: the platform's extension-point registry is the only authority and the CLI keeps no copy that could lag it. `app upload` sends the block and the platform rejects the upload naming any unregistered slot.
106
+
107
+ ## Development
108
+ ```bash
109
+ brevo app upload # validate + save the configuration (diff shown first)
110
+ brevo app install # make it available in an account
111
+ brevo app uninstall # remove it from that account
112
+ ```
113
+ `brevo app upload` has no edit flags — it pushes the whole of `app-config.json`. `install` / `uninstall` take an optional `[account-id]`; omitted, the target resolves from the authenticated account.
114
+
115
+ ## Docs
116
+ - [Brevo API Documentation](https://developers.brevo.com)
117
+ - [Brevo CLI reference](https://developers.brevo.com/docs/cli-reference) — full command and option list
118
+ - [Brevo CLI repo](https://github.com/getbrevo/brevo-cli)
119
+ {{/if}}
@@ -1,4 +1,5 @@
1
1
  # CLAUDE.md — {{APP_NAME}}
2
+ {{#if oauth}}
2
3
 
3
4
  ## Project
4
5
  Brevo OAuth integration for app `{{APP_ID}}`.
@@ -65,3 +66,74 @@ This is a **public app**, so the flow uses **PKCE (RFC 7636)** and no client sec
65
66
  - Add `/auth/logout` that calls `POST /realms/{realm}/oauth/revoke` and clears the token store (the endpoint is supported by the OAuth server but the scaffold doesn't use it yet)
66
67
  - Add automated tests around the callback and refresh routes (e.g. with `supertest` + axios mocks) before extending the flow
67
68
  - Persist (or rotate) the CSRF `state` per session — the scaffold stores a single global state, which is fine for solo localhost testing but not for anything else
69
+ {{/if}}
70
+ {{#if ui_app}}
71
+
72
+ ## Project
73
+ Brevo **UI app** `{{APP_ID}}` — an action link that renders inside Brevo CRM records.
74
+ Scaffolded with the [Brevo CLI](https://github.com/getbrevo/brevo-cli) — see the [Brevo CLI reference](https://developers.brevo.com/docs/cli-reference) for the full command list.
75
+
76
+ > **There is no local server to run.** A UI app has no OAuth callback and no scaffolded code — Brevo opens a URL you already host. This project *is* the app's configuration.
77
+
78
+ ## Structure
79
+ ```
80
+ .
81
+ ├── app-config.json App metadata + the `ui_app` block (the whole app)
82
+ ├── .gitignore Ignores .env.local and build artifacts
83
+ ├── AGENTS.md AI agent guidance
84
+ ├── CLAUDE.md Claude Code guidance (this file)
85
+ └── README.md Human-facing setup guide
86
+ ```
87
+
88
+ ## Workflow
89
+ Everything about this app lives in the `ui_app` block of `app-config.json`. Edit the file, then:
90
+
91
+ ```bash
92
+ brevo app upload # validate + save the configuration (shows a diff first)
93
+ brevo app install # make it available in an account
94
+ brevo app uninstall # remove it from that account again
95
+ ```
96
+
97
+ `brevo app upload` has no edit flags — it always pushes the whole of `app-config.json`. `install` and `uninstall` both take an optional `[account-id]`; omitted, the target resolves from the authenticated account (a corporate account is asked which sub-account).
98
+
99
+ ## The `ui_app` block
100
+ The presence of `ui_app` is the app-type discriminator — it is what makes this a UI app rather than an OAuth one. That is why `auth` is `{}`, and it must stay empty: `brevo app upload` refuses a UI app carrying `scopes` or `redirectUris`.
101
+
102
+ `extension_type` is the only field at the root, because an app is one kind of extension rather than several:
103
+
104
+ | Value | What it does | Destination field |
105
+ |-------|--------------|-------------------|
106
+ | `actionLink` | Opens your URL in a new tab | `redirect_link` |
107
+ | `iframeExtension` | Embeds your URL in a modal | `modal_iframe_url` |
108
+
109
+ Values are camelCase. The older snake_case spellings (`action_link`) are rejected outright, not aliased.
110
+
111
+ ### `surface_point_list` entries
112
+ One entry is one placement, and each carries its own text and destination — so an app on three slots can label and deep-link each differently.
113
+
114
+ | Key | Required | Notes |
115
+ |-----|----------|-------|
116
+ | `surface_point_name` | yes | The registry **slug**, in dot notation — e.g. `contactDetails.header.menu`. This is *not* the dotted extension-point name a spec quotes (`contactDetails.headerMenu.action`); authoring that one fails validation at upload. |
117
+ | `label` | yes | Max 48 chars. The menu entry's text on an `.action` slot; the CTA button on a `.widget` slot's card. |
118
+ | `more_info` | no | Max 255 chars. The menu entry's second line; a card's description. |
119
+ | `redirect_link` | for `actionLink` | `https://` (or `http://localhost`). Record context arrives as **query parameters** — the path is never templated. |
120
+ | `modal_iframe_url` | for `iframeExtension` | Rejected on an `actionLink`, which navigates rather than embedding. |
121
+ | `context` | no | Narrows the record context Brevo sends. Omitted, the slot's full allow-list applies. |
122
+ | `size` | no | `{ "width": "280px", "height": "160px" }` — a positive integer with an explicit `px` or `%` unit (`%` capped at 100). An omitted axis keeps the slot's default. |
123
+
124
+ A card's **title** is the app name (`{{APP_NAME}}`) — there is no field for it.
125
+
126
+ `label`, `more_info`, `redirect_link`, `modal_iframe_url`, `context` and `size` all live **per entry**, never at the `ui_app` root. The root spellings are refused by name with a migration hint, as are the older `heading` / `subheading`.
127
+
128
+ Do not add `link_target`, `version` or `extension_point_name`. The platform owns all three — `brevo app upload` injects or strips them — so a copy in this file only shows up as permanent, unfixable drift in the upload diff.
129
+
130
+ ## Adding a placement
131
+ `brevo app create` authors exactly one. Add further placements by hand as extra `surface_point_list` entries, each with its own `label` and destination, then run `brevo app upload`.
132
+
133
+ Slot names are **not** checked locally, by design: the platform's extension-point registry is the only authority on them and the CLI keeps no copy that could lag it. `brevo app upload` sends the block and the platform rejects the upload naming any unregistered slot — so let `upload` be the check rather than guessing.
134
+
135
+ ## Next steps
136
+ - Host the `redirect_link` endpoint and read the record context off the query string (`recordId`, `recordType`, `accountId`, …) — treat every parameter as untrusted input; it arrives via the browser
137
+ - Run `brevo app upload`, then `brevo app install`, then open a CRM record to see the placement
138
+ - Keep `auth` as `{}` — a UI app uses no OAuth, and upload refuses one that carries OAuth fields
139
+ {{/if}}
@@ -1,4 +1,5 @@
1
1
  # {{APP_NAME}}
2
+ {{#if oauth}}
2
3
 
3
4
  Brevo OAuth integration for testing and implementing app authorization flows, scaffolded with the Brevo CLI.
4
5
 
@@ -80,3 +81,104 @@ To avoid unintentionally accumulating stale redirect URLs over time, periodicall
80
81
  - [Brevo API Documentation](https://developers.brevo.com)
81
82
  - [Brevo CLI reference](https://developers.brevo.com/docs/cli-reference)
82
83
  - [Brevo CLI](https://github.com/getbrevo/brevo-cli)
84
+ {{/if}}
85
+ {{#if ui_app}}
86
+
87
+ Brevo **UI app** — an action link that renders inside Brevo CRM records, scaffolded with the Brevo CLI.
88
+
89
+ > **There is no local server to run and no code to install.** A UI app has no OAuth callback. Brevo opens a URL you already host, and this project is the app's configuration.
90
+
91
+ ## Setup
92
+
93
+ 1. Open `app-config.json` and check the `ui_app` block — the placement, its label, and the URL Brevo will open.
94
+
95
+ 2. Push it to Brevo (the CLI shows a diff of local vs. server first):
96
+
97
+ ```bash
98
+ brevo app upload
99
+ # or skip the confirmation: brevo app upload --yes
100
+ ```
101
+
102
+ 3. Make it available in an account, then open a CRM record to see it:
103
+
104
+ ```bash
105
+ brevo app install
106
+ # or target a specific sub-account: brevo app install <account-id>
107
+ ```
108
+
109
+ Remove it again with `brevo app uninstall` (same optional `<account-id>`).
110
+
111
+ ## Project structure
112
+
113
+ ```
114
+ .
115
+ ├── app-config.json App metadata + the `ui_app` block (the whole app)
116
+ ├── .gitignore Ignores .env.local and build artifacts
117
+ ├── AGENTS.md AI agent guidance
118
+ ├── CLAUDE.md Claude Code guidance
119
+ └── README.md (this file)
120
+ ```
121
+
122
+ ## The `ui_app` block
123
+
124
+ `ui_app` is what makes this a UI app rather than an OAuth one, which is why `auth` is `{}` — leave it empty.
125
+
126
+ `extension_type` sits at the root and picks how the app opens:
127
+
128
+ | Value | What it does | Destination field |
129
+ |-------|--------------|-------------------|
130
+ | `actionLink` | Opens your URL in a new tab | `redirect_link` |
131
+ | `iframeExtension` | Embeds your URL in a modal | `modal_iframe_url` |
132
+
133
+ Each entry in `surface_point_list` is one placement, with its own text and destination:
134
+
135
+ | Key | Required | Notes |
136
+ |-----|----------|-------|
137
+ | `surface_point_name` | yes | Where it appears, as a dotted slug — e.g. `contactDetails.header.menu` |
138
+ | `label` | yes | Max 48 chars — the menu entry's text, or a card's button |
139
+ | `more_info` | no | Max 255 chars — the menu entry's second line, or a card's description |
140
+ | `redirect_link` | for `actionLink` | `https://` (or `http://localhost`) |
141
+ | `modal_iframe_url` | for `iframeExtension` | `https://` (or `http://localhost`) |
142
+ | `context` | no | Narrows the record context Brevo sends |
143
+ | `size` | no | `{ "width": "280px", "height": "160px" }` — `px`, or `%` up to 100 |
144
+
145
+ A card's title is the app name — there is no field for it. Don't add `link_target`: the CLI sets it on upload, and a copy in this file shows up as drift you can't clear.
146
+
147
+ ## Record context
148
+
149
+ Brevo appends the record context to your URL as **query parameters** — the path is never templated. For example:
150
+
151
+ ```
152
+ https://example.com/open?recordId=123&recordType=contact&accountId=456
153
+ ```
154
+
155
+ These values reach you through the browser, so validate them like any other user input.
156
+
157
+ ## Adding a placement
158
+
159
+ `brevo app create` sets up one. To add another, append a `surface_point_list` entry with its own `label` and destination, then:
160
+
161
+ ```bash
162
+ brevo app upload
163
+ ```
164
+
165
+ Slot names are checked by Brevo, not locally — `brevo app upload` fails and names any slot that isn't registered.
166
+
167
+ ## Updating your app
168
+
169
+ `brevo app upload` has no edit flags — it always pushes the full contents of `app-config.json`. To change the app's name, label, destination or placement, edit the file and run:
170
+
171
+ ```bash
172
+ brevo app upload
173
+ ```
174
+
175
+ It fetches the current server state first and shows a diff before pushing — review it before confirming (or pass `--yes` to skip the prompt).
176
+
177
+ `brevo app upload` has no `--app-id` flag either: it always resolves the app from this directory's `app-config.json`. To manage a different app, `cd` into that app's project directory.
178
+
179
+ ## Docs
180
+
181
+ - [Brevo API Documentation](https://developers.brevo.com)
182
+ - [Brevo CLI reference](https://developers.brevo.com/docs/cli-reference)
183
+ - [Brevo CLI](https://github.com/getbrevo/brevo-cli)
184
+ {{/if}}