@getvitops/cli 1.0.0 → 2.1.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,340 @@
1
1
  # @getvitops/cli
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 20e518e: Add `vitops indexing` — tell search engines about a deploy, instead of opening Search Console by hand.
8
+
9
+ Configure it in your site config under `seo.indexing`, then run it after deploying:
10
+
11
+ ```jsonc
12
+ "seo": {
13
+ "indexing": {
14
+ "indexNow": { "key": "…" },
15
+ "searchConsole": { "siteUrl": "sc-domain:acme.ca" },
16
+ "priorityUrls": ["https://acme.ca/", "https://acme.ca/services"]
17
+ }
18
+ }
19
+ ```
20
+
21
+ ```
22
+ vitops indexing --dry # print the plan, make no requests
23
+ vitops indexing # submit
24
+ vitops indexing --check # a day or two later: did Google actually index them?
25
+ ```
26
+
27
+ It reads your sitemap, diffs it against the previous run, and submits only what changed —
28
+ pinging **IndexNow** and re-submitting your sitemap through the **Search Console API**.
29
+ `--check` inspects `priorityUrls` and exits non-zero on a page Google hasn't indexed, so a
30
+ scheduled CI job can catch a page that quietly fell out of the index.
31
+
32
+ **Be clear-eyed about what this can do.** Google exposes no API that requests indexing — the
33
+ button in Search Console isn't available anywhere, URL Inspection is read-only, and the sitemap
34
+ ping endpoint was removed in 2023. IndexNow reaches Bing, Yandex, Naver, Seznam and Yep; Google
35
+ doesn't participate. So this automates every sanctioned step and then verifies the outcome; it
36
+ does not make Google re-index on demand, and nothing can. Google's Indexing API is deliberately
37
+ not wired: it's scoped to job postings and livestreams, and using it for ordinary pages violates
38
+ its terms.
39
+
40
+ Also new, and worth wiring at the same time — **`gitLastmod()` for real sitemap dates**:
41
+
42
+ ```js
43
+ import vitops, { gitLastmod } from '@getvitops/astro';
44
+ vitops({ sitemap: { serialize: await gitLastmod() } });
45
+ ```
46
+
47
+ `@astrojs/sitemap` emits no `<lastmod>` by default, which means a crawler is told your pages
48
+ exist but never that one changed — and it's what lets `vitops indexing` submit a handful of URLs instead
49
+ of all of them. `gitLastmod()` derives each date from the source file's last commit, and leaves a
50
+ page alone rather than guessing (dynamic routes, ambiguous slugs, shallow clones): an inaccurate
51
+ `lastmod` is worse than none, because Google stops trusting the field site-wide.
52
+
53
+ Requires `fetch-depth: 0` in CI — the default shallow clone has no history to read, and it warns
54
+ when that's the case.
55
+
56
+ **Credentials.** IndexNow's key is public by design (it's served at `/<key>.txt` as the ownership
57
+ proof), so it lives in your config, and the Astro integration writes the file into `public/` for
58
+ you — `vitops indexing --new-key` generates one, `--write-key <dir>` writes it for non-Astro stacks.
59
+ Search Console needs a service account in `VITOPS_GSC_SERVICE_ACCOUNT` or
60
+ `GOOGLE_APPLICATION_CREDENTIALS`, added as an owner of the property; it's never read from the
61
+ config file.
62
+
63
+ Persist `.vitops/` between runs (a CI cache) — that's the changed-URL state, and without it every
64
+ run submits everything.
65
+
66
+ An environment whose `robots` policy says `noindex` is refused outright, so pointing this at
67
+ staging can't publish it to a search engine.
68
+
69
+ - 9bf975a: Add `vitops media` — encode raw video into web-ready outputs, instead of hand-rolling an ffmpeg
70
+ script per project.
71
+
72
+ Keep unprocessed video in a `raw/` directory and run:
73
+
74
+ ```
75
+ vitops media --raw raw --out src/assets/processed
76
+ ```
77
+
78
+ Each source becomes three files: **VP9/WebM**, an **H.264/MP4** fallback, and a **JPG poster**.
79
+ Import them like any other asset, so your bundler content-hashes them:
80
+
81
+ ```astro
82
+ ---
83
+ import hero from '../assets/processed/hero.webm';
84
+ import poster from '../assets/processed/hero.jpg';
85
+ ---
86
+ <video poster={poster.src} autoplay muted loop playsinline>
87
+ <source src={hero} type="video/webm" />
88
+ </video>
89
+ ```
90
+
91
+ In Astro it's an integration option — `vitops({ css, media: { raw: 'raw', out: 'src/assets/processed' } })`
92
+ — and it runs in the same pass that generates your CSS. `@getvitops/vite` gains a matching `media`
93
+ option, and `@getvitops/utils/media` exports `processMedia()` for anything else.
94
+
95
+ Defaults: capped at 1920px wide, CRF 32, audio dropped (the common case is a muted autoplay loop),
96
+ poster from frame 0. All of them are flags. `--dry` prints exactly what a run would do.
97
+
98
+ **Runs are cached**, on source content plus encode settings, in `.vitops/media-manifest.json` — a
99
+ 24 MB clip that took 88 seconds the first time takes 0.14 seconds the second. A missing output
100
+ re-encodes; a corrupt manifest re-encodes everything. Neither ever reads as "already done", because
101
+ that failure is silent and no rebuild would fix it.
102
+
103
+ **Commit the outputs and the manifest.** A fresh CI clone has neither and would re-encode from
104
+ scratch; committing both means CI never needs ffmpeg. It also keeps your history clean — ffmpeg
105
+ output isn't byte-reproducible across versions, so a CI re-encode would rewrite every video on any
106
+ toolchain bump. Use `--force` when you mean to re-encode.
107
+
108
+ **`ffmpeg` is an external tool, not an npm dependency** — install it yourself (`brew install
109
+ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails without it rather
110
+ than skipping: a page referencing a video that was never encoded is broken, not degraded.
111
+
112
+ Two defaults worth knowing about. The MP4 exists because older iOS and the in-app webviews inside
113
+ social apps still don't decode VP9, and it's written with `+faststart` — without which a browser
114
+ downloads the whole file before showing frame one. And a poster taken from frame 0 is often black
115
+ on a clip that fades in; that's `--poster-time`, not a bug.
116
+
117
+ ### Patch Changes
118
+
119
+ - Updated dependencies [20e518e]
120
+ - Updated dependencies [9bf975a]
121
+ - @getvitops/generator@2.1.0
122
+ - @getvitops/utils@2.1.0
123
+
124
+ ## 2.0.0
125
+
126
+ ### Minor Changes
127
+
128
+ - bf453b0: Anywhere the toolchain takes a config, that file may now be a `design-system.json` **or** the larger
129
+ site config that embeds one.
130
+
131
+ If you keep your tokens inside a `company.json` / `site.json`, you had to maintain a second file for
132
+ the tooling's sake — or duplicate your whole token set, because a site config must carry a complete
133
+ `designSystem`. Now you point the same option at the file you already have:
134
+
135
+ ```js
136
+ // astro.config.mjs
137
+ getvitops({ css: { input: 'company.json', format: 'css' } });
138
+ ```
139
+
140
+ ```
141
+ vitops generate --input company.json --format css --out dist
142
+ vitops lint --input company.json --src src
143
+ ```
144
+
145
+ The design system is taken from `designSystem.themes[theme]` with its `extends` chain resolved —
146
+ `defaultTheme`, else `default`, else whatever `--theme` / the `theme` option names. Nothing changes
147
+ for a plain `design-system.json`; the two are told apart by shape, not by filename, so you can call
148
+ the file anything.
149
+
150
+ **A site config also supplies the site-level facts generation reads**, so the path is declared once
151
+ rather than per option:
152
+ - `designSystem.defaultColorScheme: "system"` — emits the `prefers-color-scheme` block, which is
153
+ what makes `<color-scheme-toggle>`'s System position do anything.
154
+ - `legal.*.enabled` — renders the documents. `legal: {}` is now the whole declaration; `legal.input`
155
+ is optional.
156
+ - `icons.sprite` — builds `icons.svg`.
157
+ - `fonts` — `fonts: true` reads the families from it.
158
+
159
+ Each of those is already gated on a field in that config, so nothing new appears unless the config
160
+ asks for it. An explicit `site` option still wins when the two are genuinely different files.
161
+
162
+ Also:
163
+ - **`vitops validate` routes on the file's shape.** Pointed at a site config it used to report a
164
+ single `unrecognized_keys` for `designSystem` and nothing about the file's actual contents — a
165
+ wrong answer that reads like a right one. It now validates a site config as one, including the
166
+ cross-field integrity JSON Schema can't express, and checks every theme resolves to a complete
167
+ design system (`validateSite` only checked that the `extends` chain resolved, not what it resolved
168
+ to).
169
+ - **`generate()` gained `theme` and `siteEnv`.** The A/B variant for `siteEnv` is applied before the
170
+ theme is selected, so a variant can override tokens.
171
+ - **The theme editor's Save to source follows the design system into a site config.** Its patch is
172
+ design-system-relative, so the dev server merges into that subtree and writes only the surrounding
173
+ file whole. It locates the subtree in the raw on-disk object rather than the normalised one, so an
174
+ author who wrote either `designSystem` shorthand still gets their own keys edited.
175
+
176
+ - 04a51d8: Icons: one semantic vocabulary across icon sets, with the bundle derived from your source.
177
+
178
+ Name icons by meaning — `<Icon name="menu" />` resolves to `fa7-solid:bars`, `ph:list` or
179
+ `lucide:menu` depending on the set you configure, so swapping sets is a config edit rather than a
180
+ find-and-replace. A name containing `:` still passes through untouched, which is the escape hatch
181
+ for a set-specific glyph.
182
+
183
+ **New `icons` option on `getvitops()`.** Configures the sets once per site and, under
184
+ `output: 'server'`, derives astro-icon's `include` map by scanning your source — the list most
185
+ projects end up maintaining by hand. On a static build no `include` is passed at all, because
186
+ astro-icon is already zero-config there and a list could only drop a glyph the scan couldn't see.
187
+ Names you declare that don't resolve fail the build; names only the scan found just warn; names
188
+ computed at runtime are reported with file and line so you can declare them.
189
+
190
+ **New `<Icon />` component**, and a real fix with it: `Popover`, `Details` and `Drawer` imported
191
+ `astro-icon/components` at module scope, so the _optional_ peer was resolved whether or not an icon
192
+ ever rendered — hard-failing anyone who hadn't installed it. Engines now load dynamically. The
193
+ per-component `iconResolver` prop still works but is deprecated in favour of the integration option.
194
+
195
+ **New SVG sprite** (`icons.sprite: true` in your site config) for consumers that can't run an icon
196
+ integration — Bricks/WordPress, EmDash renderers, plain HTML. `<use href="…/icons.svg#ph--list">`,
197
+ no JavaScript and no icon-API request. Every semantic name also gets a set-independent `icon-<name>`
198
+ alias, so sprite markup survives an icon-set change. WordPress gets `vitops_icon()`, a
199
+ `[vitops_icon]` shortcode and a **Vitops → Icon** Bricks element.
200
+
201
+ **New `vitops icons`** command: reports which icons your source uses, which names couldn't be
202
+ resolved, and which are computed at runtime; `--sprite` builds the sprite, `--json` for CI.
203
+
204
+ **Renamed, and completed to all four directions.** Chevrons and arrows are the
205
+ one family whose _meaning is_ its direction, and that direction is physical, not
206
+ logical — a chevron in a details marker points down in every writing mode. So they
207
+ are named for where they point: `expand-more`/`expand-less` →
208
+ `chevron-down`/`chevron-up`, and `arrow-forward`/`arrow-back` →
209
+ `arrow-right`/`arrow-left`. `chevron-left`/`chevron-right` keep their names.
210
+ `arrow-up`/`arrow-down` are new, so both families now cover all four directions.
211
+ `lightning` is new.
212
+
213
+ If you passed any of the old names to `<Icon />`, `resolveIcon` or an `icons`
214
+ config, update them. They fail loudly rather than silently: an unresolvable
215
+ declared name throws, and `vitops icons` reports scanned ones.
216
+
217
+ **Fixed three Font Awesome mappings that named no real glyph** and so rendered an
218
+ empty box: `login`/`logout` were mapped to `login`/`logout` (Font Awesome calls
219
+ them `right-to-bracket`/`right-from-bracket`) and `backup` to `backup`
220
+ (`cloud-arrow-up`). Every value in all four UI sets is now checked against the
221
+ installed collection.
222
+
223
+ **Phosphor (`ph`) joins the semantic map**, with all 83 names verified against the real icon set.
224
+ Phosphor keeps every weight in one collection and varies the name (`list`, `list-bold`), unlike Font
225
+ Awesome's per-weight collections, so `resolveIcon` and `generateIconInclude` gained a `weight`
226
+ option for sets shaped that way.
227
+
228
+ Fixes: `site.icons` was a closed object, so any icon collection not in its hand-written key list was
229
+ silently dropped during validation — your config passed and the icons never bundled. It accepts any
230
+ collection name now.
231
+
232
+ - bf453b0: `vitops lint` now catches hand-written CSS that re-implements a framework primitive.
233
+
234
+ The existing linter finds classes that resolve to nothing — you named something and it isn't there.
235
+ This is the costlier inverse, where the code **works**: a centred container written by hand, a
236
+ two-column split behind a media query, a flex row. Nothing is broken, so nothing ever surfaces it,
237
+ and the design system quietly stops being where those decisions live.
238
+
239
+ Three rules to start, each requiring the combination that makes the intent unambiguous
240
+ (`margin-inline: auto` alone is not a centred track):
241
+
242
+ | Found | Suggested |
243
+ | ----------------------------------------------------------- | ------------------------------------------------------------------------- |
244
+ | `max-inline-size: var(--width-*)` + `margin-inline: auto` | `.centered`, widening a child with `breakout` / `spotlight` / `fullbleed` |
245
+ | `grid-template-columns: 1fr 2fr` inside a `min-width` query | `md-split-1-2` — or `@md:split-1-2` in the tailwind format |
246
+ | `display: flex` + `align-items: center` | `flex items-center`, or `.cluster-start` |
247
+
248
+ **Findings now carry a severity, and suggestions do not fail the command.** These are judgement
249
+ calls, and a reuse hint that broke CI on the day it shipped would be a worse defect than the drift it
250
+ reports. `--strict` promotes them for anyone who wants the ratchet. Everything the linter reported
251
+ before is an `error` and still exits 1.
252
+
253
+ `.css` files and `<style>` blocks are now read (the class linter only ever looked at markup). The
254
+ generated stylesheet is skipped by its `GENERATED … do not edit by hand` banner rather than by path —
255
+ the `css` format writes `styles.css` into `src/styles`, squarely inside what `--src` scans, and
256
+ linting it made the framework report `.split` as reinventing `.split`.
257
+
258
+ Two other gaps closed:
259
+ - **The format-spelling check ran in one direction only.** `md-split-1-2` in a tailwind project was
260
+ caught; `@md:split-1-2` in a css/bricks project was not, because variant stripping removed the
261
+ `@md:` regardless of format and left a bare class that matched nothing. The same silent no-op, in
262
+ the other direction, unreported. Stripping is format-aware now.
263
+ - **`grid-auto` and the whole `m-*` rhythm family were missing from the tailwind format entirely** —
264
+ not dropped via `TW_CLASH`, just never re-emitted when `layout.css` is skipped and a subset
265
+ re-emitted in its place. Unlike the `<bp>-` classes they have no Tailwind equivalent, and unlike a
266
+ misspelt class the linter cannot flag them, because they aren't anchored to your config. They emit
267
+ as `@utility` definitions now, keyed to the same `--rhythm-*` variables as the css format.
268
+
269
+ Run against this repo's own docs site, the rules found one real instance on the first pass; it is
270
+ fixed in the same change.
271
+
272
+ - bf453b0: Maskable favicons are now opaque, so they stop rendering as a logo in a black box.
273
+
274
+ `icon-mask.png` is declared `purpose: "maskable"` and `apple-touch-icon.png` is linked on every
275
+ page. Both sit a deliberately-inset logo on a larger canvas — that inset **is** the maskable safe
276
+ zone and was always correct — but the canvas was filled with `alpha: 0`. From any transparent
277
+ source that left 36% of `icon-mask.png` and 40% of `apple-touch-icon.png` transparent, on two files
278
+ whose entire contract is full bleed: the OS crops them to its own shape and composites the rest onto
279
+ whatever it likes, usually black. iOS discards alpha on the apple-touch-icon outright.
280
+
281
+ `backgroundColor` was already the obvious input for this and already plumbed — as far as the web
282
+ manifest, and no further. So the raster and the manifest disagreed about the very colour meant to
283
+ sit behind the icon.
284
+
285
+ Now:
286
+ - those two outputs are composited onto `backgroundColor`, defaulting to `#ffffff` — the same
287
+ default the manifest's `background_color` already used;
288
+ - `favicon.svg`, `icon-{16,32,192,512}.png` and `favicon.ico` keep the source's transparency, since
289
+ none of them is maskable;
290
+ - a transparent source with no `backgroundColor` set now warns, rather than silently inheriting
291
+ white — a dark logo would lose against it;
292
+ - `icon-192`/`icon-512` declare `purpose: "any"` explicitly. Omitting it was legal (unset means
293
+ "any"), but with a maskable present and nothing claiming "any", some launchers picked the maskable
294
+ — the one with the safe-zone inset — for slots that wanted a plain icon.
295
+
296
+ `vitops favicon` gained `--background <hex>`; it previously had no way to set this at all.
297
+ `getvitops({ favicon })` and the Vite plugin now forward the option they were already accepting.
298
+
299
+ ### Patch Changes
300
+
301
+ - bf453b0: Say plainly that `fonts` in `design-system.json` is stacks only and loads nothing.
302
+
303
+ The field's description was complete and self-consistent — "raw font stacks by name, emitted as
304
+ `--font-<name>` tokens" — and told you nothing about where the `@font-face` comes from. Following
305
+ it literally leads to installing a `@fontsource*` package and importing its CSS in a layout, which
306
+ renders correctly and silently gives up subsetting, preload, and the `size-adjust` /
307
+ `ascent-override` fallback metrics: a CLS regression that looks like a working setup.
308
+
309
+ The field is unchanged. What changed is that four surfaces now name the boundary and point at the
310
+ fix — declare the family in Astro's `fonts:` config and point the token at its `cssVariable`:
311
+ - the `fonts` description in the JSON Schema, and therefore `vitops docs authoring`
312
+ - `SKILL.md`, which previously never mentioned fonts at all
313
+ - the generated `tailwind.css` header and its `@theme` fonts comment
314
+ - the generated `type-tokens.css` header (css format)
315
+
316
+ - Updated dependencies [4756788]
317
+ - Updated dependencies [bf453b0]
318
+ - Updated dependencies [bf453b0]
319
+ - Updated dependencies [bf453b0]
320
+ - Updated dependencies [bf453b0]
321
+ - Updated dependencies [bf453b0]
322
+ - Updated dependencies [bf453b0]
323
+ - Updated dependencies [bf453b0]
324
+ - Updated dependencies [bf453b0]
325
+ - Updated dependencies [04a51d8]
326
+ - Updated dependencies [bf453b0]
327
+ - Updated dependencies [bf453b0]
328
+ - Updated dependencies [bf453b0]
329
+ - Updated dependencies [bf453b0]
330
+ - Updated dependencies [bf453b0]
331
+ - Updated dependencies [bf453b0]
332
+ - Updated dependencies [bf453b0]
333
+ - Updated dependencies [bf453b0]
334
+ - Updated dependencies [bf453b0]
335
+ - @getvitops/generator@2.0.0
336
+ - @getvitops/utils@2.0.0
337
+
3
338
  ## 1.0.0
4
339
 
5
340
  ### Major Changes
package/README.md CHANGED
@@ -25,6 +25,13 @@ npx vitops generate --format bricks,css --out dist
25
25
  | `vitops favicon -i <svg\|png> -o <dir>` | generate a favicon set from a source image |
26
26
  | `vitops agents [-o AGENTS.md] [--docs-dir <dir>]` | link the agent skill + AGENTS.md pointer |
27
27
  | `vitops docs [topic] [--all]` | print live reference docs to stdout |
28
+ | `vitops indexing [--dry] [--check]` | tell search engines about a deploy |
29
+ | `vitops media [--raw <dir>] [-o <dir>]` | encode raw video to WebM + MP4 + poster |
30
+
31
+ Every `--input` takes a `design-system.json` **or** the larger site config that embeds one
32
+ (`company.json` / `site.json`) — told apart by shape, so name the file whatever you like. Add
33
+ `--theme <name>` to build a `designSystem.themes` entry other than the default. `vitops validate`
34
+ routes the same way and checks a site config as a site config.
28
35
 
29
36
  ## Using the output
30
37
 
@@ -93,6 +100,51 @@ vitops generate --input design-system.json --format design --out .
93
100
  whenever your stylesheet goes somewhere other than the repo root. Regenerate it with your CSS; it's
94
101
  derived from the same config, so it can't drift from what the browser gets.
95
102
 
103
+ ## Video (`vitops media`)
104
+
105
+ Keep unprocessed video in a `raw/` directory and encode it into web-ready outputs:
106
+
107
+ ```sh
108
+ vitops media --raw raw --out src/assets/processed
109
+ ```
110
+
111
+ Each source becomes three files — VP9/WebM, an H.264/MP4 fallback, and a JPG poster frame:
112
+
113
+ ```astro
114
+ ---
115
+ import hero from '../assets/processed/hero.webm';
116
+ import poster from '../assets/processed/hero.jpg';
117
+ ---
118
+ <video poster={poster.src} autoplay muted loop playsinline>
119
+ <source src={hero} type="video/webm" />
120
+ </video>
121
+ ```
122
+
123
+ Outputs land under `src/` so your bundler content-hashes them and they can be served immutable,
124
+ while the raw sources stay outside anything it scans.
125
+
126
+ Defaults: capped at 1920px wide, CRF 32, audio dropped (the common case is a muted autoplay loop),
127
+ poster taken from frame 0. Every one is a flag — `--max-width`, `--crf`, `--audio`,
128
+ `--poster-time`, `--max-bitrate`, `--outputs`. `vitops media --dry` prints exactly what a run
129
+ would do without encoding anything.
130
+
131
+ **Needs `ffmpeg` on `PATH`.** It's an external tool, not an npm dependency — install it yourself
132
+ (`brew install ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails
133
+ rather than skipping: a page referencing a video that was never encoded is broken, not degraded.
134
+
135
+ **Commit the outputs and `.vitops/media-manifest.json`.** Runs are cached on source content plus
136
+ encode settings, so a second run costs a hash per file instead of minutes — but a fresh CI clone
137
+ has neither, and would re-encode everything. Committing both means CI never needs ffmpeg at all.
138
+ It also keeps your history clean: ffmpeg output isn't reproducible across versions, so a CI
139
+ re-encode would rewrite every video on any toolchain bump. Use `--force` when you mean to
140
+ re-encode, and commit the result.
141
+
142
+ Two things worth knowing before you conclude something is broken:
143
+
144
+ - **Frame 0 is often black** on a clip that fades in. That's `--poster-time`, not a bug.
145
+ - **A missing output re-encodes, a corrupt manifest re-encodes everything.** Neither ever reads as
146
+ "already done" — the failure that would cause is silent, and a rebuild would never fix it.
147
+
96
148
  ## Teaching AI agents (`vitops agents` + `vitops docs`)
97
149
 
98
150
  This package **ships an agent skill** (`skill/SKILL.md`, the [Agent Skills](https://code.claude.com/docs/en/skills)