@getvitops/cli 8.2.0-alpha.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 +282 -22
- package/dist/cli.mjs +71 -62
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
- package/skill/SKILL.md +17 -16
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
4
5
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { dirname, extname, join, parse, relative, resolve } from "node:path";
|
|
6
7
|
import { CONFIG_SCHEMA_LOCAL_PATH, JURISDICTION_COUNTRIES, NUMERIC_STEPS, SCHEMA_LOCAL_PATH, buildIconSprite, defaultConfig, defaultSiteConfig, enabledDocs, expandPalette, functionalRole, generate, generateDocs, generateLegal, isConfig, movedTokens, processorsMissingLocation, renderRobotsTxt, resolveConfig, resolveInput, resolveTheme, roleColorUtilities, roleHue, roleKind, validate } from "@getvitops/generator";
|
|
@@ -9,9 +10,8 @@ import { SCOPES, SNAPSHOT_PATH, adcCredentialsPath, adcQuotaProject, collectEntr
|
|
|
9
10
|
import { REVIEWS_CACHE_PATH, fetchReviews, formatPlan as formatPlan$1, mergeCache, plan as plan$1, readCache, selectReviews, toCache, writeCache } from "@getvitops/utils/reviews";
|
|
10
11
|
import { MEDIA_MANIFEST_PATH, formatPlan as formatPlan$2, processMedia } from "@getvitops/utils/media";
|
|
11
12
|
import { homedir } from "node:os";
|
|
12
|
-
import { pathToFileURL } from "node:url";
|
|
13
13
|
import { addSite, backoffSchedule, createApexTxt, findZoneId, formatPlan as formatPlan$3, formatSummary, getSite, getVerificationToken, getWebResource, hasDrift, listApexTxt, ownerUnion, plan as plan$2, refreshTokenGrant, siteUrlFor, updateOwners, verifyWebResource } from "@getvitops/utils/onboarding";
|
|
14
|
-
import { AD_PLATFORMS, AD_PROVIDER_KEYS, formatPlan as formatPlan$4, formatSummary as formatSummary$1, hasDrift as hasDrift$1, isAdProvider, missingFields, plan as plan$3, renderTag, tagId } from "@getvitops/utils/ads";
|
|
14
|
+
import { AD_PLATFORMS, AD_PROVIDER_KEYS, formatPlan as formatPlan$4, formatSummary as formatSummary$1, hasDrift as hasDrift$1, isAdProvider, missingFields, normalizeConversionActionIds, normalizeConversionLabels, plan as plan$3, renderTag, tagId } from "@getvitops/utils/ads";
|
|
15
15
|
import { PLATFORM_PARAMS } from "@getvitops/utils/tracking";
|
|
16
16
|
import { createInterface } from "node:readline/promises";
|
|
17
17
|
import { zohoAccountsHost, zohoApiHost } from "@getvitops/utils/crm";
|
|
@@ -20,7 +20,7 @@ import { normalizeEmailChannel, resolveSender } from "@getvitops/utils/notify";
|
|
|
20
20
|
import { execFileSync } from "node:child_process";
|
|
21
21
|
import { adminCollaboratorIds, branches, createD1, createDevEnvironment, createProdEnvironment, createR2Bucket, createRepo, defaultBranch, environments, findD1, findR2Bucket, formatPlan as formatPlan$6, formatSummary as formatSummary$3, ghAuthed, mainProtected, originRemote, plan as plan$4, planDomainConsistency, protectMain, pushBranches, repoNwo, secretsPresent, setDefaultBranch, setSecret, wranglerWhoami } from "@getvitops/utils/hosting";
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// src/args.ts
|
|
24
24
|
/**
|
|
25
25
|
* A typed wrapper around `node:util`'s `parseArgs`, used by every `vitops` subcommand.
|
|
26
26
|
*
|
|
@@ -59,8 +59,7 @@ function msg(err) {
|
|
|
59
59
|
return err.message;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
//#region src/commands/shared.ts
|
|
62
|
+
// src/commands/shared.ts
|
|
64
63
|
/**
|
|
65
64
|
* Helpers shared by the `vitops` subcommands that read a `Config` (site.json) —
|
|
66
65
|
* `legal`, `robots`, `reviews`, `icons`, and the `search`/`ads` subcommand groups
|
|
@@ -158,8 +157,7 @@ function writeOut(path, contents) {
|
|
|
158
157
|
writeFileSync(path, contents);
|
|
159
158
|
}
|
|
160
159
|
|
|
161
|
-
|
|
162
|
-
//#region src/credentials.ts
|
|
160
|
+
// src/credentials.ts
|
|
163
161
|
/**
|
|
164
162
|
* Credential loaders shared by `search setup`, `search notify`, `ads setup` and
|
|
165
163
|
* `reviews` — every command that talks to Google or Cloudflare. Never reads a
|
|
@@ -329,8 +327,7 @@ function loadZohoOAuth() {
|
|
|
329
327
|
};
|
|
330
328
|
}
|
|
331
329
|
|
|
332
|
-
|
|
333
|
-
//#region src/help.ts
|
|
330
|
+
// src/help.ts
|
|
334
331
|
/**
|
|
335
332
|
* The CLI's help text, and the addressing that lets every subcommand answer
|
|
336
333
|
* `--help`.
|
|
@@ -852,11 +849,10 @@ const wantsHelp = (argv) => argv.includes("-h") || argv.includes("--help");
|
|
|
852
849
|
/** The help text to print for `vitops <command> [sub] --help`. */
|
|
853
850
|
function helpFor(command, argv) {
|
|
854
851
|
const sub = argv[0] && !argv[0].startsWith("-") ? argv[0] : void 0;
|
|
855
|
-
return (sub ? helpSection(command, sub) : void 0) ?? helpSection(command) ?? (command === "search" ? SEARCH_HELP : command === "ads" ? ADS_HELP : command === "crm" ? CRM_HELP : command === "setup" ? SETUP_HELP$1 : command === "email" ? EMAIL_HELP : void 0) ?? HELP;
|
|
852
|
+
return (sub ? helpSection(command, sub) : void 0) ?? helpSection(command) ?? (command === "search" ? "vitops search — Google Search Console\n\n vitops search setup [opts] Onboard site.searchConsole domains as GSC domain properties\n vitops search notify [opts] Tell search engines a deploy happened (sitemap + IndexNow)\n\nRun `vitops --help` for the full option list for each subcommand.\n" : command === "ads" ? "vitops ads — link this site to its ad properties\n\n vitops ads setup [opts] Ensure each platform's domain-verification DNS record\n vitops ads tags [opts] Print the consent-gated pixel snippets\n vitops ads lint [opts] Report ad properties the rest of the config can't see\n\nRun `vitops --help` for the full option list for each subcommand.\n" : command === "crm" ? "vitops crm — push form submissions into a client CRM\n\n vitops crm check [opts] Compare site.crm's fieldMap/trackingFields against the live CRM\n vitops crm setup [opts] Create missing HubSpot properties (Bigin/Jobber have no such API)\n\nRun `vitops --help` for the full option list for each subcommand.\n" : command === "setup" ? "vitops setup — the scripted path from an empty scaffold to a live site\n\n vitops setup repo [opts] GitHub repo + branch policy (main protected, dev open)\n vitops setup hosting [opts] Dev D1 + R2 + repo secrets (cloudflare) or a Node target\n vitops setup domain [opts] Plan/check the workers.dev-vs-custom-domain consistency\n (never applies the cutover — see SETUP.html)\n vitops setup search [opts] Forwards to `vitops search setup`\n vitops setup ads [opts] Forwards to `vitops ads setup`\n vitops setup all [opts] Run repo, hosting, search, ads in order\n\nRun `vitops --help` for the full option list for each subcommand.\n" : command === "email" ? "vitops email — sender-domain onboarding checks\n\n vitops email setup [domain] Onboard a from-address domain, waiting for its DNS to resolve\n vitops email check [opts] Non-blocking deploy guard: warn if a from domain isn't onboarded\n\nRun `vitops --help` for the full option list for each subcommand.\n" : void 0) ?? "vitops — generate design-system outputs from a design-system.json\n\nAnywhere a command takes --input, that file may be a design-system.json OR the\nlarger site config that embeds one (company.json / site.json). The two are told\napart by shape, and a site config also supplies the site-level facts generation\nreads — its default colour scheme, its legal documents, its icon sprite.\n\nUsage:\n vitops generate [options] Generate platform output from a config\n vitops init [options] Scaffold a starter design-system.json\n vitops validate <file> Validate a config against the schema it says it is\n vitops favicon [options] Generate a favicon set from a source image\n vitops agents [options] Link the design-system agent skill + AGENTS.md pointer\n vitops docs [topic] Print live design-system reference docs to stdout\n vitops lint [opts] [files] Report classes that resolve to nothing + primitives you re-implemented\n vitops legal [options] Render legal documents from a site config\n vitops robots [options] Render robots.txt from a site config's environment\n vitops reviews [options] Fetch + cache Google Business Profile reviews\n vitops icons [options] Report which icons your source uses, and build the sprite\n vitops search <sub> [opts] Search Console: onboard domains (setup) + notify deploys (notify)\n vitops ads <sub> [opts] Ad properties: verify domains (setup) + emit pixels (tags) + lint\n vitops setup <sub> [opts] Scripted onboarding: repo, hosting, domain, search, ads, all\n vitops email <sub> [opts] Sender-domain onboarding: check (setup) + deploy guard (check)\n vitops media [options] Encode raw video into web-ready WebM + MP4 + poster\n\nGenerate options:\n -i, --input <path> design-system.json or site config (default: ./design-system.json)\n -f, --format <list> bricks | css | tailwind | design (comma-separated; default: bricks)\n design emits DESIGN.md only (the agent-facing brief) — pair it\n with --out . to write it beside AGENTS.md, or compose:\n --format css,design\n -o, --out <dir> Output directory (default: ./dist)\n --theme <name> Which designSystem.themes entry to build, when --input is\n a site config (default: its defaultTheme, else \"default\")\n --site <path> Site config, when it is a different file from --input.\n Emits legal/*.html into the output directory (see:\n vitops legal). Omitted with a plain design-system.json:\n no legal files.\n --site-env <env> Environment whose A/B variant applies (default: production)\n\nInit options:\n -o, --out <path> Where to write (default: ./design-system.json, or\n ./site.json with --site)\n --force Overwrite an existing file\n --site Write a site config (Config) instead of a bare\n design-system.json — embeds a starter design system at\n designSystem.themes.default, since a Config can't\n merely reference a sibling file\n --name <name> organization.name, when --site is set\n --url <url> site.environments.{production,dev}.url, when --site is set\n\nValidate options:\n <file> Config to check (default: ./design-system.json). Either\n kind — a design-system.json or the config that embeds one.\n -i, --input <path> Same, as a flag\n --site-env <env> Environment whose A/B variant applies (default: production)\n\n Checks the schema, then the things a schema cannot: that every role points at a\n palette hue that exists, that every var() an authored declaration references\n resolves to a token this config emits, and that the colour grammar is current.\n\nFavicon options:\n -i, --input <path> Source SVG or PNG (required)\n -o, --out <dir> Output directory (default: ./public)\n --low-res <path> Optional simplified source for the 16px icon\n --background <hex> Background under the maskable icons, which must be\n opaque (default: #ffffff)\n\nAgents options:\n -i, --input <path> design-system.json or site config (default: ./design-system.json;\n validated if present)\n --theme <name> Theme to read, when --input is a site config\n -o, --out <path> Doc file to update, idempotently (default: ./AGENTS.md)\n --docs-dir <dir> Legacy layout: write the docs bundle as files to this dir\n instead of linking the packaged skill\n\nDocs options:\n <topic> classes | authoring | formats | color | scales | patterns | elements\n (no topic: list topics with summaries)\n -i, --input <path> design-system.json or site config (default: ./design-system.json)\n --theme <name> Theme to render docs from, when --input is a site config\n --all Print every topic, concatenated\n\nIcons options:\n -i, --input <path> Site config carrying the \"icons\" block (default: ./site.json)\n --site-env <env> Environment whose A/B variant applies (default: production)\n --src <dir> Source to scan for icon usage (default: ./src)\n --sprite Also build the SVG sprite\n -o, --out <dir> Where to write icons.svg with --sprite (default: ./dist)\n --json Machine-readable report on stdout\n\nLint options:\n -i, --input <path> design-system.json or site config (default: ./design-system.json)\n --theme <name> Theme to judge classes against, when --input is a site config\n -f, --format <fmt> Format you build (bricks | css | tailwind; default: bricks).\n Responsive md-* classes are real in css/bricks, inert in tailwind.\n -s, --src <dir> Directory to scan (default: ./src)\n --strict Also fail on suggestions (reuse hints), not just on\n classes that resolve to nothing\n [files...] Lint these paths instead of scanning --src. Unreadable\n and non-source paths are skipped, so this can be wired\n into a pre-commit hook that appends the staged files.\n --fix Rewrite pre-1.0 colour-grammar token references\n (var(--<role>-<suffix>) → var(--color-<target>-<role>…))\n in CSS and <style> blocks. Applied in ONE pass, so the\n renames that rotate cannot compound. Nothing else is\n rewritten — every other finding is a judgement call.\n\n Run this after a major bump: it is the migration tool for anything the\n changelog says was renamed.\n\nLegal options:\n -i, --input <path> Site config: .json, or a .js/.ts module with a default\n export (default: ./site.json)\n -d, --doc <name> privacy | terms | cookies (repeatable; default: those\n enabled in the config)\n -f, --format <fmt> md | html | portable-text (default: md)\n html suits WordPress/Bricks and plain sites;\n portable-text suits EmDash\n -o, --out <dir> Write files here (default: print to stdout)\n --site-env <env> Environment whose A/B variant applies (default: production)\n\n Generated from your config — a starting point, not legal advice. Review before\n publishing, and make sure the config describes what your site actually does.\n\nRobots options:\n -i, --input <path> Site config: .json, or a .js/.ts module with a default\n export (default: ./site.json)\n -o, --out <dir> Write robots.txt here (default: print to stdout)\n --site-env <env> Which site.environments entry to render for (default: production)\n\n Reads the resolved environment's robots directive (falling back to\n site.seo.robots) — the same fact vitops search notify's noindex gate already\n reads. A directive containing noindex disallows every user agent and emits\n no Sitemap line; otherwise it allows everything and links\n site.seo.indexing.sitemapUrl, or the derived <canonical>/sitemap-index.xml.\n\nReviews options:\n -i, --input <path> Site config carrying site.reviews (default: ./site.json)\n --site-env <env> Environment whose A/B variant applies (default: production)\n --location <key> Fetch only this site.reviews.locations key\n --cache <path> Where to read/write the cache (default: .vitops/reviews.json)\n --out <dir> Also write the selected reviews per location as JSON here\n --dry Print the plan and exit. Fetches nothing.\n --check Fetch and diff against the cache; exit non-zero on drift.\n Mutates nothing.\n\n Reviews live only on the legacy Business Profile v4 API — there is no newer\n replacement — and the calling GCP project must be allowlisted by Google before\n any call succeeds (a one-time approval; a 403 says so rather than failing\n opaquely). Auth is user OAuth, the same VITOPS_GOOGLE_CLIENT_ID /\n VITOPS_GOOGLE_CLIENT_SECRET / VITOPS_GOOGLE_REFRESH_TOKEN (or ADC login) that\n vitops search setup uses, scoped to business.manage. The cache is meant to be\n committed: reviews change slowly, and a build should not need a live Google\n credential. Review JSON-LD is off by default (site.reviews.schema) — a business\n marking up reviews of itself is not eligible for Google's rich results and\n risks a manual action.\n\nSearch subcommands:\n vitops search setup [opts] Onboard site.searchConsole domains into GSC\n vitops search notify [opts] Tell search engines about a deploy (sitemap + IndexNow)\n\nSearch setup options:\n -i, --input <path> Site config carrying site.searchConsole (default: ./site.json)\n --site-env <env> Environment whose A/B variant applies (default: production)\n --domain <name> Scope to a single site.searchConsole entry\n --dry Print the plan and exit. Changes nothing, and runs without\n credentials (planning from scratch, which it says).\n --check Report drift and exit non-zero if any domain is not fully\n onboarded. Mutates nothing, but does need credentials —\n drift is a comparison against live state.\n\n For each domain it ensures the apex verification TXT in Cloudflare, verifies\n ownership (DNS_TXT, retried with backoff while DNS propagates — a still-\n unverified domain is reported PENDING, not failed), adds the sc-domain: property,\n and adds any delegatedOwners to the web resource. DNS is only ever created,\n never edited or deleted. Credentials come from the environment:\n CLOUDFLARE_API_TOKEN (Zone:DNS:Edit + Zone:Read — the standard \"Edit zone DNS\"\n template covers both; Zone:Read is what the zone-by-name lookup uses), and\n VITOPS_GOOGLE_CLIENT_ID /\n VITOPS_GOOGLE_CLIENT_SECRET / VITOPS_GOOGLE_REFRESH_TOKEN (a user OAuth token\n scoped to siteverification + webmasters). Granting a Google Group Full-User\n access has no API and is surfaced as a reminder.\n\n Or just be logged in: with no VITOPS_GOOGLE_* set, an Application Default\n Credentials login is used —\n gcloud auth application-default login \\\n --scopes=openid,https://www.googleapis.com/auth/siteverification,\\\n https://www.googleapis.com/auth/webmasters,https://www.googleapis.com/auth/cloud-platform\n ADC authenticates through a shared OAuth client that owns no project, so it also\n needs site.google.project in the config (sent as x-goog-user-project), both APIs\n enabled on that project, and serviceusage.services.use on it. One project per site\n is what keeps each site's API usage and billing separate.\n\nSearch notify options:\n -i, --input <path> Site config carrying seo.indexing (default: ./site.json)\n --site-env <env> Environment to notify for (default: production). An\n environment whose robots policy says noindex is refused.\n --sitemap <src> Sitemap URL or local path (default: from the config)\n --urls <list> Comma-separated URLs to submit, skipping the diff\n --all Submit every URL in the sitemap, not just what changed\n --dry Print the plan and exit. Makes no requests.\n --check Read-only: ask Google whether seo.indexing.priorityUrls\n are indexed. Exits non-zero if one is not.\n --new-key Print a fresh IndexNow key and exit\n --write-key <dir> Write the IndexNow key file into <dir> (for stacks with\n no Astro integration to do it — Bricks, WordPress)\n --snapshot <path> Changed-URL state file (default: .vitops/sitemap-snapshot.json).\n Persist it between runs (a CI cache) or every run submits\n everything.\n\n What notify can and cannot do: Google exposes no \"request indexing\" API, and its\n sitemap ping endpoint was removed in 2023 — so it resubmits your sitemap\n through the Search Console API and verifies the result with --check. IndexNow\n reaches Bing, Yandex, Naver, Seznam and Yep; Google does not participate.\n Search Console takes either credential, whichever you already have, added as an\n owner of the property: a service account in VITOPS_GSC_SERVICE_ACCOUNT (inline\n JSON) or GOOGLE_APPLICATION_CREDENTIALS (a path), or the same user OAuth\n credential \"search setup\" uses (VITOPS_GOOGLE_CLIENT_ID / _CLIENT_SECRET /\n _REFRESH_TOKEN). The service account is preferred when both are set — this runs\n on every deploy, and it does not expire.\n\nAds subcommands:\n vitops ads setup [opts] Ensure each platform's domain-verification DNS record\n vitops ads tags [opts] Print the consent-gated pixel snippets\n vitops ads lint [opts] Report ad properties the rest of the config can't see\n\nAds setup options:\n -i, --input <path> Site config carrying site.ads (default: ./site.json)\n --site-env <env> Environment whose A/B variant applies (default: production)\n --provider <name> Scope to one site.ads entry (google | meta | linkedin |\n reddit | tiktok | microsoft | pinterest | snapchat)\n --dry Print the plan and exit. Creates nothing. With\n CLOUDFLARE_API_TOKEN set it reads live DNS first, so the\n plan says what is already done; without one it plans from\n scratch and says so, rather than refusing to run.\n --check Report drift and exit non-zero if any property is not\n linked. Mutates nothing, prompts for nothing.\n --no-prompt Never ask for a missing token; fail with the field name\n instead (the default when stdin is not a TTY, e.g. in CI)\n --no-write Don't persist an answered token back into the config\n\n Four platforms verify a domain by DNS TXT — Meta, TikTok, Pinterest, Snapchat —\n and that record is the one thing this creates for you (in Cloudflare, via\n CLOUDFLARE_API_TOKEN; created only, never edited or deleted). Google Ads,\n LinkedIn, Reddit and Microsoft Ads have no domain verification at all: linking\n there is the tag and the account ID, and the run says so rather than skipping in\n silence. No platform Marketing API is called — Meta's needs a system-user token\n and Google's needs an approved developer token, so the final \"Verify\" click stays\n a reminder. The verification token is not a secret (it is published in DNS, which\n is the ownership proof), so it lives in the config and is prompted for on the\n first run.\n\nAds tags options:\n -i, --input <path> Site config carrying site.ads (default: ./site.json)\n --provider <name> Print one platform's tag\n --strategy <s> idle | async | interaction (default: idle)\n\n Prints each pixel as an INERT, consent-gated <script>: type=\"text/plain\" with the\n library URL on data-src, so an undecided visitor's page issues no third-party\n request. For stacks with no Astro integration — Bricks, WordPress, Eleventy.\n Paste into your template; @getvitops/core's consent runtime activates them.\n\nAds lint options:\n -i, --input <path> Site config carrying site.ads (default: ./site.json)\n\n Reports the gaps that are invisible at runtime: a click-ID parameter this\n platform stamps that attribution doesn't capture, a pixel while site.tracking is\n off (every conversion arrives unattributed), and a configured property with no\n tag ID. Exits non-zero on a finding.\n\nCrm check options:\n -i, --input <path> Site config carrying site.crm (default: ./site.json)\n --site-env <env> Environment to resolve (default: production)\n\n Fetches the live field/property list (HubSpot: crm/v3/properties/contacts;\n Bigin: bigin/v2/settings/fields) and reports every fieldMap/trackingFields\n target that doesn't exist there. HubSpot rejects a submission naming an\n unrecognised property; Bigin silently ignores it — this is the only way to\n see a broken mapping before a real submission finds it. Needs\n VITOPS_HUBSPOT_ACCESS_TOKEN, VITOPS_ZOHO_CLIENT_ID/_CLIENT_SECRET/\n _REFRESH_TOKEN, or (unused for now) VITOPS_JOBBER_CLIENT_ID/_CLIENT_SECRET/\n _REFRESH_TOKEN, matching whichever provider site.crm names. Jobber currently\n renders fieldMap/trackingFields as note-text lines rather than app-configured\n custom fields, so there is nothing live to check there yet. Exits non-zero\n on any missing target.\n\nCrm setup options:\n -i, --input <path> Site config carrying site.crm (default: ./site.json)\n --site-env <env> Environment to resolve (default: production)\n --dry Print what would be created and exit. Creates nothing.\n\n HubSpot: creates missing contact properties named by fieldMap/trackingFields.\n Bigin: no such API exists, so this always prints a manual reminder listing\n the fields to add in Zoho Bigin's field editor, then re-run \"crm check\".\n Jobber: app-configured custom fields aren't wired up yet, so this prints a\n reminder instead of creating anything.\n\nSetup repo options:\n --dry Print the plan and exit\n --check Report drift/blocked steps and exit non-zero, mutating nothing\n --public Create the repo public (default: private)\n --platform <p> cloudflare (default) | node — shown for parity with the hosting stage's plan\n --no-dev-resources Same — this stage never provisions D1/R2 either way\n\n Creates the GitHub repo (via `gh`), pushes main + dev, makes dev the default\n branch, and requests main branch protection + the production/dev environments.\n Idempotent — a re-run of a fully set-up repo is all skips. Branch protection and\n environment reviewers 403 on a private repo without GitHub Pro/Team; that is\n reported as blocked, with the manual Settings path, never as a failure.\n\nSetup hosting options:\n --dry Print the plan and exit\n --check Report drift/blocked steps and exit non-zero, mutating nothing\n --platform <p> cloudflare (default) | node\n --no-dev-resources Skip D1/R2 — a pure static Workers Assets project has\n nothing to provision, just the account id + token\n\n Provisions the isolated dev D1 database + R2 bucket (cloudflare) and sets the\n DEV_D1_ID / CLOUDFLARE_ACCOUNT_ID repo secrets, once `vitops setup repo` has run.\n CLOUDFLARE_API_TOKEN is always reported as blocked — no API can create one; see\n the printed dashboard link. Requires `npx wrangler login` first.\n\nSetup domain options:\n -i, --input <path> Site config (default: ./site.json)\n --site-env <env> Environment to check (default: production)\n --dry Print the plan and exit\n --check Exit non-zero if the domain state is inconsistent\n --domain <apex> Print the manual cutover steps for this apex\n\n Plans and checks the consistency between wrangler.jsonc's workers.dev/custom-domain\n state and site.json's environments/domains/robots — it NEVER edits wrangler.jsonc\n or deploys. Cutover overwrites the zone's DNS record and can take an existing live\n site down; that step is manual, on purpose. See SETUP.html's \"Domain strategy\" stage.\n\nSetup search options:\n Forwards every option to `vitops search setup` unchanged — see \"Search setup\n options:\" above.\n\nSetup ads options:\n Forwards every option to `vitops ads setup` unchanged — see \"Ads setup options:\"\n above.\n\nSetup all options:\n --dry Print every stage's plan and exit\n --check Report drift across every stage and exit non-zero\n --public Create the repo public, for the repo stage\n --platform <p> cloudflare (default) | node, for the repo + hosting stages\n --no-dev-resources Skip D1/R2 in the hosting stage (repo stage never provisions them)\n\n Runs repo, hosting, search, ads in order. Stops at the first stage that fails;\n a blocked step (the API token, GitHub Pro/Team gating) is reported, not fatal.\n\nEmail subcommands:\n vitops email setup [domain] Onboard a from-address domain, waiting for its DNS to resolve\n vitops email check [opts] Non-blocking deploy guard: warn if a from domain isn't onboarded\n\nEmail setup options:\n -i, --input <path> Site config carrying site.notifications.email (default: ./site.json)\n --site-env <env> Environment whose A/B variant applies (default: production)\n [domain] Onboard this exact name instead of deriving candidates from\n config + a source scan\n --src <dir> Source to scan for from: literals as a supplement to config\n (default: ./src)\n --dry Print the plan and exit. Needs no credential — plans from\n scratch and says so.\n --check Report drift and exit non-zero. Mutates nothing.\n --no-prompt Never ask before enabling a domain (the default when\n stdin is not a TTY, e.g. in CI — it just enables)\n --timeout <s> Cap on each DNS-propagation poll wait, in seconds (default: 5)\n\n The onboarded entity is an EXACT name, and it is usually a subdomain\n (send.example.com), never the apex a from address names. When the exact\n domain is absent but a sibling in the same zone is already onboarded, this\n is reported blocked rather than silently enabling the apex too — enabling\n it rewrites _dmarc.<domain>, which can tighten policy on every OTHER mail\n sent from that domain. Sending from the onboarded sibling is the\n recommended fix. Needs CLOUDFLARE_API_TOKEN (Zone:Read + an Email Sending\n scope — broader than the \"Edit zone DNS\" template other commands use).\n\nEmail check options:\n -i, --input <path> Site config carrying site.notifications.email (default: ./site.json)\n --site-env <env> Environment whose A/B variant applies (default: production)\n --src <dir> Source to scan for from: literals (default: ./src)\n --strict Exit non-zero on drift (the default just warns and exits 0)\n --skip Skip the check entirely (same as VITOPS_SKIP_EMAIL_CHECK)\n\n Meant to run before a deploy. With no CLOUDFLARE_API_TOKEN it reports \"not\n checked\" and exits 0, so a CI job without the secret never fails on the\n check itself. Skip per-site with site.notifications.email.verifySender: false\n when the scan can't see the real from address and the warning would be a\n false positive.\n\nMedia options:\n --raw <dir> Directory of unprocessed video, walked recursively\n (default: ./raw)\n -o, --out <dir> Where the encoded outputs go (default: ./src/assets/processed).\n Subdirectories under --raw are preserved.\n --max-width <px> Cap the output width, keeping aspect ratio (default: 1920;\n 0 disables scaling)\n --crf <n> Quality on VP9's scale, 0-63, lower is better (default: 32).\n The MP4 fallback uses the equivalent on H.264's scale.\n --max-bitrate <r> Optional ceiling, e.g. 2M or 800k (default: none, which is\n constant quality rather than constrained)\n --audio Keep the audio track (default: dropped — the common case is\n a muted autoplay loop)\n --poster-time <s> Timestamp the poster frame is taken from (default: 0, which\n is often black on a clip that fades in)\n --outputs <list> Comma-separated: webm | mp4 | poster (default: all three)\n --manifest <path> Cache file (default: .vitops/media-manifest.json)\n --force Re-encode everything, ignoring the cache\n --dry Print the plan and exit. Encodes nothing.\n\n Needs ffmpeg on PATH — it is an external tool, not an npm dependency, and this\n command fails rather than quietly skipping. Commit the outputs and the manifest:\n ffmpeg output is not reproducible across versions, so re-encoding in CI churns\n the diff on every toolchain bump. Use --force when you mean to re-encode.\n\nCommon:\n -h, --help Show this help\n";
|
|
856
853
|
}
|
|
857
854
|
|
|
858
|
-
|
|
859
|
-
//#region src/commands/search.ts
|
|
855
|
+
// src/commands/search.ts
|
|
860
856
|
/**
|
|
861
857
|
* `vitops search` — everything that talks to search engines: `setup` onboards a
|
|
862
858
|
* domain into Search Console (DNS + verification + property), `notify` tells the
|
|
@@ -866,10 +862,9 @@ function helpFor(command, argv) {
|
|
|
866
862
|
/**
|
|
867
863
|
* Adapt a `Config` to the indexing module's own option shape.
|
|
868
864
|
*
|
|
869
|
-
*
|
|
870
|
-
*
|
|
871
|
-
*
|
|
872
|
-
* origin — see below.
|
|
865
|
+
* `IndexNowConfig` and `SearchConsoleConfig` are the schema's own types now, so those blocks pass
|
|
866
|
+
* straight through. What this adapter still does is genuine resolution — the origin and the
|
|
867
|
+
* robots policy come from the resolved environment, not from the `seo.indexing` block — see below.
|
|
873
868
|
*/
|
|
874
869
|
function toIndexingConfig(cfg, siteEnv) {
|
|
875
870
|
const site = cfg.site;
|
|
@@ -888,16 +883,15 @@ function toIndexingConfig(cfg, siteEnv) {
|
|
|
888
883
|
/**
|
|
889
884
|
* Adapt a `Config` to the onboarding module's own option shape.
|
|
890
885
|
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
886
|
+
* `DomainSetup` is the `site.searchConsole.<domain>` block plus its key, so this is a spread and a
|
|
887
|
+
* filter. `--domain` scopes to a single entry, and a name that isn't there is an error, not an
|
|
888
|
+
* empty run.
|
|
894
889
|
*/
|
|
895
890
|
function toSearchConsoleSetup(cfg, domainFilter) {
|
|
896
891
|
const entries = cfg.site.searchConsole ?? {};
|
|
897
892
|
const setups = Object.entries(entries).map(([domain, e]) => ({
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
...e.fullUserGroup ? { fullUserGroup: e.fullUserGroup } : {}
|
|
893
|
+
...e,
|
|
894
|
+
domain
|
|
901
895
|
}));
|
|
902
896
|
return pickOne(setups, domainFilter, (s) => s.domain, {
|
|
903
897
|
flag: "domain",
|
|
@@ -1258,8 +1252,7 @@ async function cmdSearch(argv) {
|
|
|
1258
1252
|
}
|
|
1259
1253
|
}
|
|
1260
1254
|
|
|
1261
|
-
|
|
1262
|
-
//#region src/prompt.ts
|
|
1255
|
+
// src/prompt.ts
|
|
1263
1256
|
/**
|
|
1264
1257
|
* Asking for the config values `vitops ads setup` cannot derive, and writing the
|
|
1265
1258
|
* answers back.
|
|
@@ -1396,8 +1389,7 @@ function writeConfigPatch(path, provider, field, value) {
|
|
|
1396
1389
|
return { written: true };
|
|
1397
1390
|
}
|
|
1398
1391
|
|
|
1399
|
-
|
|
1400
|
-
//#region src/commands/ads.ts
|
|
1392
|
+
// src/commands/ads.ts
|
|
1401
1393
|
/**
|
|
1402
1394
|
* `vitops ads` — everything that links this site to an ad platform. Three
|
|
1403
1395
|
* subcommands, split by what they touch: `setup` writes DNS, `tags` writes
|
|
@@ -1406,9 +1398,10 @@ function writeConfigPatch(path, provider, field, value) {
|
|
|
1406
1398
|
/**
|
|
1407
1399
|
* Adapt a `Config` to the ads module's own option shape.
|
|
1408
1400
|
*
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
*
|
|
1401
|
+
* `AdPropertySetup` is the `site.ads.<provider>` block plus its key, so this spreads the block.
|
|
1402
|
+
* (It used to copy a hand-picked subset of fields, which silently dropped `conversionActionId` and
|
|
1403
|
+
* `reporting` on the way to the planner.) The domain default is the one derived value: a platform
|
|
1404
|
+
* verifies the host the site is published at, so
|
|
1412
1405
|
* `domains.canonical` answers it, and only an ad account verified against some
|
|
1413
1406
|
* other host needs `site.ads.<provider>.domain` stated.
|
|
1414
1407
|
*/
|
|
@@ -1427,14 +1420,9 @@ function toAdProperties(cfg, providerFilter) {
|
|
|
1427
1420
|
const e = entries[provider];
|
|
1428
1421
|
const domain = e.domain ?? canonicalHost;
|
|
1429
1422
|
return {
|
|
1423
|
+
...e,
|
|
1430
1424
|
provider,
|
|
1431
|
-
...
|
|
1432
|
-
...e.pixelId ? { pixelId: e.pixelId } : {},
|
|
1433
|
-
...e.conversionLabel ? { conversionLabel: e.conversionLabel } : {},
|
|
1434
|
-
...domain ? { domain } : {},
|
|
1435
|
-
...e.domainVerification ? { domainVerification: e.domainVerification } : {},
|
|
1436
|
-
...e.category ? { category: e.category } : {},
|
|
1437
|
-
...e.enabled != null ? { enabled: e.enabled } : {}
|
|
1425
|
+
...domain ? { domain } : {}
|
|
1438
1426
|
};
|
|
1439
1427
|
});
|
|
1440
1428
|
if (providerFilter && !isAdProvider(providerFilter)) fail(`--provider "${providerFilter}" is not a known ad platform (expected: ${AD_PROVIDER_KEYS.join(" | ")})`);
|
|
@@ -1649,6 +1637,12 @@ async function cmdAdsLint(argv) {
|
|
|
1649
1637
|
const uncaptured = platform.clickIdParams.filter((param) => !(param in PLATFORM_PARAMS));
|
|
1650
1638
|
if (uncaptured.length) findings.push(`${p.provider}: click ID ${uncaptured.join(", ")} is not in the capture vocabulary — every conversion from this platform will arrive unattributed`);
|
|
1651
1639
|
if (!trackingOn) findings.push(`${p.provider}: site.tracking.enabled is not true, so ${platform.clickIdParams.join("/")} is never captured — conversions will be unattributed`);
|
|
1640
|
+
if (p.provider === "google" && p.reporting === "server") {
|
|
1641
|
+
const labels = normalizeConversionLabels(p.conversionLabel);
|
|
1642
|
+
const actionIds = normalizeConversionActionIds(p.conversionActionId);
|
|
1643
|
+
const configuredNoId = Object.keys(labels).filter((action) => labels[action] && !actionIds[action]);
|
|
1644
|
+
if (configuredNoId.length) findings.push(`google: reporting is "server" but conversionActionId.${configuredNoId.join(", .")} is not set — ${configuredNoId.length === 1 ? "that action" : "those actions"} still fire client-side, not a failure but not what "server" asked for either`);
|
|
1645
|
+
}
|
|
1652
1646
|
}
|
|
1653
1647
|
if (findings.length === 0) {
|
|
1654
1648
|
console.log(`✓ ${properties.length} ad propert${properties.length === 1 ? "y" : "ies"} check out`);
|
|
@@ -1672,8 +1666,7 @@ async function cmdAds(argv) {
|
|
|
1672
1666
|
}
|
|
1673
1667
|
}
|
|
1674
1668
|
|
|
1675
|
-
|
|
1676
|
-
//#region src/commands/crm.ts
|
|
1669
|
+
// src/commands/crm.ts
|
|
1677
1670
|
/**
|
|
1678
1671
|
* `vitops crm` — the two things `site.crm` needs a human-in-the-loop for.
|
|
1679
1672
|
*
|
|
@@ -1858,8 +1851,7 @@ async function cmdCrm(argv) {
|
|
|
1858
1851
|
}
|
|
1859
1852
|
}
|
|
1860
1853
|
|
|
1861
|
-
|
|
1862
|
-
//#region src/commands/email.ts
|
|
1854
|
+
// src/commands/email.ts
|
|
1863
1855
|
/**
|
|
1864
1856
|
* `vitops email` — connect `site.notifications.email.from` to whether its domain
|
|
1865
1857
|
* is actually onboarded onto an outbound email provider.
|
|
@@ -2172,8 +2164,7 @@ async function cmdEmail(argv) {
|
|
|
2172
2164
|
}
|
|
2173
2165
|
}
|
|
2174
2166
|
|
|
2175
|
-
|
|
2176
|
-
//#region src/commands/setup.ts
|
|
2167
|
+
// src/commands/setup.ts
|
|
2177
2168
|
/**
|
|
2178
2169
|
* `vitops setup` — the scripted path from an empty scaffold to a live site.
|
|
2179
2170
|
* Subcommands: `repo | hosting | domain | search | ads | all`.
|
|
@@ -2588,8 +2579,7 @@ async function cmdSetup(argv) {
|
|
|
2588
2579
|
}
|
|
2589
2580
|
}
|
|
2590
2581
|
|
|
2591
|
-
|
|
2592
|
-
//#region src/agents.ts
|
|
2582
|
+
// src/agents.ts
|
|
2593
2583
|
/**
|
|
2594
2584
|
* Side-effect-free helpers for the `vitops agents` / `vitops docs` commands.
|
|
2595
2585
|
* Kept out of cli.ts (which runs main() on import) so they are unit-testable.
|
|
@@ -2654,6 +2644,11 @@ const TOPICS = {
|
|
|
2654
2644
|
path: "concepts/legal.md",
|
|
2655
2645
|
summary: "privacy policy / terms / cookie notice derived from config facts"
|
|
2656
2646
|
},
|
|
2647
|
+
blocks: {
|
|
2648
|
+
path: "",
|
|
2649
|
+
external: "emdash",
|
|
2650
|
+
summary: "EmDash Portable Text blocks and every field (needs @getvitops/emdash installed)"
|
|
2651
|
+
},
|
|
2657
2652
|
elements: {
|
|
2658
2653
|
path: "bricks/elements.md",
|
|
2659
2654
|
summary: "Bricks Builder custom elements, controls, defaults"
|
|
@@ -2705,8 +2700,7 @@ function linkSkill(linkPath, targetDir) {
|
|
|
2705
2700
|
}
|
|
2706
2701
|
}
|
|
2707
2702
|
|
|
2708
|
-
|
|
2709
|
-
//#region src/css-scan.ts
|
|
2703
|
+
// src/css-scan.ts
|
|
2710
2704
|
/** Strip comments, preserving newlines so line numbers survive. */
|
|
2711
2705
|
function stripComments(css) {
|
|
2712
2706
|
return css.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ""));
|
|
@@ -2808,8 +2802,7 @@ function extractStyleBlocks(src) {
|
|
|
2808
2802
|
return out;
|
|
2809
2803
|
}
|
|
2810
2804
|
|
|
2811
|
-
|
|
2812
|
-
//#region src/lint-css.ts
|
|
2805
|
+
// src/lint-css.ts
|
|
2813
2806
|
/**
|
|
2814
2807
|
* The framework's own track widths. Declared in two places today
|
|
2815
2808
|
* (`core/css/layout.css` and the tailwind emitter), so a rule keyed to the token
|
|
@@ -3024,8 +3017,7 @@ function lintCss(files, format) {
|
|
|
3024
3017
|
return findings;
|
|
3025
3018
|
}
|
|
3026
3019
|
|
|
3027
|
-
|
|
3028
|
-
//#region src/lint-markup.ts
|
|
3020
|
+
// src/lint-markup.ts
|
|
3029
3021
|
/**
|
|
3030
3022
|
* The third linting direction: markup that lays out a repeated set of things
|
|
3031
3023
|
* without the framework's pattern for it.
|
|
@@ -3247,8 +3239,7 @@ function lintMarkup(files) {
|
|
|
3247
3239
|
return findings;
|
|
3248
3240
|
}
|
|
3249
3241
|
|
|
3250
|
-
|
|
3251
|
-
//#region src/lint-tokens.ts
|
|
3242
|
+
// src/lint-tokens.ts
|
|
3252
3243
|
/**
|
|
3253
3244
|
* Rewrite every old name in one pass.
|
|
3254
3245
|
*
|
|
@@ -3304,8 +3295,7 @@ function lintTokens(files, renames) {
|
|
|
3304
3295
|
return findings;
|
|
3305
3296
|
}
|
|
3306
3297
|
|
|
3307
|
-
|
|
3308
|
-
//#region src/lint.ts
|
|
3298
|
+
// src/lint.ts
|
|
3309
3299
|
/**
|
|
3310
3300
|
* `vitops lint` — find framework classes in consumer source that resolve to
|
|
3311
3301
|
* nothing in the format being built.
|
|
@@ -3488,8 +3478,7 @@ function lintSource(files, v, format) {
|
|
|
3488
3478
|
return findings;
|
|
3489
3479
|
}
|
|
3490
3480
|
|
|
3491
|
-
|
|
3492
|
-
//#region src/cli.ts
|
|
3481
|
+
// src/cli.ts
|
|
3493
3482
|
/**
|
|
3494
3483
|
* `vitops` — CLI for the Vitops design-system generator.
|
|
3495
3484
|
*
|
|
@@ -4157,7 +4146,7 @@ function generatorAssetsDir() {
|
|
|
4157
4146
|
const AGENTS_START = "<!-- vitops:start -->";
|
|
4158
4147
|
const AGENTS_END = "<!-- vitops:end -->";
|
|
4159
4148
|
/** Print a live reference doc (rendered from the project's config) to stdout. */
|
|
4160
|
-
function cmdDocs(argv) {
|
|
4149
|
+
async function cmdDocs(argv) {
|
|
4161
4150
|
const { values, positionals } = parseArgs({
|
|
4162
4151
|
args: argv,
|
|
4163
4152
|
options: {
|
|
@@ -4181,10 +4170,31 @@ function cmdDocs(argv) {
|
|
|
4181
4170
|
return;
|
|
4182
4171
|
}
|
|
4183
4172
|
if (topic && !TOPICS[topic]) fail(`unknown topic "${topic}". Valid topics:\n${topicList()}`);
|
|
4184
|
-
const
|
|
4185
|
-
const docs = generateDocs(
|
|
4186
|
-
const
|
|
4187
|
-
|
|
4173
|
+
const names = values.all ? Object.keys(TOPICS) : [topic];
|
|
4174
|
+
const docs = names.every((n) => TOPICS[n].external) ? {} : generateDocs(loadDesignSystem(resolve(values.input), values.theme), generatorAssetsDir());
|
|
4175
|
+
const parts = [];
|
|
4176
|
+
for (const name of names) {
|
|
4177
|
+
const t = TOPICS[name];
|
|
4178
|
+
if (!t.external) {
|
|
4179
|
+
parts.push(docs[t.path]);
|
|
4180
|
+
continue;
|
|
4181
|
+
}
|
|
4182
|
+
const doc = await externalDoc(t.external);
|
|
4183
|
+
if (doc) parts.push(doc);
|
|
4184
|
+
else if (!values.all) fail(`topic "${name}" needs @getvitops/${t.external} installed in this project`);
|
|
4185
|
+
}
|
|
4186
|
+
process.stdout.write(parts.join("\n"));
|
|
4187
|
+
}
|
|
4188
|
+
/** Render a topic owned by an optional package, resolved from the project, not from the CLI. */
|
|
4189
|
+
async function externalDoc(pkg) {
|
|
4190
|
+
try {
|
|
4191
|
+
const manifest = createRequire(join(process.cwd(), "package.json")).resolve(`@getvitops/${pkg}/package.json`);
|
|
4192
|
+
const exp = JSON.parse(readFileSync(manifest, "utf8")).exports["."].import;
|
|
4193
|
+
const entry = join(dirname(manifest), exp);
|
|
4194
|
+
return (await import(pathToFileURL(entry).href)).renderBlocksDoc?.() ?? null;
|
|
4195
|
+
} catch {
|
|
4196
|
+
return null;
|
|
4197
|
+
}
|
|
4188
4198
|
}
|
|
4189
4199
|
function cmdLint(argv) {
|
|
4190
4200
|
const { values, positionals } = parseArgs({
|
|
@@ -4471,6 +4481,5 @@ async function main() {
|
|
|
4471
4481
|
}
|
|
4472
4482
|
main().catch((err) => fail(msg(err)));
|
|
4473
4483
|
|
|
4474
|
-
//#endregion
|
|
4475
4484
|
export { };
|
|
4476
4485
|
//# sourceMappingURL=cli.mjs.map
|