Sync latest main into feature_set-brevo-cli-v2 - #80
Merged
shubham773 merged 11 commits intoAug 31, 2026
Merged
Conversation
…filtered registry reads [BEX-422]; rename deploy/rollback to install/uninstall; warn on app delete (#56) * feat: accept per-entry card size in ui_app.surface_point_list [BEX-416] Each entry may author size {width, height} (px, both required, positive integers) at the same level as context. Shape-only validation, like the rest of validateUiApp — bounds and registry concerns stay the platform's. Absent size keeps today's behaviour: the host page's per-slot default applies. Partner-authored, so it round-trips through the upload diff and write-back untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: size axes are CSS lengths — px or percent, at least one axis [BEX-416] Redesign from review on the platform PR: a size axis is '<positive integer>px' (absolute) or '<1-100>%' (relative to the host slot's box), and a single-axis object is valid — the omitted axis stays on the host slot's default. An empty size object is refused. Matches the platform's validateEntrySizeShape grammar exactly, so nothing valid here 400s at app upload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: both size axes are optional — an empty size object authors nothing [BEX-416] validateSurfacePointSize no longer requires at least one of width or height: an empty object is valid and means the host slot's default, the same as omitting the key — the platform normalizes it to no size at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: move label, more_info and the destination URL into each surface_point_list entry [BEX-426] Each placement now carries its own label, more_info and redirect_link / modal_iframe_url; the ui_app root keeps only extension_type (and the injected link_target). Root CTA spellings are refused by name with a migration hint, per-entry violations name their entry, and the create flow authors exactly one placement (single-select page prompt) — more placements are hand-authored in app-config.json and uploaded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: rename app deploy/rollback to install/uninstall; warn on delete `brevo app deploy` / `brevo app rollback` become `brevo app install` / `brevo app uninstall`, matching the platform resource they act on (POST/DELETE .../installs). Full internal rename: files, message keys, CLI.* constants, service methods, help screen, and JSON output keys (`installed`/`uninstalled`, reason NOT_INSTALLED). Both commands are pre-GA and absent from published builds, so no shipped surface changes. The wire contract is untouched — `deploy_client_id` is the server's field name and stays. The command exports are `appInstallCommand` / `appUninstallCommand` because the bare names collide with the `skill:cli install`/`uninstall` exports, which would blind the build's leak-marker guard. `brevo app delete` now warns before the confirmation prompt that deleting an app also removes it from every account where it is installed or published, and that installs and credentials cannot be recovered. `--force` still skips both the warning and the prompt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(ui-app): update surface_point_name examples to the dot-notation slugs (#64) The extension_points registry's surface_point_name slugs are being renamed from kebab-case (contact-details-header-menu) to dot notation (contactDetails.header.menu). The CLI validates shape only and fetches the registry live, so no behavior changes — but the error-message examples and the comments describing the slug format taught the old kebab spelling. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat: narrow surface-point reads by the chosen extension type [BEX-422] (#63) Both registry reads in `app create` now pass `?extension_type=` — the locations read so the page prompt cannot offer a page whose every slot the catalogue hides for the chosen type, and the row read as the server-side half of the type check. The unfiltered retry drops both parameters, and rowSupportsExtensionType stays as the client-side check for servers predating the filter. fetchSurfacePoints normalizes the endpoint's enabled_extension_types (what app-store-bo-be serves since BEX-422) onto extension_type_list, newest spelling first, alongside the two older namings. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(app-create): label UI placements from the registry, not a local map [BEX-426] The placement prompt showed a CLI-owned prettified name per slot — `headerMenu` as `Header "More" (•••) menu`, the kind segment as `menu entry` / `card`. It now shows the registry's own values joined verbatim: `section_name — component_type`, e.g. `headerMenu — action`. Same reasoning that removed the record-page map earlier: a local map gives every server-supplied identifier a second name to keep in step with the platform, needs a guess for anything it doesn't know (so the prompt mixed two vocabularies), and lets the prompt show something the API never said. Choice VALUES are unchanged — still the row's `surface_point_name` slug — so nothing about what gets authored into app-config.json changes. Removes `EXTENSION_PLACE_LABELS` plus the now-unused `EXTENSION_KIND_ACTION` / `EXTENSION_KIND_WIDGET`, and the two placement-suffix strings. Folds the "no friendly-name map" note in constants.ts to cover placements as well as pages so neither map gets reintroduced. Pre-GA (UI apps are absent from published builds), so nothing shipped changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: move ui_app link_target into each surface_point_list entry [BEX-426] `link_target` was the last CTA-adjacent field left at the `ui_app` root. It qualifies a per-entry destination — where THAT entry's `redirect_link` opens — so a root copy could only ever say one thing about every placement, and the platform's read path already treats each entry as the only source for it. `extension_type` is now the only field at the block root. - `brevo app upload` injects `_blank` onto every `actionLink` entry instead of the block root (`withInjectedLinkTargets`). An `iframeExtension` is untouched, and an entry that already carries a value keeps it. - `validateUiApp` refuses a leftover root `link_target` by name, with a hint to remove it rather than relocate it: the field is not authored in `app-config.json` at all. A per-entry value is checked against the `_blank` pin and refused outright on an `iframeExtension` entry, naming the entry. - The wire-only key strip needed no change — it already strips at every depth, so the upload diff, the write-back and the scaffold pull followed the field one level down on their own. `app-config.json` still never carries it. The field is echoed per entry now, so the response no longer withholds it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(app): align the next-steps hints in the UI-app box The two step lines carried hard-coded padding — 14 spaces after `brevo app upload`, 3 after `brevo app install` — for commands one character apart in length, so the parenthetical notes landed ten columns apart inside the same box. Replace both with a `numberedSteps` helper that derives the note column from the widest command in the list, and route `APP_CREATE_BASE_ONLY_NEXT` through it as well: its single-step output is byte-identical, so the OAuth path is untouched, and a second step added there later cannot reintroduce the drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(app): gate install/uninstall on app type, and name the target account Only a UI app is installed into an account. The capability matrix has encoded that since it was written — `account-install` is its example of a type-driven capability — but nothing on the install path consulted it, and the installs endpoint has no app-type check of its own, so an OAuth app installed with a 201 and rendered nothing. Refuse both verbs via `assertCapability`, so a third app type answers by appearing in the table rather than by editing a comparison. The refusal is biased to fire only when it is sure: a record carrying OAuth material is definitely an OAuth app, one carrying none reads as a UI app and is allowed through, so the failure mode is a missed refusal rather than a wrongly refused UI app. A read failure does not block — same policy the upload gate already had, since guarding a silent no-op must not become a new way to fail. `assertUploadedBeforeInstall` becomes `assertInstallable` and answers both questions off one app read. It also stops trusting the local config unconditionally: `app-config.json` may only answer for the app it describes, so `--app-id <other-app>` run inside a linked project is now gated against the app it names. Previously the directory's config answered, and an OAuth app named explicitly from inside a UI-app project passed both checks on the strength of a config that was not about it. `uninstall` takes the type check and still skips the upload check: an app installed by an older CLI must stay removable whatever its version says, while an OAuth app never had an install to remove. The one case this strands is an OAuth app installed by a CLI predating the check. Both verbs now name the account rather than printing a bare ID. Two of the three resolution paths choose the account for the user, who then has no number of their own to check it against — a picked sub-account reads `Company2 (account 4043630)`, the caller's own account reads `Acme Retail (your own account, ID 12345)`, and that identifier may be a UUID they have never seen. Both names come off responses those paths already read, so nothing costs an extra request. An explicit `[account-id]` is deliberately not looked up — its whole purpose is reaching an account the listing will not show — and its wording is unchanged, so CI output is untouched. `--json` gains an additive `accountName`; `accountId` stays the raw identifier. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(cli): show a UUID in the --app-id examples An app ID is a UUID — `app create` issues one and `app-config.json` stores it — but every `--help` example said `--app-id 42`, which reads as a database row. Eight commands taught the wrong shape identically. Route them all through one `EXAMPLE_APP_ID` constant so they cannot drift into showing two shapes, and update the README's one occurrence. The agent docs already use `--app-id <id>`, so they need no change. Also folds this branch's three changes into the existing changeset rather than adding a second file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(app-create): drop the disabled Iframe choice from the UI integration prompt The integration-type prompt showed Iframe as a disabled "coming soon" choice so partners could see the roadmap where the decision is made. Removed for now: Link (actionLink) is the only choice offered until iframe authoring is ready. The question is still asked with its one choice — same pattern as the gated app-type and distribution prompts — and a hand-authored iframeExtension block still validates and uploads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…; install/uninstall rename + delete warning [BEX-427] (#68) * feat: accept per-entry card size in ui_app.surface_point_list [BEX-416] Each entry may author size {width, height} (px, both required, positive integers) at the same level as context. Shape-only validation, like the rest of validateUiApp — bounds and registry concerns stay the platform's. Absent size keeps today's behaviour: the host page's per-slot default applies. Partner-authored, so it round-trips through the upload diff and write-back untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: size axes are CSS lengths — px or percent, at least one axis [BEX-416] Redesign from review on the platform PR: a size axis is '<positive integer>px' (absolute) or '<1-100>%' (relative to the host slot's box), and a single-axis object is valid — the omitted axis stays on the host slot's default. An empty size object is refused. Matches the platform's validateEntrySizeShape grammar exactly, so nothing valid here 400s at app upload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: both size axes are optional — an empty size object authors nothing [BEX-416] validateSurfacePointSize no longer requires at least one of width or height: an empty object is valid and means the host slot's default, the same as omitting the key — the platform normalizes it to no size at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: move label, more_info and the destination URL into each surface_point_list entry [BEX-426] Each placement now carries its own label, more_info and redirect_link / modal_iframe_url; the ui_app root keeps only extension_type (and the injected link_target). Root CTA spellings are refused by name with a migration hint, per-entry violations name their entry, and the create flow authors exactly one placement (single-select page prompt) — more placements are hand-authored in app-config.json and uploaded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: rename app deploy/rollback to install/uninstall; warn on delete `brevo app deploy` / `brevo app rollback` become `brevo app install` / `brevo app uninstall`, matching the platform resource they act on (POST/DELETE .../installs). Full internal rename: files, message keys, CLI.* constants, service methods, help screen, and JSON output keys (`installed`/`uninstalled`, reason NOT_INSTALLED). Both commands are pre-GA and absent from published builds, so no shipped surface changes. The wire contract is untouched — `deploy_client_id` is the server's field name and stays. The command exports are `appInstallCommand` / `appUninstallCommand` because the bare names collide with the `skill:cli install`/`uninstall` exports, which would blind the build's leak-marker guard. `brevo app delete` now warns before the confirmation prompt that deleting an app also removes it from every account where it is installed or published, and that installs and credentials cannot be recovered. `--force` still skips both the warning and the prompt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(ui-app): update surface_point_name examples to the dot-notation slugs (#64) The extension_points registry's surface_point_name slugs are being renamed from kebab-case (contact-details-header-menu) to dot notation (contactDetails.header.menu). The CLI validates shape only and fetches the registry live, so no behavior changes — but the error-message examples and the comments describing the slug format taught the old kebab spelling. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat: narrow surface-point reads by the chosen extension type [BEX-422] (#63) Both registry reads in `app create` now pass `?extension_type=` — the locations read so the page prompt cannot offer a page whose every slot the catalogue hides for the chosen type, and the row read as the server-side half of the type check. The unfiltered retry drops both parameters, and rowSupportsExtensionType stays as the client-side check for servers predating the filter. fetchSurfacePoints normalizes the endpoint's enabled_extension_types (what app-store-bo-be serves since BEX-422) onto extension_type_list, newest spelling first, alongside the two older namings. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(app-create): label UI placements from the registry, not a local map [BEX-426] The placement prompt showed a CLI-owned prettified name per slot — `headerMenu` as `Header "More" (•••) menu`, the kind segment as `menu entry` / `card`. It now shows the registry's own values joined verbatim: `section_name — component_type`, e.g. `headerMenu — action`. Same reasoning that removed the record-page map earlier: a local map gives every server-supplied identifier a second name to keep in step with the platform, needs a guess for anything it doesn't know (so the prompt mixed two vocabularies), and lets the prompt show something the API never said. Choice VALUES are unchanged — still the row's `surface_point_name` slug — so nothing about what gets authored into app-config.json changes. Removes `EXTENSION_PLACE_LABELS` plus the now-unused `EXTENSION_KIND_ACTION` / `EXTENSION_KIND_WIDGET`, and the two placement-suffix strings. Folds the "no friendly-name map" note in constants.ts to cover placements as well as pages so neither map gets reintroduced. Pre-GA (UI apps are absent from published builds), so nothing shipped changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: move ui_app link_target into each surface_point_list entry [BEX-426] `link_target` was the last CTA-adjacent field left at the `ui_app` root. It qualifies a per-entry destination — where THAT entry's `redirect_link` opens — so a root copy could only ever say one thing about every placement, and the platform's read path already treats each entry as the only source for it. `extension_type` is now the only field at the block root. - `brevo app upload` injects `_blank` onto every `actionLink` entry instead of the block root (`withInjectedLinkTargets`). An `iframeExtension` is untouched, and an entry that already carries a value keeps it. - `validateUiApp` refuses a leftover root `link_target` by name, with a hint to remove it rather than relocate it: the field is not authored in `app-config.json` at all. A per-entry value is checked against the `_blank` pin and refused outright on an `iframeExtension` entry, naming the entry. - The wire-only key strip needed no change — it already strips at every depth, so the upload diff, the write-back and the scaffold pull followed the field one level down on their own. `app-config.json` still never carries it. The field is echoed per entry now, so the response no longer withholds it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(app): align the next-steps hints in the UI-app box The two step lines carried hard-coded padding — 14 spaces after `brevo app upload`, 3 after `brevo app install` — for commands one character apart in length, so the parenthetical notes landed ten columns apart inside the same box. Replace both with a `numberedSteps` helper that derives the note column from the widest command in the list, and route `APP_CREATE_BASE_ONLY_NEXT` through it as well: its single-step output is byte-identical, so the OAuth path is untouched, and a second step added there later cannot reintroduce the drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(app): gate install/uninstall on app type, and name the target account Only a UI app is installed into an account. The capability matrix has encoded that since it was written — `account-install` is its example of a type-driven capability — but nothing on the install path consulted it, and the installs endpoint has no app-type check of its own, so an OAuth app installed with a 201 and rendered nothing. Refuse both verbs via `assertCapability`, so a third app type answers by appearing in the table rather than by editing a comparison. The refusal is biased to fire only when it is sure: a record carrying OAuth material is definitely an OAuth app, one carrying none reads as a UI app and is allowed through, so the failure mode is a missed refusal rather than a wrongly refused UI app. A read failure does not block — same policy the upload gate already had, since guarding a silent no-op must not become a new way to fail. `assertUploadedBeforeInstall` becomes `assertInstallable` and answers both questions off one app read. It also stops trusting the local config unconditionally: `app-config.json` may only answer for the app it describes, so `--app-id <other-app>` run inside a linked project is now gated against the app it names. Previously the directory's config answered, and an OAuth app named explicitly from inside a UI-app project passed both checks on the strength of a config that was not about it. `uninstall` takes the type check and still skips the upload check: an app installed by an older CLI must stay removable whatever its version says, while an OAuth app never had an install to remove. The one case this strands is an OAuth app installed by a CLI predating the check. Both verbs now name the account rather than printing a bare ID. Two of the three resolution paths choose the account for the user, who then has no number of their own to check it against — a picked sub-account reads `Company2 (account 4043630)`, the caller's own account reads `Acme Retail (your own account, ID 12345)`, and that identifier may be a UUID they have never seen. Both names come off responses those paths already read, so nothing costs an extra request. An explicit `[account-id]` is deliberately not looked up — its whole purpose is reaching an account the listing will not show — and its wording is unchanged, so CI output is untouched. `--json` gains an additive `accountName`; `accountId` stays the raw identifier. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(cli): show a UUID in the --app-id examples An app ID is a UUID — `app create` issues one and `app-config.json` stores it — but every `--help` example said `--app-id 42`, which reads as a database row. Eight commands taught the wrong shape identically. Route them all through one `EXAMPLE_APP_ID` constant so they cannot drift into showing two shapes, and update the README's one occurrence. The agent docs already use `--app-id <id>`, so they need no change. Also folds this branch's three changes into the existing changeset rather than adding a second file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(app-create): drop the disabled Iframe choice from the UI integration prompt The integration-type prompt showed Iframe as a disabled "coming soon" choice so partners could see the roadmap where the decision is made. Removed for now: Link (actionLink) is the only choice offered until iframe authoring is ready. The question is still asked with its one choice — same pattern as the gated app-type and distribution prompts — and a hand-authored iframeExtension block still validates and uploads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: take UI apps out of the pre-GA gate [BEX-290] UI apps now ship in every build. FEATURE_STAGE['ui-app-type'] and FEATURE_STAGE['account-install'] flip to 'ga', and everything the build-time gate held back moves to the live surface: - `app install` / `app uninstall` command definitions move from preview-definitions.ts into definitions.ts, and their names leave LEAK_MARKERS in scripts/build.mjs (only submit/status/withdraw remain gated). - The UI-app authoring and install/uninstall strings move from lang/preview-messages.ts back into lang/en.ts. - `app create` drops its `__BREVO_PREVIEW__` guards: the UI-app choice, resolveUiApp and the created-UI-app box are reachable in published builds (still prompt-only — no --type flag). - The App-install section of root help renders unconditionally. Docs follow: CLAUDE.md / AGENTS.md sections retitled to "UI apps are GA", README command table gains install/uninstall, and agent-context/SKILL.md + AGENTS.md document the UI-app flow, the ui_app config block, and install semantics. The pending changeset records the GA note. Public app distribution stays pre-GA and eliminated from published builds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: close the gaps the UI-apps GA flip left open [BEX-290] The GA flip shipped with loose ends of its own, found in review: - flip APP_TYPES.ui.availability to 'ga' and update the test that cemented the stale 'preview' value - document the per-entry size key in SKILL.md / AGENTS.md, which told agents every undocumented key is rejected at upload - state the corporate-account exception in README's install row - pad the gate test's presence assertion so 'uninstall' can no longer satisfy the check that 'app install' survived a public build - assert the GA install surface is PRESENT in both builds (GA_MARKERS in scripts/build.mjs), the mirror image of LEAK_MARKERS - restore gatedSection('account-install') around the root-help section so FEATURE_STAGE stays the one decision help and runtime both read; GA removed only the __BREVO_PREVIEW__ wrapper - retire the "UI apps aren't live" premises in comments (validators, constants, contract, ui/index) and the one-edit-GA claim in preview.ts's header - re-home RELEASE-CHECKLIST.md / docs.md pointers to the docs branch (docs/public-cli-ui-apps-feature-changes), where the still-open corporate-discriminator assumption is now tracked Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: uninstall's sub-account picker asked "install into" [BEX-290] The corporate sub-account picker hardcoded install's prompt, so `brevo app uninstall` asked which account to "install into". The wording now travels per command through resolveInstallTarget: uninstall asks "Select the account to uninstall from:". The shared error strings stay shared — both correctly point at `app install`. Tests pin each command's prompt so the strings can't be re-merged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: label the caller's own-account identifier as org ID, not ID [BEX-290] The install/uninstall confirmation and result messages named the caller's own-account identifier as plain "ID", which reads as a generic identifier even though it's specifically the organization ID. * docs(changeset): condense the pending changeset to user-facing feature notes [BEX-416] The changeset had grown into a per-commit log — internal file moves, pre-GA caveats mooted by the GA flip shipping in the same release, and implementation rationale that belongs in the PR, not the CHANGELOG. Rewritten as concise feature-level notes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(validators): extract per-entry context/size checks from validateSurfacePointList Sonar S3776 flagged the function at cognitive complexity 20 (limit 15). The per-entry context and size blocks move into validateEntryContext / validateEntrySize, mirroring the existing validateEntryCtaFields pattern, leaving the list function with the list-level rules only. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: add branch-local UI-apps release status; guard both status docs from main [BEX-290] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: surface_point_name slugs are dot notation, not kebab-case [BEX-290] The registry renamed its surface_point_name slugs from kebab-case to dot notation (contact-details-header-menu -> contactDetails.header.menu) in a 2026-08-18 platform migration, verified applied on prod 2026-08-24. PR #64 updated most CLI examples; this catches the stragglers: CLAUDE.md's two-names note, the slug bullets in agent-context/SKILL.md and AGENTS.md, the rejectRootCtaFields migration-hint example, and a comment in authoring.ts. Also records the type === 'corporate' discriminator on /v3/account/info as verified (confirmed against a live corporate account, 2026-08-21) in CLAUDE.md and the AccountResponse.type comment - it was the last assumed wire contract, and the docs-branch open-questions log is closing it out in a companion commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: UI-only install picker; credentials refuses UI apps [BEX-290] `brevo app credentials` on a UI app used to print an empty credential form (blank client ID, "(none)" everywhere) and cache the emptiness. It now refuses with a typed message before any side effect, routed through the capability matrix ('oauth-flow'), which has named this command as OAuth-only since it was written. The `app install` / `app uninstall` picker now offers only UI apps — an OAuth row was a choice whose only outcome was the type-gate refusal one step later. Rows classify by the same OAuth-material bias as `assertInstallable`, the full-record check after the pick backstops it, and an emptied list errors pointing at `brevo app create`. The delete/withdraw/scaffold pickers stay unfiltered. Also scrub the two test fixtures using 'email' as a context value: the platform vocabulary is exactly recordId/recordName/userId/locale/ accountId, and email stays out pending PII sign-off (decision 2026-08-24: not adding it for now). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: retire the docs branch - working docs live on this branch now [BEX-290] CLAUDE.md pointed at docs/public-cli-ui-apps-feature-changes as the durable home of RELEASE-CHECKLIST.md / docs.md; that split is retired. The three working docs (plus QA-TESTCASES.md) now sit at this branch's root, covered by the same never-merge-into-main rule as the release-status docs, and CLAUDE.md's guard section, working-docs section and runbook pointer say so. Also closes out the references and claims that went stale with the move and with recent verifications: - src/types.ts: the surface-point alias comment no longer claims BEX-361 is unbuilt (shipped; aliases are cleanup-ready per docs.md), and the absent auth-block comment records the contract as confirmed live (2026-08-12) instead of assumed - same fix in upload.test.ts. - QA-TESTCASES.md: the private-app half was deleted from features_set_public_cli once worked, not kept there - the header now says where to recover it from history. - docs.md: the app-credentials-on-a-UI-app UX decision is recorded as decided and shipped (refuse via the capability matrix). - UI-APPS-RELEASE-STATUS.md: the docs-debt section and the kebab-slug / corporate-discriminator follow-ups are marked resolved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: split the working docs by feature - UI-apps halves stay here [BEX-290] Companion to feature_set-brevo-cli-v2's adoption commit (59ee376): the public-apps halves of RELEASE-CHECKLIST.md, docs.md and QA-TESTCASES.md (release copy, the Before public-apps GA runbook, QA suites 2/5/6/7/10/13, the BEX-405 guard explainer and build-gate residue note) moved to that branch. This branch keeps the UI-apps halves: - RELEASE-CHECKLIST.md - the UI-apps GA record (worked through at BEX-290; stays until PR #68 + the Version Packages PR publish). - docs.md - the UI-apps background narrative and Part 2 (ship steps, platform asks, CLI cleanups, UX decisions, parked items, Resolved). - QA-TESTCASES.md - Suite 12 with its own entry conditions and sign-off. CLAUDE.md's guard list, runbook pointer and working-docs section now describe the split. The docs/public-cli-ui-apps-feature-changes branch is deleted; its final pre-split state is preserved in closed PR #53. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(validators): report a missing surface_point_name key by name, not as a blank slot [BEX-416] An entry carrying the pre-rename `surface_point` spelling (or no name key at all) was reported as 'Surface point cannot be empty', which points at the wrong thing — the name IS there, under the wrong key. Missing key now gets its own message, with a rename hint when `surface_point` is present. Found by QA sweep 3 (TC-12.6 case 4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: record QA sweep 3 results — terminal-only Suite 12 gaps closed [BEX-416] Sweep 3 (2026-08-24, public build, production) closes TC-12.3 file half, TC-12.4 write-back, TC-12.5(a)-(c), TC-12.5b, TC-12.6 both halves, TC-12.12, TC-12.13, and settles the PKCE question from the templates. Records the new 502-retry duplicate-create finding (fixed on fix/post-502-retry-duplicate-create). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(qa): port sweep-3 Result lines into Suite 12; record finding dispositions [BEX-416] Result lines for TC-12.3/3b/4/5/5b/6/8/12/13 and the TC-12.14 PKCE settlement; sign-off table updated — terminal-only Suite 12 is closed, the browser-bound install set (BEX-438) and the prompt-flow family remain. Status doc records the 502-retry fix (PR #70), the accepted same-name-apps decision (no uniqueness ask), and the fixed TC-12.6 message nit (62b380e). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): retry a 502 only for idempotent methods, never POST/PATCH (#70) A 502 comes from a gateway, not the origin, so the origin may have processed the request before the response was lost. The blind one-shot replay duplicated a create in live QA: POST /v3/app-store/apps landed, its response surfaced as a 502, and the retry created the same app twice (the CLI reported only the second ID, orphaning the first). GET/PUT/DELETE replays can only repeat a result, so they keep the retry; POST and PATCH now surface the 502 instead. The 401/429 retries are unchanged — those statuses mean the origin refused the request without processing it. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * docs: PR #70 merged — 502-retry fix is on main [BEX-416] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: drop the branch-local working docs before #68 merges [BEX-290] QA-TESTCASES.md, RELEASE-CHECKLIST.md, docs.md and UI-APPS-RELEASE-STATUS.md are branch-local release-state notes (see CLAUDE.md) and must never land on the public main. They stay on disk (git-excluded locally) for the remaining release steps; their full content is preserved in this branch's history. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(smoke): add opt-in ui suite — pty-driven UI-app lifecycle [BEX-416] New scripts/smoke/ui-app.ts (yarn smoke --suite=ui / --with-ui): interactive create driven through a real pty, upload no-op after create, per-entry label/more_info edit round-tripped through upload, install, uninstall, repeat-uninstall (informational not-installed), delete. Runs on the published surface — UI apps are GA — and skips itself against a build whose app-type prompt offers no UI app. Core gains execExpectPty: expect-driven answers (each sent only after its prompt renders, since registry reads happen between prompts) into script(1) behind `cat |` — macOS script dies on node's socketpair stdio and on socket-backed FIFOs, but tolerates a real pipe(2), and keeps its child running past our post-last-answer stdin EOF. install/uninstall join GATED_COMMANDS so --against=published skips them on older npm builds; the ui app is wired into both cleanup paths. The suite's edited label is plain words: the upload endpoint refuses special characters in a per-entry label (found live — the CLI validator only checks blank + length; the server rule is the divergence, tracked in the branch-local docs). Verified live 2026-08-24: 13/13 green, install/uninstall exercised for the first time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(app): print the delete consequence line under --force [BEX-427] `brevo app delete --force` still deletes without prompting, but now prints the install-loss warning first so a scripted delete leaves a record of what it did. The line is kept out of --json runs: logWarn writes to stdout, and the --json contract is a single parseable JSON document. The confirm copy ("This cannot be undone") is deliberately unchanged — the 30-day soft-delete window is an internal operator affordance we do not surface to users (decision recorded on BEX-427). Changeset, SKILL.md and AGENTS.md updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(smoke): pin the pty shell and render numeric --json fields [BEX-416] Three fixes from a Sonar/robustness pass over the pty driver: - `spawn('sh', …)` resolved the shell through PATH, so a writable PATH entry could decide what runs (S4036). It is `/bin/sh` by absolute path now. The pipeline that shell interprets (`cat`, `script`) stays on PATH deliberately: `script`'s location differs across the platforms the driver supports, and the command string is built only from this file's literals plus an already resolved brevo path. - The single-quote-in-single-quote escape is hoisted to `SQ_IN_SQ` so the template that uses it is no longer nested (S4624). - `optStr()` returned '(none)' for a finite number, which is why a step detail line had to pre-stringify its value — and `String(res.accountId ?? '')` in the install step turned an object into "[object Object]" rather than '(none)'. `optStr()` now renders finite numbers and refuses everything else, so the caller passes the raw value. An account ID is a string on some resolution paths and a number on others, which is what made this reachable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(validators): quote the matched axis in the size range error [BEX-416] The out-of-range message interpolated `String(value)`, where `value` is `unknown` — so a non-string axis would have rendered as its default stringification. It can't reach that line today (the regex test fails first for anything but a string), but the pattern is anchored, so `match[0]` IS the authored string and is typed `string`. Quote that instead, and the message can never stringify an `unknown`. Also pins the wording in a test — entry name included — so the quoted value cannot silently become "[object Object]" again. The existing table asserts only that the throw happens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(changeset): fold the 502-retry patch into the branch changeset [BEX-416] CLAUDE.md is one changeset file per branch — append, don't multiply. The 502-retry fix arrived on its own `patch` file via PR #70, which merged into this branch rather than main, so both files were pending together and would have produced two entries for one release. Its summary is now a paragraph in `surface-point-entry-size.md`; the bump stays `minor`, which already outranks the patch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: repoint working-doc references at the branches that hold them [BEX-290] Six source comments pointed readers at `docs/public-cli-ui-apps-feature-changes`, which was deleted on 2026-08-24 when the working docs were split across the two feature branches — and CLAUDE.md in this same branch already says so, so the tree contradicted itself. The public-apps GA runbook references now name `feature_set-brevo-cli-v2`, which holds that half and persists until public apps ship. The UI-apps `docs.md` references drop the branch pointer entirely and point at CLAUDE.md's "Working docs" section instead: that half is branch-local and goes away when this branch merges, so naming any branch there would go stale again. Comment-only — no behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(app): refuse the install/uninstall app picker when it cannot be drawn [BEX-290] `resolveInstallTarget` fell through to `promptAppSelection` with no guard, so `brevo app install --json` (or any run off a TTY) outside a linked project and without `--app-id` reached the interactive picker: the spinner and choice list rendered into stdout — breaking the single-parseable-document contract `--json` promises and leaking app IDs into what a script is parsing — and inquirer then aborted with a raw ERR_USE_AFTER_CLOSE readline stack rather than anything a caller can act on. `app delete`, `app credentials` and `app withdraw` have all carried `assertAppSelectionAllowed` in front of their pickers since it was written; the two commands that went GA at BEX-290 were the ones missing it. Added there, before the apps-list round trip, with the `--app-id` form of the calling command so the refusal names a copy-pasteable replacement. Only the picker path is affected: a run that names its app with `--app-id` or through a linked app-config.json is unchanged, which is what keeps --json/CI working. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(build): stop gated command strings shipping in the public bundle [BEX-405] `CLI.APP_SUBMIT`/`APP_STATUS`/`APP_WITHDRAW` and the `/withdraw` + `/state` endpoints were referenced only from preview-gated modules, but survived into the published bundle because esbuild cannot prune properties from an object literal. No command reached them, yet `strings` on the published tarball named three unreleased commands — which is what the build-level gate exists to prevent. Apply the treatment `lang/preview-messages.ts` already uses: move them into `lib/preview-constants.ts` and spread that in behind `__BREVO_PREVIEW__`, so the whole object is unreachable on a public build and the bundler drops it. That makes the residue checkable for the first time, so add both guards: `previewCli`/`previewEndpoints` to LEAK_MARKERS (module bindings), and a new LEAK_STRINGS check on the user-facing command strings themselves — the bindings answer "did a gated module survive", the strings answer "what does the tarball reveal". Verified in three directions: absent from the public build, present in preview, and a deliberately reintroduced leak fails the build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(smoke): pace API calls and skip the public suite when the server refuses [BEX-416] Two fixes to the run's signal-to-noise, both found by a full --suite=all sweep. Pacing: the harness made ~40 API-backed calls back to back, which is what pushed a busy account over the throttle in the first place. The existing retry is reactive — it discovers the limit by tripping it — and in that run the teardown delete exhausted its retries and leaked an app. Add paceBrevoCall(), a minimum interval between `brevo` invocations, measured start-to-start so a command that already took longer than the gap adds no delay and only bursts are slowed. New --gap=<ms> flag, default 1000, --gap=0 restores the old behaviour. Reported in the summary and the JSON report so a run's wall-clock stays explicable. Public-suite skip: detectCapabilities can only read the client — it greps --help, so it answers "does this build offer --distribution public", never "will the server accept it". Those come apart on an account where Brevo refuses the create, and the whole lifecycle then reported nine hard failures. Add markFeatureUnavailable() so the create step can downgrade the capability on the server's refusal and skip; the eight steps that need that app then skip through the existing requireFeature rather than each raising "no public app from the create step". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(smoke): pin the pty pipeline's binaries to absolute paths [BEX-416] SonarCloud raised S4036 on PR #68 — "OS commands should not rely on PATH resolution" — against the pty driver. Fair: the shell was pinned to /bin/sh precisely so no PATH-writable directory could decide what runs, but the pipeline it interprets still invoked `cat` and `script` by bare name, so two of the three words were doing the thing the pin exists to prevent. Resolve both from fixed absolute candidates (/usr/bin/script or /bin/script, /bin/cat or /usr/bin/cat), first hit wins, with no bare-name fallback — a fallback would hand the decision back to PATH on exactly the machine where the absolute lookup failed. A missing binary now fails with a message naming what was looked for and where, instead of surfacing as a pty allocation error later. The old comment argued for leaving them on PATH because `script` sits in different places per platform and the command string holds only this file's literals. The candidate list covers the platform difference, so that argument no longer buys anything. Verified behaviour-preserving: the pinned pipeline still allocates a pty (isTTY=true through /bin/sh -c '/bin/cat | /usr/bin/script …'). Per CLAUDE.md, Sonar hotspots get fixed rather than marked Safe — test and script files included. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…branch project docs on app type [BEX-409] (#72) * fix(templates): branch project docs on app type so UI apps get UI-app docs [BEX-290] `templates/index.ts` has shipped `oauth` / `ui_app` template flags since BEX-290, but only `app-config.json.tmpl` ever used them. The three project docs were written from the OAuth templates for every app, so a scaffolded UI app was handed a `CLAUDE.md`, `AGENTS.md` and `README.md` describing an OAuth integration it does not have — including instructions to `yarn --cwd src/oauth install` and `brevo app start oauth` against a `src/oauth/` directory a UI app never receives (`finishProject` returns before the feature offer, and `app scaffold` refuses with APP_SCAFFOLD_NO_FEATURES_FOR_UI_APP). Each of the three templates now carries an `{{#if oauth}}` branch holding the existing body verbatim and an `{{#if ui_app}}` branch documenting what a UI app actually is: the `ui_app` block as the app-type discriminator, `extension_type` as the only root field, the per-entry `surface_point_list` contract (`surface_point_name` slug vs. the dotted extension-point name, `label`, `more_info`, `redirect_link` / `modal_iframe_url`, `context`, `size` with their real limits), the server-owned keys not to author, and the `upload` -> `install` / `uninstall` flow in place of a local server. `gitignore.tmpl` is deliberately left unbranched: the credentials line is harmless for a UI app and still covers a hand-added OAuth directory. OAuth projects are unaffected — the marker lines are stripped in full, so both distributions render byte-identical files to before. Tests assert that, that neither branch can render empty, and that the UI branch never names the OAuth feature, quotes the authorable slug rather than the dotted name, and presents no server-owned key as a field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(app): show the ui_app config on install and diff it on upload [BEX-290] `brevo app install` now prints the configuration it is about to install — app ID, name, version, extension type and every placement — read from the server, because an install makes the stored snapshot visible in an account and that is not necessarily what the local app-config.json says. The read is shared with the type/upload gates via `serverApp`, so it costs one request. A linked config that has drifted from the stored block is reported as a notice (not a refusal) naming `brevo app upload`; `--json` gains additive `version` and `ui_app` keys. `brevo app upload` now diffs the ui_app block instead of printing only its desired state with a bare `(changed)` marker: each changed value renders as `before -> after`, added placements are tagged `(new)`, dropped ones trail `(removed)`, and entries match 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 block is shared by every install and an upload is live there immediately, with no re-install — and asks a confirmation that names that consequence. `--yes` skips the question but still prints the warning, mirroring `app delete --force`; `--json` prints neither and stays parseable. The warning is unconditional for a UI app because the platform exposes no install-listing read, so a count either way would be invented. Block equality moves to src/app-types/ui/compare.ts so the upload diff and the install drift check share one normalization, and the per-entry value rows are table-driven in ui/fields.ts so a new field cannot appear in the plain renderer without appearing in the diff (per-entry `size` now renders in both, having rendered nowhere before). Docs updated in the same change: README, agent-context/SKILL.md, agent-context/AGENTS.md, AGENTS.md and CLAUDE.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci(smoke): consolidate the smoke test into one reusable workflow The steps, timeout and concurrency lived twice — once in smoke-pre-merge.yml and once in smoke-post-merge.yml — so an ad-hoc run could drift from the run that actually gates a release. They now live once in smoke.yml, called by both triggers through `workflow_call`, and the two trigger workflows keep only their events and their `if:` guard. smoke.yml also owns the single manual "Run workflow" button (`against: published` or `local`, with the ref picked in "Use workflow from"), which is why the dispatch triggers are gone from the trigger workflows. The npm-propagation wait is guarded on `against: published` with an expected version, and non-blocking behaviour is now an input the caller asks for rather than a hard-coded `continue-on-error` — so only the pre-merge release PR is non-blocking. Checkout keeps `persist-credentials: false`; the run steps read their inputs through `env:` rather than interpolating into the shell. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci(smoke): install with --ignore-scripts SonarCloud raises "Lifecycle scripts are enabled by default" on the new install step (new code, so the identical line in the older workflows is not flagged). Fixed rather than dismissed, per the repo's Sonar policy: the job needs nothing a lifecycle script provides — the only one in the tree is husky's `prepare`, which installs git hooks — and it holds an API-key secret while it runs them. Verified against this lockfile in a clean install: `yarn build` (esbuild, which ships a postinstall) and `yarn smoke` (tsx) both work, and the full Jest suite passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…73) - APP_CREATE_TYPE_PROMPT: 'Distribution type?' -> 'What distribution type should this app use?', matching the full-question convention every other list/choice prompt in the flow already uses (APP_CREATE_APP_TYPE_PROMPT, APP_CREATE_UI_INTEGRATION_PROMPT, etc). - APP_CREATE_UI_INTEGRATION_EXTERNAL_LINK: drop 12 hand-typed padding spaces that existed only to align against the removed Iframe choice (2026-08-19). Dead weight now that Link is the only entry. - Label / More info / Redirect link prompts: tightened from three dense compound sentences into shorter inline copy, still self-contained (an earlier draft moved the "renders as" explanation to a separate `Tip:` line above the prompt; reverted -- that line is genuinely supplementary for APP_CREATE_REDIRECT_HINT's use case, but essential here, so hiding it above a bare `Label:` left the question meaningless once scrolled past). Redirect-link prompt no longer repeats its own query-param example since the created-app box already prints a live one. - Extended the 80-column prompt-width test to cover all three UI-app field prompts -- APP_CREATE_UI_REDIRECT_LINK_PROMPT had drifted to ~100 chars with nothing catching it, which is exactly what this test exists to prevent. No behavior change; copy only. Verified: yarn build, yarn test (1364/1364), yarn lint all clean. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…the SonarCloud findings [BEX-444] (#75) * feat(ci): gate the release with pre- and post-publish checks The release path had no assertion about the artifact itself. `yarn build` guards the gated public-app surface and `yarn test` runs jest against src/, but nothing looked at the tarball: `files:` is an allow-list, so a dropped `agent-context/SKILL.md` or a template that stopped being copied into dist/ publishes silently, and a dependency that drifts into devDependencies packs fine and then fails on the first user's machine with MODULE_NOT_FOUND. Nothing verified the OIDC posture either — if provenance quietly stopped attaching, no run would have noticed. `scripts/release-check.mjs` has two modes over one set of assertions: - `pre` (before the publish) inspects the tarball `npm pack` produces. - `post` (after it) inspects the tarball the registry serves. Both assert the same shape — required files present, forbidden files absent (the branch-local working docs, `.env`, `credentials.json`, `.brevo.json`, keys), every `src/templates/files/*.tmpl` shipped, no secret-shaped string in packed content, and the tarball installs into an empty tree where the installed `brevo --version` runs. Checking the same artifact from both sides is the point; a pre-publish gate on a different artifact isn't a gate. `post` adds what only exists after a publish: the version resolves (retried for 5 minutes of propagation), `latest` moved to it, a SLSA provenance attestation is attached, the publisher is the OIDC identity, and the download matches the advertised `dist.integrity`. It deliberately does not re-check the leak markers (build.mjs owns those and prepublishOnly reruns it) and is not the smoke test (that authenticates and drives real commands; this only proves the artifact). Wired in as `Pre-publish checks` before the changesets step and `Post-publish checks` right after it, ahead of the Slack announcement. Also runnable locally: `yarn release:check pre|post --version=x.y.z`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ci): clear the four SonarCloud findings on release-check.mjs Fixed rather than dismissed, per CLAUDE.md. - Two nested template literals (S4624) hoisted into named locals. - `split('\n').join(…)` → `replaceAll` (S7781). - The propagation wait no longer shells out to `sleep`, which Sonar flags as a PATH lookup (S4036). It was a subprocess to get a blocking sleep; making `waitForRegistry`/`postflight` async and awaiting a timer removes the process entirely, so there is nothing to resolve off PATH. Re-verified after the change: `pre` 7/7, `post --version=2.2.0` 12/12, `post --version=2.1.0` still fails on the dist-tag as it should, and the retry loop still prints and paces its attempts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: trim the commentary on the release gates Cut the comments back to what the code can't say for itself: 85 comment lines down to 29 in release-check.mjs, the section banners gone, the two release.yaml step comments down to four lines each, and the CLAUDE.md entry from five paragraphs to three. Behaviour untouched — `pre` 7/7, `post --version=2.2.0` 12/12, and the stale-version negative still fails on the dist-tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ci): pin the scheme on the gate's tarball fetch The URL comes from registry metadata, so `-L` could in principle follow a redirect down to http. Same hardening as the Homebrew fetch. Re-verified: `post --version=2.2.0` still 12/12, integrity unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: clear the nine SonarCloud findings on main Fixed, not marked Safe, per CLAUDE.md. Six vulnerabilities and three code smells. Lifecycle scripts on credentialed runners (5 findings) — `--ignore-scripts` on every dependency install in `release.yaml`, `push.yaml` and `pre-release.yaml`'s publish job, and on the `npm install -g npm@11.5.1` step in both publish workflows. `pre-release.yaml`'s smoke job already did this and its comment already records why it's safe; this brings the rest in line. The `HUSKY: 0` on the prerelease install is folded in — husky's `prepare` is the only lifecycle script in the tree, and `--ignore-scripts` supersedes it. Verified in a clean clone rather than assumed: `yarn install --frozen-lockfile --ignore-scripts` then lint, `yarn build` (esbuild resolves its binary from the `@esbuild/<platform>` optional dependency, not a postinstall), all 1403 tests, and `yarn smoke --help` (tsx). Also confirmed `npm install -g npm@11.5.1 --ignore-scripts` yields a working npm 11.5.1 — the OIDC publish needs that version, so a broken upgrade would have been a broken release. Protocol downgrade over redirects (1) — the Homebrew tarball fetch now passes `--proto '=https' --proto-redir '=https'`, so `-L` cannot be walked down to http. Confirmed it returns the same sha256 for 2.2.0 that the formula carries. Code smells (3) — a useless `?? {}` inside a spread in `scripts/smoke/core.ts` (spreading undefined is already a no-op), an `a && a.b` chain in `src/services/cli-info.ts` restructured into a guarded block rather than optional-chained (TypeScript doesn't narrow through `?.` comparisons, so the `lastChecked` arithmetic needs the guard), and `expect(x.length).toBe(n)` → `toHaveLength(n)` in the skill tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ci): dispatch the post-publish smoke at smoke.yml, not the trigger workflow The release job's smoke dispatch still pointed at `smoke-post-merge.yml`, which lost its `workflow_dispatch` trigger (and its `version` input) when the smoke workflows were split into a reusable `smoke.yml` plus two thin trigger workflows. `gh workflow run` on a `workflow_call`-only workflow answers `HTTP 422: Workflow does not have 'workflow_dispatch' trigger`, so the step failed after the publish had already succeeded — the 2.2.0 release run went red, no smoke ran, and the Homebrew bump step was skipped. - release.yaml dispatches `smoke.yml` with `against=published` and `expected_version=<tag>`, the same inputs smoke-post-merge.yml passes on the `release` path. - smoke.yml's `workflow_dispatch` gains `expected_version` so the dispatched run still waits for npm to serve the new version (blank on a manual run). - The Homebrew bump is gated on `!cancelled() && …published == 'true'` instead of the bare condition. It stays behind the published gate and stays last; it just no longer gets skipped when a step between it and the publish fails, which is what left npm on 2.2.0 with the formula on 2.1.0. - Comments in all three files record why the dispatch target is smoke.yml. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: trim the commentary on the smoke-dispatch fix Cut the added comments to what the code can't say for itself. No behaviour change — the dispatch target, inputs and `!cancelled()` gate are untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(build): drop sourcesContent and scan every published file for gated strings `package.json` `files:` ships the whole of `dist/`, so `index.js.map` is as public as the bundle. esbuild's `sourcesContent` embedded the untouched TypeScript of all 69 surviving modules, comments included — which undid what `minifyWhitespace` is there to do: `brevo app submit` and `brevo app withdraw` were absent from `index.js` and one grep away in `index.js.map`, while `LEAK_STRINGS` (whose stated job is that `strings` on a published artifact must not name an unreleased feature) read only the bundle and reported it clean. No gated module was ever in the map — `preview-definitions.ts` is eliminated and absent from `sources` — and this repo is public, so nothing secret was exposed. What leaked was the prose the gate exists to remove, in the one file the gate did not look at. Two changes: - `sourcesContent: false`. The map still resolves a stack trace to `src/lib/foo.ts:123` — that is `sources` + `mappings`, not the text. It also drops the map from 725 kB to 135 kB. - The `LEAK_STRINGS` scan now walks every file under `dist/` and names the offending file in the error. `LEAK_MARKERS` stays on the bundle: it asks whether a gated module survived elimination, which is answerable only there. Verified by re-enabling `sourcesContent` and watching the build fail, naming `dist/bin/index.js.map` for all three strings. Preview build unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci(smoke): add suite + account inputs, pin every release lane The manual "Run workflow" button now selects which app types to smoke and which test account to authenticate as. `suite` maps straight onto the runner's `--suite`, so the dropdown values are the argument — no label-to-flag mapping to drift. `account` names a configured SECRET rather than accepting a key: workflow_dispatch inputs are recorded on the run page and this repo is public, so a pasted key would be world-readable and permanent. A selection whose secret is missing fails the run instead of falling back to the default account, which would silently report on an account nobody chose. A suite containing the public-app (preview) surface is now refused unless `against=local`. Previously that pairing ran, auto-skipped the gated steps and went green — coverage the run never had. Every automated lane pins `suite`/`account` explicitly so retuning a dropdown cannot change what a gate verifies: pre-merge local / all (non-blocking, widest, PREVIEW=1) post-merge published / private,ui release.yaml dispatch / private,ui post-merge and the release dispatch move from private to private,ui: the two suites are disjoint, so `app install` / `app uninstall` and the whole `ui_app` path were unverified against the published package despite UI apps shipping in every build since 2.2.0. Both are pinned identically because the dispatch is the path that actually runs after a changesets publish. The account-resolution step sits under the same `continue-on-error` as the smoke itself, so a misconfigured secret cannot turn the deliberately non-blocking pre-merge check red. CLAUDE.md: a feature leaving preview must join the smoke test's live suites in the same PR — a GA'd feature the smoke never exercises is the mirror of shipping commands without docs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…opy (#77) * fix(smoke): repair stale UI-app prompt patterns and pin them to the copy The `ui` suite failed on main at step 24 with "pty run timed out waiting for prompt 6/9" after 122s. The pty was fine — it cleared five prompts, list navigation and two live registry reads. It was waiting for prompt copy that no longer exists. 890b19e (#73) reworded three UI-app create prompts and did not touch this suite. All three expect patterns were stale; the run died at the first: waiting for /the menu entry text, and the button text on a card/ CLI prints Label — the menu entry’s text, and the card’s button text: The new patterns anchor on short, punctuation-free openings. The old ones spanned a curly apostrophe and an em dash, and were long enough to wrap in a fixed-width pty transcript — two independent ways to break on a reword. The patterns become a named export pinned by a test against `messages`. The smoke must duplicate this copy: it drives the real binary, and under `--against=published` that binary legitimately lags this repo, so no assertion inside the suite can catch a reword. Verified in both directions — restoring the old pattern fails the test. Also fixes a skip message that named the wrong layer. With `suite=all` the runner builds PREVIEW=1, so the public commands ARE in the binary and the API is what refuses them — yet every dependent step reported "public-distribution not available in this build". The runtime reason is now recorded and quoted, because "not in this binary" and "the server said no" send a reader to different repos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(smoke): add clarifying comment on placement prompt fixture Addresses PR #77 review comment — clarifies that 'contact' is a representative, non-load-bearing page name in the smoke pattern test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Make OAUTH_BASE overridable via BREVO_OAUTH_BASE_URL, matching the existing BREVO_API_URL / BREVO_OAUTH_PROXY_URL pattern, so scope lookups and scaffolded project templates can target a non-production OAuth realm. Add an `environment` input (production/staging) to the smoke workflow. Staging URLs and test-account keys are read from new repo secrets (BREVO_STAGING_*), never inlined in the workflow file, since this repo is public. Co-authored-by: Claude <noreply@anthropic.com>
Brings the latest main (through #79) into the public-apps branch: UI apps GA [BEX-290], deploy/rollback -> install/uninstall rename [BEX-427], per-entry ui_app fields [BEX-416/422/426], install/upload config surfacing [BEX-409], release gates + smoke [BEX-444]. CLAUDE.md conflicts resolved: kept this branch's public-apps-gated sections and its branch-local working-docs description (public-apps = "this branch"); took main's now-true "UI apps are GA" section and canonical corporate-discriminator wording; kept both working-docs merge rules. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s (BEX-454) (#81) * fix(app submit): skip redundant fetch and show raw missing-field names (BEX-454) The command ran the state preflight and then always fetched the full app before the submittability gate, so a not-submittable app triggered a wasted GET /cli/apps/{id} call. Move the gate to run right after the state read, before the fetch. Also stop relabelling the missing-field names: the state API returns keys like logoLink / oauth.scopes, which the human path was rewriting to 'Logo URL' / 'Scopes'. Both --json and interactive output now show the names exactly as the API returns them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app status): show the status message returned by the API (BEX-454) app status ignored any message the state endpoint returns and always rendered locally-canned per-state copy. Prefer the API's message field when present; keep the per-state copy as a fallback for an older server that omits it (or sends a blank string), matching how state / submittable / missing_fields already tolerate an older server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Shubham Verma <shubham.verma@brevo.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What
Brings the latest
main(through #79) intofeature_set-brevo-cli-v2(the public-apps branch), which was 9 commits behind. Includes:deploy/rollback→install/uninstallrename [BEX-427]ui_appfields [BEX-416 / BEX-422 / BEX-426]Conflict resolution
Only
CLAUDE.mdconflicted (4 regions), resolved treating the file as living on the public-apps branch:main's now-true "UI apps are GA" section and its canonical corporate-discriminator wording.Everything else auto-merged. Branch-local working docs (
PUBLIC-APPS-RELEASE-STATUS.md,RELEASE-CHECKLIST.md,docs.md,QA-TESTCASES.md) are preserved on this branch.Verification
yarn build(tsc + bundle): passyarn test: 59 suites / 1416 tests passyarn lint: cleanfeature_set-brevo-cli-v2requires linear history (no merge commits). Squash-merge this PR — do not use a merge commit. Squashing loses nothing here sincemainalready carries the granular commit history.