@getvitops/cli 8.2.0-alpha.0 → 8.3.0-alpha.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,342 @@
1
1
  # @getvitops/cli
2
2
 
3
+ ## 8.3.0-alpha.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 161c7f3: A contact form that works end to end: `vitops setup turnstile`, `<wc-turnstile>`, the `vitops.form` EmDash block, and the route the emdash template ships to receive it.
8
+
9
+ **CSS, all formats** (no existing class changes): `forms.css` gains `.form-notice--target:not(:target)` (a message revealed only when the URL fragment names it, which is how a prerendered page shows a no-JS form's result) and `:where(wc-turnstile) { display: block }`.
10
+
11
+ **`@getvitops/core`:** new `<wc-turnstile sitekey="…">` in `elements.js`. It loads Cloudflare Turnstile on the first interaction with its form (no request to `challenges.cloudflare.com` for a visitor who only reads the page), holds an early submit until the token arrives, and leaves its slotted no-JS explanation in place until JS takes over. Turnstile is category `necessary`, so it is not behind the consent banner.
12
+
13
+ **`@getvitops/utils` / `@getvitops/cli`:** new `@getvitops/utils/turnstile` and `vitops setup turnstile [--dry] [--check] [--no-write] [--no-secret]`. It plans from the site's hostnames, creates a managed widget unless one already covers them (an existing widget is adopted, never edited, rotated or deleted), writes the public key to `site.security.turnstile.siteKey`, and pipes the one-time secret to `wrangler secret put VITOPS_TURNSTILE_SECRET` without writing it to any file. Needs `CLOUDFLARE_API_TOKEN` with **Account > Turnstile > Edit** and `CLOUDFLARE_ACCOUNT_ID`.
14
+
15
+ **`@getvitops/astro`:** `virtual:getvitops/head` gains `turnstileSiteKey` (read from `site.security.turnstile.siteKey`) so blocks can render the widget. No other change.
16
+
17
+ **`@getvitops/generator`:** `TIERS` gains a `turnstile` entry (wc-turnstile, claimed by `vitops.form`); `wc-multi-field`'s `emdashExempt` reason is reworded now the form block exists.
18
+
19
+ **`@getvitops/emdash`:** new `vitops.form` block: a field repeater (text, email, phone, long text, choose one, tick box), required flags, help text, success/error messages, optional Turnstile. It renders a semantic `<form method="post">` that works with no JavaScript, with a honeypot named per `createConversionRoute`'s defaults. A field named `phone` submits as `tel`, because the route reserves `phone` and `event`.
20
+
21
+ **`@getvitops/create`:** the emdash template ships `src/pages/api/track.ts` (`prerender = false`, `createConversionRoute()`), a seeded contact form, and `challenges.cloudflare.com` in both templates' Report-Only CSP (`script-src`, `frame-src`). The route refuses a post with nowhere to deliver and fails closed when a site key exists without `VITOPS_TURNSTILE_SECRET`. SETUP.html gains the Turnstile step. Existing sites: copy the route and the CSP change, then run `vitops setup turnstile`.
22
+
23
+ - fec1b4b: The EmDash template's two build scripts are now CLI commands, so fixes reach existing sites with `vp update` instead of a re-copy.
24
+
25
+ - `vitops emdash prepare-db` migrates and seeds the local D1 the build prerenders from (was `scripts/prepare-local-db.mjs`). The start and stop calls time out after 5 minutes and it prints progress.
26
+ - `vitops emdash check-prerender [dir]` fails the build if a prerendered page is not a real page (was `scripts/check-prerender.mjs`).
27
+
28
+ **Migration for a site scaffolded earlier:** delete `scripts/prepare-local-db.mjs` and `scripts/check-prerender.mjs`, then in `package.json` set `"prepare-local-db": "vitops emdash prepare-db"`, `"build": "astro build && vitops emdash check-prerender"`, and use the same two commands in place of `node scripts/…` inside `deploy` and `deploy:dev`. A site that never had the scripts needs only the `package.json` entries.
29
+
30
+ - b47df4d: **Added: `vitops audit` — Lighthouse + axe budgets over a built site.** Non-breaking, opt-in.
31
+
32
+ `vitops audit [--build] [--url <base>] [--pages /a,/b] [--warn] [--out <dir>]` serves the existing build with `astro preview` on a dedicated port (4398; it aborts rather than attach to something else already there), audits `/` plus `/privacy`, `/terms`, `/cookies`, `/contact` and every link in `seed/seed.json`'s `primary*`/`footer*` menus, and skips any route that 404s (a 404 on `/` fails the run). Per page it runs axe (tags wcag2a, wcag2aa, wcag21a, wcag21aa) and Lighthouse on the same Chromium. Default budgets: LCP < 2500 ms, CLS < 0.1, TBT < 200 ms (the INP proxy), no serious or critical axe violations; override with `--lcp`, `--cls`, `--tbt`, `--impact`. Writes `report.json`, `report.md` and `pages/<page>.{json,md}` to `.vitops/audit/`; exits non-zero on failure unless `--warn`. Every report states that a pass is **not** AODA certification and lists the manual checks.
33
+
34
+ `playwright`, `@axe-core/playwright` and `lighthouse` are **optional peer dependencies** of `@getvitops/utils`, loaded lazily — nothing is installed or loaded unless you run the command, and a missing one fails with the install command. Pure planner and report rendering are `@getvitops/utils/audit` (new subpath).
35
+
36
+ CI: the reusable `site-ci.yml` gains opt-in `run-audit` (default `false`) and `audit-args` inputs; callers pinned to `@v1` that pass neither are unaffected.
37
+
38
+ ### Patch Changes
39
+
40
+ - 224a731: **`@getvitops/astro` (no CSS change; all formats unaffected)**
41
+
42
+ - **`site.fonts` is no longer silently ignored.** `vitops()` now turns webfonts on by default when the site config (`site` option, or a `css.input` that is a site config) declares `site.fonts`, and logs that it did. Before, no `@font-face`, preload or warning was emitted unless you also passed `fonts: true`. Pass `fonts: false` to opt out. Migration: none — sites that already set `fonts` behave the same; sites that declared `site.fonts` without it will start loading those families.
43
+ - **The icon sprite is served when the site config asks for it.** `site.icons.sprite: true` made the generator write `icons.svg`, but only the `icons` integration option copied it to `public/vitops/`, so `/vitops/icons.svg` was never served. It is now mirrored whenever the site config sets it.
44
+ - **Strict CSP is achievable.** New build-time exports `inlineScriptHashes()`, `cspScriptHash()` and `contentSecurityPolicy()` give the SHA-256 hashes of the two inline scripts `<Head />` emits (colour-scheme bootstrap, consent-gate stub) and a starting policy built on them, with no `'unsafe-inline'` in `script-src`. The scripts moved from `Head.astro`'s frontmatter to `src/inline-scripts.ts` (shipped in the package) so the hash and the emitted bytes are the same constant; the rendered output is byte-identical.
45
+
46
+ **`@getvitops/create` (templates; existing scaffolded sites are not changed)**
47
+
48
+ - **emdash: every public page is prerendered.** `index.astro`, `[...slug].astro` (via `getStaticPaths` over the published `pages`) and `404.astro` set `prerender = true`. Unknown URLs now return the prerendered 404 with a real 404 status — the old `Astro.rewrite('/404')` from an on-demand route to a prerendered one was refused by Astro, so every unknown URL was a 500. `scripts/pull-content.mjs` (first step of `deploy`/`deploy:dev`) loads production's D1 into the local build database, because the Cloudflare adapter prerenders against a local D1 that is empty on CI; it fails closed. The deploy-prod workflow also runs on a `repository_dispatch` of type `emdash-publish`. Migration for an existing site: copy those pages, the script and the two `package.json` scripts, and add the trigger; previews of unpublished drafts no longer work on the public routes (see AGENTS.md).
49
+ - **emdash: seed menus are `primary` (Contact), `footer-sitemap` (Home, Contact) and `footer-legal` (Privacy Policy, Terms & Conditions, Cookie Notice)**, replacing `primary` (Home, About) and `footer` (Terms, Privacy), with placeholder `contact` and `cookies` pages so every seeded link resolves. The template Layout still reads the old menu names until it is updated; the seed only applies to an empty database, so existing sites keep their menus.
50
+ - **Both templates: `badge` and `tag` patterns (group `label`) are enabled, `button` carries `class: "btn"` so `<a class="btn">` is styled, and `neutral`/`surface` are declared as surface roles** (they were chromatic, which emitted no `.bg-neutral`/`text-neutral-muted`). css/bricks/tailwind output of a scaffolded site changes accordingly.
51
+ - **Both templates ship `public/_headers` with a `Content-Security-Policy-Report-Only` default** naming `<Head />`'s inline scripts by hash. It blocks nothing. `_headers` applies to static assets only, not Worker-rendered responses; see AGENTS.md for the EmDash admin and HTML-block trade-off.
52
+ - **Guidance is Vite+-first**: `AGENTS.md`, README, the `setup` skill, `SETUP.html` and the session hook use `vp`/`vpr`/`vp exec` instead of `pnpm`/`npx`, with one command table; `vpr dev` is documented as returning immediately. There is no `vp change`/`vp pm change` (checked against vp 1.5.0); the release flow is `dev` → promote.
53
+ - emdash ships `.agents/skills/custom-blocks/SKILL.md` (compose, extend, site-local, upstream; naming and lifecycle rules) and the favicon `themeColor`/`name` now derive from `site.json`.
54
+ - Label vocabulary: `badge` is a static label, `tag` is static or dynamic; `chip`/`pill` are not used.
55
+
56
+ **`@getvitops/cli`**: the bundled `vitops-design-system` skill points at `vp exec vitops docs <topic>`.
57
+
58
+ No consumer-facing change in `@getvitops/core`, `@getvitops/generator`, `@getvitops/utils` or `@getvitops/vite`.
59
+
60
+ - Updated dependencies [93d2067]
61
+ - Updated dependencies [161c7f3]
62
+ - Updated dependencies [175d16e]
63
+ - Updated dependencies [83d6f3c]
64
+ - Updated dependencies [892b204]
65
+ - Updated dependencies [06d6112]
66
+ - Updated dependencies [6297337]
67
+ - Updated dependencies [55061e2]
68
+ - Updated dependencies [b47df4d]
69
+ - @getvitops/generator@8.3.0-alpha.0
70
+ - @getvitops/utils@8.3.0-alpha.0
71
+
72
+ ## 8.2.0
73
+
74
+ ### Minor Changes
75
+
76
+ - 0574388: Add `site.crm` — push form submissions into HubSpot, Zoho Bigin, or Jobber.
77
+
78
+ Client sites need form submissions to land in the client's CRM. The options available before this
79
+ — a HubSpot/Bigin/Jobber iframe or JS embed — fail on three counts: the markup can't be
80
+ white-labelled against this design system, nothing can operate on the submission before it leaves
81
+ the page, and the submission never touched `createConversionRoute()`, so `_ac` ad-click attribution
82
+ was lost and no conversion notification was planned.
83
+
84
+ This adds one server-side path for all three CRMs, hung off the conversion route that already
85
+ exists. `createConversionRoute()` gets a new `crm` option (`{ config, credentials, fetch?,
86
+ timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
87
+ option, and are never config. A failed CRM write never fails the request and is folded into the
88
+ notification email's new "Delivery" section, so a broken CRM token is never silent.
89
+
90
+ New `@getvitops/utils/crm` subpath: `deliverToCrm()`, `mapSubmission()`, `deriveLastName()` (Bigin
91
+ requires a last name; this derives one from an explicit field, a full name, or an email address,
92
+ refusing rather than fabricating one), `submitToHubSpot()`, `submitToBigin()`, `submitToJobber()`.
93
+ `identifySubmitter()` was promoted out of `notify`'s private name-guessing cascade into
94
+ `@getvitops/utils/tracking`, since the CRM mapper needed the whole identity (first/last name,
95
+ email, phone, company), not just a display name.
96
+
97
+ Jobber is GraphQL, not REST, and answers `200 OK` for failures — a rejected `clientCreate` returns
98
+ `userErrors[]`, a malformed request returns top-level `errors[]`. `submitToJobber()` classifies on
99
+ the response body, not just the status. Its refresh tokens also rotate by default: **Refresh Token
100
+ Rotation must be turned off** in the app's Developer Center settings, since a Worker reading a
101
+ static `VITOPS_JOBBER_REFRESH_TOKEN` cannot persist a rotated one — a rotated token is detected at
102
+ exchange time and reported with the fix, rather than surfacing an hour later as an opaque 401.
103
+ Attribution and unmapped extra fields render into a best-effort note by default (works on every
104
+ Jobber plan); a resubmitting visitor creates a second Jobber client unless `crm.jobber.dedupe: true`
105
+ is set once its lookup query has been verified against a live account (defaults `false` pending
106
+ that verification — see `jobber.ts`'s docblock).
107
+
108
+ New `site.crm` config block (`provider`, `hubspot`/`bigin`/`jobber`, `fieldMap`, `trackingFields`)
109
+ and two CLI commands: `vitops crm check` (fetches the live HubSpot property list or Bigin field
110
+ list and reports every mapped name that doesn't exist there — HubSpot rejects an unknown field
111
+ outright, Bigin silently drops it; Jobber has nothing live to check yet, since its mapped values
112
+ currently render as note text) and `vitops crm setup` (creates missing HubSpot properties; Bigin
113
+ and Jobber have no such API, so both always print a manual reminder).
114
+
115
+ The generated privacy policy now names HubSpot, Zoho Bigin, or Jobber when `site.crm` configures
116
+ one. Bigin gets the same cross-border-transfer derivation Matomo Cloud already gets: a
117
+ `dataCenter: 'ca'` asserts no transfer outside Canada, any other data centre does. Jobber's
118
+ operator (Octopusapp Inc. d/b/a Jobber) is Canadian, but its own privacy policy names the United
119
+ States as a processing location too, so the policy always asserts a cross-border transfer for
120
+ Jobber regardless of config — a Canadian operator is not read as Canadian-only storage.
121
+
122
+ Credentials: `VITOPS_HUBSPOT_ACCESS_TOKEN` for HubSpot; `VITOPS_ZOHO_CLIENT_ID` /
123
+ `VITOPS_ZOHO_CLIENT_SECRET` / `VITOPS_ZOHO_REFRESH_TOKEN` for Bigin; `VITOPS_JOBBER_CLIENT_ID` /
124
+ `VITOPS_JOBBER_CLIENT_SECRET` / `VITOPS_JOBBER_REFRESH_TOKEN` for Jobber. Never config.
125
+
126
+ No consumer-facing change in `@getvitops/astro` beyond the new `crm` route option and a build
127
+ warning when `site.crm` is configured with no route answering `TRACKING_ENDPOINT` — CSS/Bricks/
128
+ tailwind outputs are unaffected.
129
+
130
+ - 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.
131
+ - 0574388: Add `vitops email setup|check` — connect `site.notifications.email.from` to whether its domain is
132
+ actually onboarded onto an outbound email provider.
133
+
134
+ Before this, nothing checked that. `EMAIL.send()` throws `E_SENDER_NOT_VERIFIED` at runtime with no
135
+ build or deploy-time warning — the failure mode is silent and production-only.
136
+
137
+ `vitops email setup [domain]` onboards a sending domain (currently Cloudflare Email Sending only)
138
+ and polls DNS until its SPF/DKIM/DMARC records resolve or a timeout is reached; `vitops email
139
+ check` is the non-blocking deploy guard both `@getvitops/create` templates' `deploy`/`deploy:dev`
140
+ scripts now run first (`--strict` makes it a real gate; opt out per-site with
141
+ `site.notifications.email.verifySender: false`, or globally with `--skip` /
142
+ `VITOPS_SKIP_EMAIL_CHECK`).
143
+
144
+ Two things worth knowing before running it:
145
+
146
+ - **The onboarded entity is an exact name, and it is usually a subdomain**
147
+ (`send.yourdomain.com`), never the apex your `from` address names. When the exact domain isn't
148
+ onboarded but a sibling in the same zone already is, the command reports that as the fix — send
149
+ from the onboarded subdomain — rather than silently trying to onboard the apex, which rewrites
150
+ `_dmarc.<domain>` and can tighten DMARC policy on every other mail your domain sends.
151
+ - Candidates come from `site.notifications.email.from` (or its derived `noreply@<domains
152
+ .canonical>` default) first; a best-effort scan of `--src` only supplements it, and never guesses
153
+ at a computed `from` value.
154
+
155
+ New `@getvitops/utils/email` subpath and `site.notifications.email.verifySender` config field.
156
+ Needs `CLOUDFLARE_API_TOKEN` with an Email Sending scope in addition to the `Zone:Read` other
157
+ commands already use.
158
+
159
+ - 8d8aad7: **New: server-side Google Ads offline conversion upload.** Set
160
+ `site.ads.google.reporting: "server"` and a matching
161
+ `conversionActionId` (a numeric id or full
162
+ `customers/…/conversionActions/…` resource name — **not** the same value
163
+ as `conversionLabel`, which is the opaque label the web tag uses and
164
+ which the Ads API's upload endpoint doesn't accept) and
165
+ `createConversionRoute`'s new `googleAdsOffline` option uploads the
166
+ conversion via `ConversionUploadService.UploadClickConversions` instead
167
+ of relying on the visitor's browser to fire `gtag.js`. An action with no
168
+ matching `conversionActionId` keeps firing client-side regardless of
169
+ `reporting`, so turning it on before the id is filled in never silently
170
+ drops a conversion. `@getvitops/astro/routes` exports a matching
171
+ `toGoogleAdsConfig(config)` adapter, alongside `toCrmConfig`.
172
+
173
+ Structurally typed against Google's documented REST shape, not verified
174
+ against a live Ads account — no developer-token/MCC access exists
175
+ anywhere in this toolchain's own setup to test against. Wrapped so a
176
+ wrong guess degrades to a stated failure reason (folded into the
177
+ conversion email's "Delivery" section, same as a failed CRM write) rather
178
+ than breaking, the same caveat `@getvitops/utils/crm`'s Jobber executor
179
+ already carries for its own unverified surfaces.
180
+
181
+ `vitops ads lint` now flags a `reporting: "server"` property whose
182
+ configured actions have no matching `conversionActionId` — not a
183
+ failure, since that action keeps firing client-side, but worth surfacing
184
+ rather than leaving someone to wonder why nothing changed.
185
+
186
+ Not built: `vitops ads provision` (upserting conversion actions via
187
+ `ConversionActionService` and printing back both the label and the
188
+ resource id) — same credential blocker, and deliberately left unscoped
189
+ until there's a live account to verify the request/response shape
190
+ against.
191
+
192
+ ### Patch Changes
193
+
194
+ - 89265af: **New package: `@getvitops/config`** — the site-config provider blocks (`site.crm`, `site.ads`,
195
+ `site.reviews`, `site.seo.indexing`, `site.searchConsole`, `site.notifications`, `site.tracking`,
196
+ `site.security`, `site.deployment`) as zod schemas and inferred types, with the closed provider lists
197
+ (`AD_PROVIDERS`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`) on a zod-free `./providers` entry. It joins the
198
+ `fixed` group.
199
+
200
+ Why: `@getvitops/utils` holds the provider code but could not import its configuration types — the schema
201
+ lived in `@getvitops/generator`, a layer above — so it kept a hand-copied mirror beside each module,
202
+ an adapter converting into it, and a test holding the copy to the schema. The mirrors are gone.
203
+
204
+ - **`@getvitops/generator`**: `AD_PROVIDERS`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`, `AdProvider` and
205
+ `CrmProviderConfig` are re-exported unchanged. No import path changes.
206
+ - **`@getvitops/utils`**: the config types in `./crm`, `./ads`, `./indexing`, `./onboarding`, `./reviews`,
207
+ `./tracking` and `./notify` (`CrmConfig`, `HubSpotConfig`, `BiginConfig`, `JobberConfig`,
208
+ `IndexNowConfig`, `SearchConsoleConfig`, `ReviewsConfig`, `ReviewLocationConfig`, `TrackingConfig`,
209
+ `NotificationsConfig`, `EmailChannelConfig`, `DomainSetup`, `AdPropertySetup`) are now inferred from the
210
+ schema. Same names, same fields, but **optional fields now follow zod's inference**, so a hand-written
211
+ object literal typed against them may need `| undefined` handled. `AdPropertySetup` gains the fields the
212
+ schema always had (`conversionActionId`, `reporting`).
213
+ - **`@getvitops/astro`**: `GetvitopsAdProperty` and `GetvitopsTrackingOptions` are aliases of the schema
214
+ types. `toCrmConfig()` and `toGoogleAdsConfig()` are **deprecated** — they returned their argument's
215
+ field unchanged; pass `config.site.crm` / `config.site.ads?.google` directly.
216
+ - **`@getvitops/cli`**: `vitops ads` now passes `conversionActionId` and `reporting` through to the planner;
217
+ it previously copied a hand-picked subset of the block and dropped them.
218
+
219
+ Formats reached: none — no CSS change.
220
+
221
+ - c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
222
+ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
223
+ './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
224
+ `<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
225
+ offered, now usable by the client directly, no dev server required. An edit is visible
226
+ site-wide immediately (a runtime `:root` override, served via a new plugin API route and
227
+ `page:fragments`) and — once `site.deployment.repo`/`branch` and a `VITOPS_GITHUB_TOKEN`
228
+ secret are set (see the scaffold's `SETUP.html`) — auto-commits to your `dev` branch, so it
229
+ reaches the git-tracked `site.json` through your repo's normal review path rather than
230
+ becoming a second, unreviewed source of truth. Until that's configured, the live half still
231
+ works; only the auto-commit half is skipped (and says so in the admin page). The save route
232
+ runs the exact same accessibility (APCA contrast) gate `generate()` enforces at build time,
233
+ synchronously, before persisting — nothing non-compliant ever goes live even momentarily.
234
+
235
+ Requires `@astrojs/react` + `react` + `react-dom` (already scaffold dependencies, for
236
+ EmDash's own admin) and `@getvitops/core` (added as a new scaffold dependency this release).
237
+ Declares two new plugin capabilities when enabled: `network:request` (for the GitHub
238
+ auto-commit) and `hooks.page-fragments:register` (for the live override) — reviewed once at
239
+ plugin-install time.
240
+
241
+ **Breaking for `@getvitops/emdash`**: `vitopsEmdash()`'s `hooks.page-fragments:register`
242
+ capability is now also granted whenever `designSystemEditor` is set, independent of the
243
+ existing `scripts: 'fragments'` option (which governs something unrelated — web-component
244
+ script delivery). No change if you don't use `designSystemEditor`.
245
+
246
+ Also new, supporting the above:
247
+
248
+ - `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
249
+ `/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
250
+ `design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
251
+ standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
252
+ into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
253
+ `mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
254
+ (re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
255
+ subtree" implementation shared by the dev endpoint and the new plugin route. New
256
+ `site.deployment.repo`/`branch` config fields.
257
+ - `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
258
+ wrapper over GitHub's Contents API.
259
+ - `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
260
+ unchanged — `/__vitops/design-system`) so the same element can target the new plugin
261
+ route instead of the dev-only Vite endpoint.
262
+
263
+ - 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
264
+ both projects' GA releases.
265
+
266
+ **Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
267
+ `emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
268
+ `^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
269
+ the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
270
+ code here needed to change, only the declared range.
271
+
272
+ The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
273
+ the version bump:
274
+
275
+ - `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
276
+ `css.format: 'tailwind'` without installing either, which threw on build.
277
+ - `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
278
+ `vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
279
+ the build never generated.
280
+ - `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
281
+ `site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
282
+ migration sequence explicitly, rather than relying solely on first-request auto-migration. This
283
+ needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
284
+ which only a human can set — run `emdash migrate --status` against the real deployed database,
285
+ review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
286
+ <value>`. Deploys fail clearly on the migration-apply step until it's set.
287
+ - `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
288
+ not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
289
+ errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
290
+ Uncomment it once scaffolded into its own repo.
291
+
292
+ No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
293
+ to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
294
+ losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
295
+ now names the bad key instead of reporting a bare "Invalid input").
296
+
297
+ Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
298
+ (`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
299
+ has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
300
+ by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
301
+ hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
302
+ `postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
303
+ `node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
304
+ hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
305
+ (the same path `astro sync` would generate for real). Both are gitignored and only ever exist
306
+ inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
307
+ Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
308
+ evolving default) and making the template a real pnpm workspace member (would change how its
309
+ `@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
310
+
311
+ **Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
312
+ `AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
313
+ `.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
314
+ repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
315
+ compose with vite-plus's own `defineConfig`). A type-level shim
316
+ (`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
317
+ runtime `vp test` failure for this one file is still open.
318
+
319
+ - Updated dependencies [65a13cc]
320
+ - Updated dependencies [67e2088]
321
+ - Updated dependencies [89265af]
322
+ - Updated dependencies [0574388]
323
+ - Updated dependencies [c3c1bcb]
324
+ - Updated dependencies [0574388]
325
+ - Updated dependencies [1a8231d]
326
+ - Updated dependencies [779d22e]
327
+ - Updated dependencies [0574388]
328
+ - Updated dependencies [8d8aad7]
329
+ - Updated dependencies [0574388]
330
+ - Updated dependencies [1a3e811]
331
+ - Updated dependencies [0574388]
332
+ - Updated dependencies [dde3df3]
333
+ - Updated dependencies [d9f213b]
334
+ - Updated dependencies [7f4f50e]
335
+ - Updated dependencies [7fd5f8f]
336
+ - Updated dependencies [ceb8310]
337
+ - @getvitops/generator@8.2.0
338
+ - @getvitops/utils@8.2.0
339
+
3
340
  ## 8.2.0-alpha.0
4
341
 
5
342
  ### Minor Changes
@@ -14,7 +351,7 @@
14
351
 
15
352
  This adds one server-side path for all three CRMs, hung off the conversion route that already
16
353
  exists. `createConversionRoute()` gets a new `crm` option (`{ config, credentials, fetch?,
17
- timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
354
+ timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
18
355
  option, and are never config. A failed CRM write never fails the request and is folded into the
19
356
  notification email's new "Delivery" section, so a broken CRM token is never silent.
20
357
 
@@ -66,7 +403,7 @@ timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the e
66
403
 
67
404
  `vitops email setup [domain]` onboards a sending domain (currently Cloudflare Email Sending only)
68
405
  and polls DNS until its SPF/DKIM/DMARC records resolve or a timeout is reached; `vitops email
69
- check` is the non-blocking deploy guard both `@getvitops/create` templates' `deploy`/`deploy:dev`
406
+ check` is the non-blocking deploy guard both `@getvitops/create` templates' `deploy`/`deploy:dev`
70
407
  scripts now run first (`--strict` makes it a real gate; opt out per-site with
71
408
  `site.notifications.email.verifySender: false`, or globally with `--skip` /
72
409
  `VITOPS_SKIP_EMAIL_CHECK`).
@@ -78,7 +415,7 @@ check` is the non-blocking deploy guard both `@getvitops/create` templates' `dep
78
415
  from the onboarded subdomain — rather than silently trying to onboard the apex, which rewrites
79
416
  `_dmarc.<domain>` and can tighten DMARC policy on every other mail your domain sends.
80
417
  - Candidates come from `site.notifications.email.from` (or its derived `noreply@<domains
81
- .canonical>` default) first; a best-effort scan of `--src` only supplements it, and never guesses
418
+ .canonical>` default) first; a best-effort scan of `--src` only supplements it, and never guesses
82
419
  at a computed `from` value.
83
420
 
84
421
  New `@getvitops/utils/email` subpath and `site.notifications.email.verifySender` config field.
@@ -88,8 +425,8 @@ check` is the non-blocking deploy guard both `@getvitops/create` templates' `dep
88
425
  ### Patch Changes
89
426
 
90
427
  - c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
91
- designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
92
- './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
428
+ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
429
+ './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
93
430
  `<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
94
431
  offered, now usable by the client directly, no dev server required. An edit is visible
95
432
  site-wide immediately (a runtime `:root` override, served via a new plugin API route and
@@ -150,7 +487,7 @@ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import c
150
487
  needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
151
488
  which only a human can set — run `emdash migrate --status` against the real deployed database,
152
489
  review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
153
- <value>`. Deploys fail clearly on the migration-apply step until it's set.
490
+ <value>`. Deploys fail clearly on the migration-apply step until it's set.
154
491
  - `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
155
492
  not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
156
493
  errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
@@ -384,7 +721,7 @@ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import c
384
721
  It is **required for a user credential and wrong for a service account**, which is why it is not
385
722
  sent unconditionally. A user credential authenticates through a shared OAuth client that owns no
386
723
  project, so Google refuses the call outright — measured: `403 "requires a quota project, which is
387
- not set by default"`. A service account already belongs to a project, and pointing it at a
724
+ not set by default"`. A service account already belongs to a project, and pointing it at a
388
725
  different one would newly require `serviceusage.services.use` there, turning a working CI
389
726
  credential into a 403 for no benefit.
390
727
 
@@ -409,7 +746,7 @@ not set by default"`. A service account already belongs to a project, and pointi
409
746
  **Fixed: an ADC file in `GOOGLE_APPLICATION_CREDENTIALS` no longer hard-exits.** That variable is
410
747
  Google's own convention _and_ where gcloud writes ADC, so the same variable carries two kinds of
411
748
  credential. Parsing an `authorized_user` file as a service account failed with `service account
412
- JSON is missing client_email or private_key` — while holding a credential the command already knew
749
+ JSON is missing client_email or private_key` — while holding a credential the command already knew
413
750
  how to use. It is now discriminated on `type`, the field Google's own libraries switch on.
414
751
 
415
752
  **Also fixed — `search setup --dry` and `--check` are not offline.** The docs implied otherwise. The
@@ -1011,28 +1348,20 @@ carousel vocabulary, because they render live from the generator.
1011
1348
  {
1012
1349
  "designSystem": {
1013
1350
  "themes": {
1014
- "default": {
1015
- /* … */
1016
- },
1351
+ "default": {/* … */},
1017
1352
  },
1018
1353
  },
1019
1354
  "organization": {
1020
1355
  "name": "Acme",
1021
1356
  "contact": "hq",
1022
1357
  "locations": {
1023
- "hq": {
1024
- /* … */
1025
- },
1358
+ "hq": {/* … */},
1026
1359
  },
1027
1360
  },
1028
1361
  "site": {
1029
1362
  "defaultLocale": "en",
1030
- "domains": {
1031
- /* … */
1032
- },
1033
- "analytics": {
1034
- /* … */
1035
- },
1363
+ "domains": {/* … */},
1364
+ "analytics": {/* … */},
1036
1365
  },
1037
1366
  }
1038
1367
  ```
@@ -1174,7 +1503,7 @@ carousel vocabulary, because they render live from the generator.
1174
1503
  toolchain bump. Use `--force` when you mean to re-encode.
1175
1504
 
1176
1505
  **`ffmpeg` is an external tool, not an npm dependency** — install it yourself (`brew install
1177
- ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails without it rather
1506
+ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails without it rather
1178
1507
  than skipping: a page referencing a video that was never encoded is broken, not degraded.
1179
1508
 
1180
1509
  Two defaults worth knowing about. The MP4 exists because older iOS and the in-app webviews inside
@@ -1842,7 +2171,7 @@ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails
1842
2171
  - `.chip-list__chip` and `.chip-list__chip-remove` are **removed**. A tag list is a list of tags, so
1843
2172
  its items are now the existing `.tag` / `.tag__remove` — which also means they pick up tag role
1844
2173
  variants. **Migration:** replace `<span class="chip-list__chip">x <button
1845
- class="chip-list__chip-remove">` with `<span class="tag">x <button class="tag__remove">`. Note the
2174
+ class="chip-list__chip-remove">` with `<span class="tag">x <button class="tag__remove">`. Note the
1846
2175
  items change appearance: `.tag` is outlined (border + neutral text) where the old chip was filled
1847
2176
  with `--color-surface-muted`.
1848
2177
  - The `radii.chip` primitive is **removed** from the example config; it was only ever an alias of