@getvitops/cli 8.1.0 → 8.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,470 @@
1
1
  # @getvitops/cli
2
2
 
3
+ ## 8.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0574388: Add `site.crm` — push form submissions into HubSpot, Zoho Bigin, or Jobber.
8
+
9
+ Client sites need form submissions to land in the client's CRM. The options available before this
10
+ — a HubSpot/Bigin/Jobber iframe or JS embed — fail on three counts: the markup can't be
11
+ white-labelled against this design system, nothing can operate on the submission before it leaves
12
+ the page, and the submission never touched `createConversionRoute()`, so `_ac` ad-click attribution
13
+ was lost and no conversion notification was planned.
14
+
15
+ This adds one server-side path for all three CRMs, hung off the conversion route that already
16
+ exists. `createConversionRoute()` gets a new `crm` option (`{ config, credentials, fetch?,
17
+ timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
18
+ option, and are never config. A failed CRM write never fails the request and is folded into the
19
+ notification email's new "Delivery" section, so a broken CRM token is never silent.
20
+
21
+ New `@getvitops/utils/crm` subpath: `deliverToCrm()`, `mapSubmission()`, `deriveLastName()` (Bigin
22
+ requires a last name; this derives one from an explicit field, a full name, or an email address,
23
+ refusing rather than fabricating one), `submitToHubSpot()`, `submitToBigin()`, `submitToJobber()`.
24
+ `identifySubmitter()` was promoted out of `notify`'s private name-guessing cascade into
25
+ `@getvitops/utils/tracking`, since the CRM mapper needed the whole identity (first/last name,
26
+ email, phone, company), not just a display name.
27
+
28
+ Jobber is GraphQL, not REST, and answers `200 OK` for failures — a rejected `clientCreate` returns
29
+ `userErrors[]`, a malformed request returns top-level `errors[]`. `submitToJobber()` classifies on
30
+ the response body, not just the status. Its refresh tokens also rotate by default: **Refresh Token
31
+ Rotation must be turned off** in the app's Developer Center settings, since a Worker reading a
32
+ static `VITOPS_JOBBER_REFRESH_TOKEN` cannot persist a rotated one — a rotated token is detected at
33
+ exchange time and reported with the fix, rather than surfacing an hour later as an opaque 401.
34
+ Attribution and unmapped extra fields render into a best-effort note by default (works on every
35
+ Jobber plan); a resubmitting visitor creates a second Jobber client unless `crm.jobber.dedupe: true`
36
+ is set once its lookup query has been verified against a live account (defaults `false` pending
37
+ that verification — see `jobber.ts`'s docblock).
38
+
39
+ New `site.crm` config block (`provider`, `hubspot`/`bigin`/`jobber`, `fieldMap`, `trackingFields`)
40
+ and two CLI commands: `vitops crm check` (fetches the live HubSpot property list or Bigin field
41
+ list and reports every mapped name that doesn't exist there — HubSpot rejects an unknown field
42
+ outright, Bigin silently drops it; Jobber has nothing live to check yet, since its mapped values
43
+ currently render as note text) and `vitops crm setup` (creates missing HubSpot properties; Bigin
44
+ and Jobber have no such API, so both always print a manual reminder).
45
+
46
+ The generated privacy policy now names HubSpot, Zoho Bigin, or Jobber when `site.crm` configures
47
+ one. Bigin gets the same cross-border-transfer derivation Matomo Cloud already gets: a
48
+ `dataCenter: 'ca'` asserts no transfer outside Canada, any other data centre does. Jobber's
49
+ operator (Octopusapp Inc. d/b/a Jobber) is Canadian, but its own privacy policy names the United
50
+ States as a processing location too, so the policy always asserts a cross-border transfer for
51
+ Jobber regardless of config — a Canadian operator is not read as Canadian-only storage.
52
+
53
+ Credentials: `VITOPS_HUBSPOT_ACCESS_TOKEN` for HubSpot; `VITOPS_ZOHO_CLIENT_ID` /
54
+ `VITOPS_ZOHO_CLIENT_SECRET` / `VITOPS_ZOHO_REFRESH_TOKEN` for Bigin; `VITOPS_JOBBER_CLIENT_ID` /
55
+ `VITOPS_JOBBER_CLIENT_SECRET` / `VITOPS_JOBBER_REFRESH_TOKEN` for Jobber. Never config.
56
+
57
+ No consumer-facing change in `@getvitops/astro` beyond the new `crm` route option and a build
58
+ warning when `site.crm` is configured with no route answering `TRACKING_ENDPOINT` — CSS/Bricks/
59
+ tailwind outputs are unaffected.
60
+
61
+ - b3d9c07: `vitops docs blocks` prints every EmDash block type and its fields, so an agent can write valid Portable Text without reading the plugin source. It reads `@getvitops/emdash` from the project (and says so if it isn't installed); `@getvitops/emdash` exports `renderBlocksDoc()` for it.
62
+ - 0574388: Add `vitops email setup|check` — connect `site.notifications.email.from` to whether its domain is
63
+ actually onboarded onto an outbound email provider.
64
+
65
+ Before this, nothing checked that. `EMAIL.send()` throws `E_SENDER_NOT_VERIFIED` at runtime with no
66
+ build or deploy-time warning — the failure mode is silent and production-only.
67
+
68
+ `vitops email setup [domain]` onboards a sending domain (currently Cloudflare Email Sending only)
69
+ and polls DNS until its SPF/DKIM/DMARC records resolve or a timeout is reached; `vitops email
70
+ check` is the non-blocking deploy guard both `@getvitops/create` templates' `deploy`/`deploy:dev`
71
+ scripts now run first (`--strict` makes it a real gate; opt out per-site with
72
+ `site.notifications.email.verifySender: false`, or globally with `--skip` /
73
+ `VITOPS_SKIP_EMAIL_CHECK`).
74
+
75
+ Two things worth knowing before running it:
76
+
77
+ - **The onboarded entity is an exact name, and it is usually a subdomain**
78
+ (`send.yourdomain.com`), never the apex your `from` address names. When the exact domain isn't
79
+ onboarded but a sibling in the same zone already is, the command reports that as the fix — send
80
+ from the onboarded subdomain — rather than silently trying to onboard the apex, which rewrites
81
+ `_dmarc.<domain>` and can tighten DMARC policy on every other mail your domain sends.
82
+ - Candidates come from `site.notifications.email.from` (or its derived `noreply@<domains
83
+ .canonical>` default) first; a best-effort scan of `--src` only supplements it, and never guesses
84
+ at a computed `from` value.
85
+
86
+ New `@getvitops/utils/email` subpath and `site.notifications.email.verifySender` config field.
87
+ Needs `CLOUDFLARE_API_TOKEN` with an Email Sending scope in addition to the `Zone:Read` other
88
+ commands already use.
89
+
90
+ - 8d8aad7: **New: server-side Google Ads offline conversion upload.** Set
91
+ `site.ads.google.reporting: "server"` and a matching
92
+ `conversionActionId` (a numeric id or full
93
+ `customers/…/conversionActions/…` resource name — **not** the same value
94
+ as `conversionLabel`, which is the opaque label the web tag uses and
95
+ which the Ads API's upload endpoint doesn't accept) and
96
+ `createConversionRoute`'s new `googleAdsOffline` option uploads the
97
+ conversion via `ConversionUploadService.UploadClickConversions` instead
98
+ of relying on the visitor's browser to fire `gtag.js`. An action with no
99
+ matching `conversionActionId` keeps firing client-side regardless of
100
+ `reporting`, so turning it on before the id is filled in never silently
101
+ drops a conversion. `@getvitops/astro/routes` exports a matching
102
+ `toGoogleAdsConfig(config)` adapter, alongside `toCrmConfig`.
103
+
104
+ Structurally typed against Google's documented REST shape, not verified
105
+ against a live Ads account — no developer-token/MCC access exists
106
+ anywhere in this toolchain's own setup to test against. Wrapped so a
107
+ wrong guess degrades to a stated failure reason (folded into the
108
+ conversion email's "Delivery" section, same as a failed CRM write) rather
109
+ than breaking, the same caveat `@getvitops/utils/crm`'s Jobber executor
110
+ already carries for its own unverified surfaces.
111
+
112
+ `vitops ads lint` now flags a `reporting: "server"` property whose
113
+ configured actions have no matching `conversionActionId` — not a
114
+ failure, since that action keeps firing client-side, but worth surfacing
115
+ rather than leaving someone to wonder why nothing changed.
116
+
117
+ Not built: `vitops ads provision` (upserting conversion actions via
118
+ `ConversionActionService` and printing back both the label and the
119
+ resource id) — same credential blocker, and deliberately left unscoped
120
+ until there's a live account to verify the request/response shape
121
+ against.
122
+
123
+ ### Patch Changes
124
+
125
+ - 89265af: **New package: `@getvitops/config`** — the site-config provider blocks (`site.crm`, `site.ads`,
126
+ `site.reviews`, `site.seo.indexing`, `site.searchConsole`, `site.notifications`, `site.tracking`,
127
+ `site.security`, `site.deployment`) as zod schemas and inferred types, with the closed provider lists
128
+ (`AD_PROVIDERS`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`) on a zod-free `./providers` entry. It joins the
129
+ `fixed` group.
130
+
131
+ Why: `@getvitops/utils` holds the provider code but could not import its configuration types — the schema
132
+ lived in `@getvitops/generator`, a layer above — so it kept a hand-copied mirror beside each module,
133
+ an adapter converting into it, and a test holding the copy to the schema. The mirrors are gone.
134
+
135
+ - **`@getvitops/generator`**: `AD_PROVIDERS`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`, `AdProvider` and
136
+ `CrmProviderConfig` are re-exported unchanged. No import path changes.
137
+ - **`@getvitops/utils`**: the config types in `./crm`, `./ads`, `./indexing`, `./onboarding`, `./reviews`,
138
+ `./tracking` and `./notify` (`CrmConfig`, `HubSpotConfig`, `BiginConfig`, `JobberConfig`,
139
+ `IndexNowConfig`, `SearchConsoleConfig`, `ReviewsConfig`, `ReviewLocationConfig`, `TrackingConfig`,
140
+ `NotificationsConfig`, `EmailChannelConfig`, `DomainSetup`, `AdPropertySetup`) are now inferred from the
141
+ schema. Same names, same fields, but **optional fields now follow zod's inference**, so a hand-written
142
+ object literal typed against them may need `| undefined` handled. `AdPropertySetup` gains the fields the
143
+ schema always had (`conversionActionId`, `reporting`).
144
+ - **`@getvitops/astro`**: `GetvitopsAdProperty` and `GetvitopsTrackingOptions` are aliases of the schema
145
+ types. `toCrmConfig()` and `toGoogleAdsConfig()` are **deprecated** — they returned their argument's
146
+ field unchanged; pass `config.site.crm` / `config.site.ads?.google` directly.
147
+ - **`@getvitops/cli`**: `vitops ads` now passes `conversionActionId` and `reporting` through to the planner;
148
+ it previously copied a hand-picked subset of the block and dropped them.
149
+
150
+ Formats reached: none — no CSS change.
151
+
152
+ - c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
153
+ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
154
+ './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
155
+ `<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
156
+ offered, now usable by the client directly, no dev server required. An edit is visible
157
+ site-wide immediately (a runtime `:root` override, served via a new plugin API route and
158
+ `page:fragments`) and — once `site.deployment.repo`/`branch` and a `VITOPS_GITHUB_TOKEN`
159
+ secret are set (see the scaffold's `SETUP.html`) — auto-commits to your `dev` branch, so it
160
+ reaches the git-tracked `site.json` through your repo's normal review path rather than
161
+ becoming a second, unreviewed source of truth. Until that's configured, the live half still
162
+ works; only the auto-commit half is skipped (and says so in the admin page). The save route
163
+ runs the exact same accessibility (APCA contrast) gate `generate()` enforces at build time,
164
+ synchronously, before persisting — nothing non-compliant ever goes live even momentarily.
165
+
166
+ Requires `@astrojs/react` + `react` + `react-dom` (already scaffold dependencies, for
167
+ EmDash's own admin) and `@getvitops/core` (added as a new scaffold dependency this release).
168
+ Declares two new plugin capabilities when enabled: `network:request` (for the GitHub
169
+ auto-commit) and `hooks.page-fragments:register` (for the live override) — reviewed once at
170
+ plugin-install time.
171
+
172
+ **Breaking for `@getvitops/emdash`**: `vitopsEmdash()`'s `hooks.page-fragments:register`
173
+ capability is now also granted whenever `designSystemEditor` is set, independent of the
174
+ existing `scripts: 'fragments'` option (which governs something unrelated — web-component
175
+ script delivery). No change if you don't use `designSystemEditor`.
176
+
177
+ Also new, supporting the above:
178
+
179
+ - `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
180
+ `/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
181
+ `design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
182
+ standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
183
+ into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
184
+ `mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
185
+ (re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
186
+ subtree" implementation shared by the dev endpoint and the new plugin route. New
187
+ `site.deployment.repo`/`branch` config fields.
188
+ - `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
189
+ wrapper over GitHub's Contents API.
190
+ - `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
191
+ unchanged — `/__vitops/design-system`) so the same element can target the new plugin
192
+ route instead of the dev-only Vite endpoint.
193
+
194
+ - 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
195
+ both projects' GA releases.
196
+
197
+ **Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
198
+ `emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
199
+ `^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
200
+ the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
201
+ code here needed to change, only the declared range.
202
+
203
+ The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
204
+ the version bump:
205
+
206
+ - `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
207
+ `css.format: 'tailwind'` without installing either, which threw on build.
208
+ - `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
209
+ `vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
210
+ the build never generated.
211
+ - `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
212
+ `site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
213
+ migration sequence explicitly, rather than relying solely on first-request auto-migration. This
214
+ needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
215
+ which only a human can set — run `emdash migrate --status` against the real deployed database,
216
+ review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
217
+ <value>`. Deploys fail clearly on the migration-apply step until it's set.
218
+ - `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
219
+ not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
220
+ errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
221
+ Uncomment it once scaffolded into its own repo.
222
+
223
+ No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
224
+ to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
225
+ losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
226
+ now names the bad key instead of reporting a bare "Invalid input").
227
+
228
+ Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
229
+ (`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
230
+ has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
231
+ by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
232
+ hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
233
+ `postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
234
+ `node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
235
+ hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
236
+ (the same path `astro sync` would generate for real). Both are gitignored and only ever exist
237
+ inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
238
+ Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
239
+ evolving default) and making the template a real pnpm workspace member (would change how its
240
+ `@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
241
+
242
+ **Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
243
+ `AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
244
+ `.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
245
+ repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
246
+ compose with vite-plus's own `defineConfig`). A type-level shim
247
+ (`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
248
+ runtime `vp test` failure for this one file is still open.
249
+
250
+ - Updated dependencies [65a13cc]
251
+ - Updated dependencies [67e2088]
252
+ - Updated dependencies [89265af]
253
+ - Updated dependencies [0574388]
254
+ - Updated dependencies [c3c1bcb]
255
+ - Updated dependencies [0574388]
256
+ - Updated dependencies [1a8231d]
257
+ - Updated dependencies [779d22e]
258
+ - Updated dependencies [0574388]
259
+ - Updated dependencies [8d8aad7]
260
+ - Updated dependencies [0574388]
261
+ - Updated dependencies [1a3e811]
262
+ - Updated dependencies [0574388]
263
+ - Updated dependencies [dde3df3]
264
+ - Updated dependencies [d9f213b]
265
+ - Updated dependencies [7f4f50e]
266
+ - Updated dependencies [7fd5f8f]
267
+ - Updated dependencies [ceb8310]
268
+ - @getvitops/generator@8.2.0
269
+ - @getvitops/utils@8.2.0
270
+
271
+ ## 8.2.0-alpha.0
272
+
273
+ ### Minor Changes
274
+
275
+ - 0574388: Add `site.crm` — push form submissions into HubSpot, Zoho Bigin, or Jobber.
276
+
277
+ Client sites need form submissions to land in the client's CRM. The options available before this
278
+ — a HubSpot/Bigin/Jobber iframe or JS embed — fail on three counts: the markup can't be
279
+ white-labelled against this design system, nothing can operate on the submission before it leaves
280
+ the page, and the submission never touched `createConversionRoute()`, so `_ac` ad-click attribution
281
+ was lost and no conversion notification was planned.
282
+
283
+ This adds one server-side path for all three CRMs, hung off the conversion route that already
284
+ exists. `createConversionRoute()` gets a new `crm` option (`{ config, credentials, fetch?,
285
+ timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
286
+ option, and are never config. A failed CRM write never fails the request and is folded into the
287
+ notification email's new "Delivery" section, so a broken CRM token is never silent.
288
+
289
+ New `@getvitops/utils/crm` subpath: `deliverToCrm()`, `mapSubmission()`, `deriveLastName()` (Bigin
290
+ requires a last name; this derives one from an explicit field, a full name, or an email address,
291
+ refusing rather than fabricating one), `submitToHubSpot()`, `submitToBigin()`, `submitToJobber()`.
292
+ `identifySubmitter()` was promoted out of `notify`'s private name-guessing cascade into
293
+ `@getvitops/utils/tracking`, since the CRM mapper needed the whole identity (first/last name,
294
+ email, phone, company), not just a display name.
295
+
296
+ Jobber is GraphQL, not REST, and answers `200 OK` for failures — a rejected `clientCreate` returns
297
+ `userErrors[]`, a malformed request returns top-level `errors[]`. `submitToJobber()` classifies on
298
+ the response body, not just the status. Its refresh tokens also rotate by default: **Refresh Token
299
+ Rotation must be turned off** in the app's Developer Center settings, since a Worker reading a
300
+ static `VITOPS_JOBBER_REFRESH_TOKEN` cannot persist a rotated one — a rotated token is detected at
301
+ exchange time and reported with the fix, rather than surfacing an hour later as an opaque 401.
302
+ Attribution and unmapped extra fields render into a best-effort note by default (works on every
303
+ Jobber plan); a resubmitting visitor creates a second Jobber client unless `crm.jobber.dedupe: true`
304
+ is set once its lookup query has been verified against a live account (defaults `false` pending
305
+ that verification — see `jobber.ts`'s docblock).
306
+
307
+ New `site.crm` config block (`provider`, `hubspot`/`bigin`/`jobber`, `fieldMap`, `trackingFields`)
308
+ and two CLI commands: `vitops crm check` (fetches the live HubSpot property list or Bigin field
309
+ list and reports every mapped name that doesn't exist there — HubSpot rejects an unknown field
310
+ outright, Bigin silently drops it; Jobber has nothing live to check yet, since its mapped values
311
+ currently render as note text) and `vitops crm setup` (creates missing HubSpot properties; Bigin
312
+ and Jobber have no such API, so both always print a manual reminder).
313
+
314
+ The generated privacy policy now names HubSpot, Zoho Bigin, or Jobber when `site.crm` configures
315
+ one. Bigin gets the same cross-border-transfer derivation Matomo Cloud already gets: a
316
+ `dataCenter: 'ca'` asserts no transfer outside Canada, any other data centre does. Jobber's
317
+ operator (Octopusapp Inc. d/b/a Jobber) is Canadian, but its own privacy policy names the United
318
+ States as a processing location too, so the policy always asserts a cross-border transfer for
319
+ Jobber regardless of config — a Canadian operator is not read as Canadian-only storage.
320
+
321
+ Credentials: `VITOPS_HUBSPOT_ACCESS_TOKEN` for HubSpot; `VITOPS_ZOHO_CLIENT_ID` /
322
+ `VITOPS_ZOHO_CLIENT_SECRET` / `VITOPS_ZOHO_REFRESH_TOKEN` for Bigin; `VITOPS_JOBBER_CLIENT_ID` /
323
+ `VITOPS_JOBBER_CLIENT_SECRET` / `VITOPS_JOBBER_REFRESH_TOKEN` for Jobber. Never config.
324
+
325
+ No consumer-facing change in `@getvitops/astro` beyond the new `crm` route option and a build
326
+ warning when `site.crm` is configured with no route answering `TRACKING_ENDPOINT` — CSS/Bricks/
327
+ tailwind outputs are unaffected.
328
+
329
+ - 0574388: Add `vitops email setup|check` — connect `site.notifications.email.from` to whether its domain is
330
+ actually onboarded onto an outbound email provider.
331
+
332
+ Before this, nothing checked that. `EMAIL.send()` throws `E_SENDER_NOT_VERIFIED` at runtime with no
333
+ build or deploy-time warning — the failure mode is silent and production-only.
334
+
335
+ `vitops email setup [domain]` onboards a sending domain (currently Cloudflare Email Sending only)
336
+ and polls DNS until its SPF/DKIM/DMARC records resolve or a timeout is reached; `vitops email
337
+ check` is the non-blocking deploy guard both `@getvitops/create` templates' `deploy`/`deploy:dev`
338
+ scripts now run first (`--strict` makes it a real gate; opt out per-site with
339
+ `site.notifications.email.verifySender: false`, or globally with `--skip` /
340
+ `VITOPS_SKIP_EMAIL_CHECK`).
341
+
342
+ Two things worth knowing before running it:
343
+ - **The onboarded entity is an exact name, and it is usually a subdomain**
344
+ (`send.yourdomain.com`), never the apex your `from` address names. When the exact domain isn't
345
+ onboarded but a sibling in the same zone already is, the command reports that as the fix — send
346
+ from the onboarded subdomain — rather than silently trying to onboard the apex, which rewrites
347
+ `_dmarc.<domain>` and can tighten DMARC policy on every other mail your domain sends.
348
+ - Candidates come from `site.notifications.email.from` (or its derived `noreply@<domains
349
+ .canonical>` default) first; a best-effort scan of `--src` only supplements it, and never guesses
350
+ at a computed `from` value.
351
+
352
+ New `@getvitops/utils/email` subpath and `site.notifications.email.verifySender` config field.
353
+ Needs `CLOUDFLARE_API_TOKEN` with an Email Sending scope in addition to the `Zone:Read` other
354
+ commands already use.
355
+
356
+ ### Patch Changes
357
+
358
+ - c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
359
+ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
360
+ './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
361
+ `<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
362
+ offered, now usable by the client directly, no dev server required. An edit is visible
363
+ site-wide immediately (a runtime `:root` override, served via a new plugin API route and
364
+ `page:fragments`) and — once `site.deployment.repo`/`branch` and a `VITOPS_GITHUB_TOKEN`
365
+ secret are set (see the scaffold's `SETUP.html`) — auto-commits to your `dev` branch, so it
366
+ reaches the git-tracked `site.json` through your repo's normal review path rather than
367
+ becoming a second, unreviewed source of truth. Until that's configured, the live half still
368
+ works; only the auto-commit half is skipped (and says so in the admin page). The save route
369
+ runs the exact same accessibility (APCA contrast) gate `generate()` enforces at build time,
370
+ synchronously, before persisting — nothing non-compliant ever goes live even momentarily.
371
+
372
+ Requires `@astrojs/react` + `react` + `react-dom` (already scaffold dependencies, for
373
+ EmDash's own admin) and `@getvitops/core` (added as a new scaffold dependency this release).
374
+ Declares two new plugin capabilities when enabled: `network:request` (for the GitHub
375
+ auto-commit) and `hooks.page-fragments:register` (for the live override) — reviewed once at
376
+ plugin-install time.
377
+
378
+ **Breaking for `@getvitops/emdash`**: `vitopsEmdash()`'s `hooks.page-fragments:register`
379
+ capability is now also granted whenever `designSystemEditor` is set, independent of the
380
+ existing `scripts: 'fragments'` option (which governs something unrelated — web-component
381
+ script delivery). No change if you don't use `designSystemEditor`.
382
+
383
+ Also new, supporting the above:
384
+ - `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
385
+ `/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
386
+ `design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
387
+ standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
388
+ into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
389
+ `mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
390
+ (re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
391
+ subtree" implementation shared by the dev endpoint and the new plugin route. New
392
+ `site.deployment.repo`/`branch` config fields.
393
+ - `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
394
+ wrapper over GitHub's Contents API.
395
+ - `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
396
+ unchanged — `/__vitops/design-system`) so the same element can target the new plugin
397
+ route instead of the dev-only Vite endpoint.
398
+
399
+ - 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
400
+ both projects' GA releases.
401
+
402
+ **Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
403
+ `emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
404
+ `^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
405
+ the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
406
+ code here needed to change, only the declared range.
407
+
408
+ The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
409
+ the version bump:
410
+ - `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
411
+ `css.format: 'tailwind'` without installing either, which threw on build.
412
+ - `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
413
+ `vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
414
+ the build never generated.
415
+ - `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
416
+ `site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
417
+ migration sequence explicitly, rather than relying solely on first-request auto-migration. This
418
+ needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
419
+ which only a human can set — run `emdash migrate --status` against the real deployed database,
420
+ review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
421
+ <value>`. Deploys fail clearly on the migration-apply step until it's set.
422
+ - `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
423
+ not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
424
+ errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
425
+ Uncomment it once scaffolded into its own repo.
426
+
427
+ No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
428
+ to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
429
+ losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
430
+ now names the bad key instead of reporting a bare "Invalid input").
431
+
432
+ Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
433
+ (`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
434
+ has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
435
+ by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
436
+ hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
437
+ `postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
438
+ `node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
439
+ hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
440
+ (the same path `astro sync` would generate for real). Both are gitignored and only ever exist
441
+ inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
442
+ Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
443
+ evolving default) and making the template a real pnpm workspace member (would change how its
444
+ `@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
445
+
446
+ **Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
447
+ `AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
448
+ `.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
449
+ repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
450
+ compose with vite-plus's own `defineConfig`). A type-level shim
451
+ (`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
452
+ runtime `vp test` failure for this one file is still open.
453
+
454
+ - Updated dependencies [65a13cc]
455
+ - Updated dependencies [67e2088]
456
+ - Updated dependencies [0574388]
457
+ - Updated dependencies [c3c1bcb]
458
+ - Updated dependencies [0574388]
459
+ - Updated dependencies [0574388]
460
+ - Updated dependencies [0574388]
461
+ - Updated dependencies [1a3e811]
462
+ - Updated dependencies [0574388]
463
+ - Updated dependencies [dde3df3]
464
+ - Updated dependencies [d9f213b]
465
+ - @getvitops/generator@8.2.0-alpha.0
466
+ - @getvitops/utils@8.2.0-alpha.0
467
+
3
468
  ## 8.1.0
4
469
 
5
470
  ### Minor Changes
@@ -54,7 +519,6 @@
54
519
 
55
520
  **Breaking (`css`/`bricks` and `tailwind` formats — `.lightbox-dialog`'s markup
56
521
  contract changed):**
57
-
58
522
  - `<Gallery />` now emits ONE `<dialog class="lightbox-dialog">` per gallery,
59
523
  wrapping a `<Carousel />`, instead of one dialog per image. A `.gallery__trigger`
60
524
  is now a plain `<a href="#slide-id">`, not a `command="show-modal"` button.
@@ -95,7 +559,6 @@
95
559
  ### Minor Changes
96
560
 
97
561
  - Add Google Business Profile reviews: fetch, cache, render.
98
-
99
562
  - **`vitops reviews`** (new CLI command) fetches a client's GBP reviews (legacy v4 API — the only
100
563
  one that has them) and writes them to `.vitops/reviews.json`, meant to be committed so a build
101
564
  never needs a live Google credential. `--dry` prints the plan; `--check` diffs against the cache
@@ -130,7 +593,6 @@
130
593
  `--site` for now.)
131
594
 
132
595
  **Fixed:**
133
-
134
596
  - `@getvitops/core`'s 12 runtime deps (`lit`, both `@oddbird` polyfills, both
135
597
  `@webcomponents` packages, `colorjs.io`, etc.) were listed as `dependencies` despite
136
598
  being bundled into `dist/elements.js`/`dist/polyfills.js`/`dist/deferred.js` — a
@@ -190,7 +652,7 @@
190
652
  It is **required for a user credential and wrong for a service account**, which is why it is not
191
653
  sent unconditionally. A user credential authenticates through a shared OAuth client that owns no
192
654
  project, so Google refuses the call outright — measured: `403 "requires a quota project, which is
193
- not set by default"`. A service account already belongs to a project, and pointing it at a
655
+ not set by default"`. A service account already belongs to a project, and pointing it at a
194
656
  different one would newly require `serviceusage.services.use` there, turning a working CI
195
657
  credential into a 403 for no benefit.
196
658
 
@@ -215,7 +677,7 @@ not set by default"`. A service account already belongs to a project, and pointi
215
677
  **Fixed: an ADC file in `GOOGLE_APPLICATION_CREDENTIALS` no longer hard-exits.** That variable is
216
678
  Google's own convention _and_ where gcloud writes ADC, so the same variable carries two kinds of
217
679
  credential. Parsing an `authorized_user` file as a service account failed with `service account
218
- JSON is missing client_email or private_key` — while holding a credential the command already knew
680
+ JSON is missing client_email or private_key` — while holding a credential the command already knew
219
681
  how to use. It is now discriminated on `type`, the field Google's own libraries switch on.
220
682
 
221
683
  **Also fixed — `search setup --dry` and `--check` are not offline.** The docs implied otherwise. The
@@ -906,7 +1368,7 @@ carousel vocabulary, because they render live from the generator.
906
1368
  Also new, and worth wiring at the same time — **`gitLastmod()` for real sitemap dates**:
907
1369
 
908
1370
  ```js
909
- import vitops, { gitLastmod } from "@getvitops/astro";
1371
+ import vitops, { gitLastmod } from '@getvitops/astro';
910
1372
  vitops({ sitemap: { serialize: await gitLastmod() } });
911
1373
  ```
912
1374
 
@@ -972,7 +1434,7 @@ carousel vocabulary, because they render live from the generator.
972
1434
  toolchain bump. Use `--force` when you mean to re-encode.
973
1435
 
974
1436
  **`ffmpeg` is an external tool, not an npm dependency** — install it yourself (`brew install
975
- ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails without it rather
1437
+ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails without it rather
976
1438
  than skipping: a page referencing a video that was never encoded is broken, not degraded.
977
1439
 
978
1440
  Two defaults worth knowing about. The MP4 exists because older iOS and the in-app webviews inside
@@ -1000,7 +1462,7 @@ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails
1000
1462
 
1001
1463
  ```js
1002
1464
  // astro.config.mjs
1003
- getvitops({ css: { input: "company.json", format: "css" } });
1465
+ getvitops({ css: { input: 'company.json', format: 'css' } });
1004
1466
  ```
1005
1467
 
1006
1468
  ```
@@ -1487,15 +1949,15 @@ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails
1487
1949
  which `@getvitops/astro` re-exports wholesale. So from `astro.config.mjs`:
1488
1950
 
1489
1951
  ```js
1490
- import { generateIconInclude } from "@getvitops/astro";
1491
- import icon from "astro-icon";
1952
+ import { generateIconInclude } from '@getvitops/astro';
1953
+ import icon from 'astro-icon';
1492
1954
 
1493
1955
  integrations: [
1494
1956
  icon({
1495
1957
  include: generateIconInclude({
1496
- ui: "fa7-solid",
1497
- brand: "simple-icons",
1498
- semantic: ["menu", "close", "search", "github"],
1958
+ ui: 'fa7-solid',
1959
+ brand: 'simple-icons',
1960
+ semantic: ['menu', 'close', 'search', 'github'],
1499
1961
  }),
1500
1962
  }),
1501
1963
  ];
@@ -1640,7 +2102,7 @@ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails
1640
2102
  - `.chip-list__chip` and `.chip-list__chip-remove` are **removed**. A tag list is a list of tags, so
1641
2103
  its items are now the existing `.tag` / `.tag__remove` — which also means they pick up tag role
1642
2104
  variants. **Migration:** replace `<span class="chip-list__chip">x <button
1643
- class="chip-list__chip-remove">` with `<span class="tag">x <button class="tag__remove">`. Note the
2105
+ class="chip-list__chip-remove">` with `<span class="tag">x <button class="tag__remove">`. Note the
1644
2106
  items change appearance: `.tag` is outlined (border + neutral text) where the old chip was filled
1645
2107
  with `--color-surface-muted`.
1646
2108
  - The `radii.chip` primitive is **removed** from the example config; it was only ever an alias of
package/dist/cli.d.mts CHANGED
@@ -1 +1 @@
1
- export { };
1
+ export {}