@getvitops/cli 2.0.0 → 3.0.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 +206 -0
- package/README.md +47 -0
- package/dist/cli.mjs +374 -15
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
- package/skill/SKILL.md +17 -0
package/dist/cli.mjs
CHANGED
|
@@ -4,8 +4,10 @@ import { parseArgs } from "node:util";
|
|
|
4
4
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join, parse, relative, resolve } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { NUMERIC_STEPS, SCHEMA_URL, buildIconSprite, defaultConfig, enabledDocs, expandPalette, functionalRole, generate, generateDocs, generateLegal,
|
|
7
|
+
import { NUMERIC_STEPS, SCHEMA_URL, buildIconSprite, defaultConfig, enabledDocs, expandPalette, functionalRole, generate, generateDocs, generateLegal, isConfig, resolveConfig, resolveInput, resolveTheme, roleColorUtilities, roleHue, roleKind, validate } from "@getvitops/generator";
|
|
8
8
|
import { collectIconRefs, generateFavicons, generateIconInclude, resolveIcon, scanFiles } from "@getvitops/utils";
|
|
9
|
+
import { SNAPSHOT_PATH, collectEntries, formatPlan, getAccessToken, inspectUrl, keyFileContents, newKey, parseServiceAccount, plan, readSnapshot, resolveKeyLocation, resolveSitemapUrl, submitBatch, submitSitemap, toSnapshot, verifyKeyFile, writeSnapshot } from "@getvitops/utils/indexing";
|
|
10
|
+
import { MEDIA_MANIFEST_PATH, formatPlan as formatPlan$1, processMedia } from "@getvitops/utils/media";
|
|
9
11
|
|
|
10
12
|
//#region src/agents.ts
|
|
11
13
|
/**
|
|
@@ -28,6 +30,10 @@ const TOPICS = {
|
|
|
28
30
|
path: "authoring.md",
|
|
29
31
|
summary: "every design-system.json field, rendered from the JSON Schema"
|
|
30
32
|
},
|
|
33
|
+
config: {
|
|
34
|
+
path: "config.md",
|
|
35
|
+
summary: "every field of the three-section config (designSystem / organization / site), rendered from the JSON Schema"
|
|
36
|
+
},
|
|
31
37
|
formats: {
|
|
32
38
|
path: "formats.md",
|
|
33
39
|
summary: "tailwind vs css vs bricks: what's emitted vs what the platform provides"
|
|
@@ -530,6 +536,8 @@ function lintSource(files, v, format) {
|
|
|
530
536
|
* vitops docs [topic] [--input <json>] [--all]
|
|
531
537
|
* vitops lint [--input <json>] [--format <fmt>] [--src <dir>]
|
|
532
538
|
* vitops legal [--input <site.json>] [--doc <name>] [--format <md|html|portable-text>] [--out <dir>]
|
|
539
|
+
* vitops indexing [--input <site.json>] [--dry] [--all] [--check]
|
|
540
|
+
* vitops media [--raw <dir>] [--out <dir>] [--force] [--dry]
|
|
533
541
|
*
|
|
534
542
|
* Thin wrapper over @getvitops/generator (generation) and @getvitops/utils (favicons).
|
|
535
543
|
* Every client brings their own consumer-editable design-system.json.
|
|
@@ -572,6 +580,8 @@ Usage:
|
|
|
572
580
|
vitops lint [options] Report framework classes in your source that resolve to nothing
|
|
573
581
|
vitops legal [options] Render legal documents from a site config
|
|
574
582
|
vitops icons [options] Report which icons your source uses, and build the sprite
|
|
583
|
+
vitops indexing [options] Tell search engines about a deploy (sitemap + IndexNow)
|
|
584
|
+
vitops media [options] Encode raw video into web-ready WebM + MP4 + poster
|
|
575
585
|
|
|
576
586
|
Generate options:
|
|
577
587
|
-i, --input <path> design-system.json or site config (default: ./design-system.json)
|
|
@@ -644,6 +654,56 @@ Legal options:
|
|
|
644
654
|
Generated from your config — a starting point, not legal advice. Review before
|
|
645
655
|
publishing, and make sure the config describes what your site actually does.
|
|
646
656
|
|
|
657
|
+
Indexing options:
|
|
658
|
+
-i, --input <path> Site config carrying seo.indexing (default: ./site.json)
|
|
659
|
+
--site-env <env> Environment to notify for (default: production). An
|
|
660
|
+
environment whose robots policy says noindex is refused.
|
|
661
|
+
--sitemap <src> Sitemap URL or local path (default: from the config)
|
|
662
|
+
--urls <list> Comma-separated URLs to submit, skipping the diff
|
|
663
|
+
--all Submit every URL in the sitemap, not just what changed
|
|
664
|
+
--dry Print the plan and exit. Makes no requests.
|
|
665
|
+
--check Read-only: ask Google whether seo.indexing.priorityUrls
|
|
666
|
+
are indexed. Exits non-zero if one is not.
|
|
667
|
+
--new-key Print a fresh IndexNow key and exit
|
|
668
|
+
--write-key <dir> Write the IndexNow key file into <dir> (for stacks with
|
|
669
|
+
no Astro integration to do it — Bricks, WordPress)
|
|
670
|
+
--snapshot <path> Changed-URL state file (default: .vitops/sitemap-snapshot.json).
|
|
671
|
+
Persist it between runs (a CI cache) or every run submits
|
|
672
|
+
everything.
|
|
673
|
+
|
|
674
|
+
What this can and cannot do: Google exposes no "request indexing" API, and its
|
|
675
|
+
sitemap ping endpoint was removed in 2023 — so it resubmits your sitemap
|
|
676
|
+
through the Search Console API and verifies the result with --check. IndexNow
|
|
677
|
+
reaches Bing, Yandex, Naver, Seznam and Yep; Google does not participate.
|
|
678
|
+
Search Console needs a service account in VITOPS_GSC_SERVICE_ACCOUNT (inline
|
|
679
|
+
JSON) or GOOGLE_APPLICATION_CREDENTIALS (a path), added as an owner of the
|
|
680
|
+
property.
|
|
681
|
+
|
|
682
|
+
Media options:
|
|
683
|
+
--raw <dir> Directory of unprocessed video, walked recursively
|
|
684
|
+
(default: ./raw)
|
|
685
|
+
-o, --out <dir> Where the encoded outputs go (default: ./src/assets/processed).
|
|
686
|
+
Subdirectories under --raw are preserved.
|
|
687
|
+
--max-width <px> Cap the output width, keeping aspect ratio (default: 1920;
|
|
688
|
+
0 disables scaling)
|
|
689
|
+
--crf <n> Quality on VP9's scale, 0-63, lower is better (default: 32).
|
|
690
|
+
The MP4 fallback uses the equivalent on H.264's scale.
|
|
691
|
+
--max-bitrate <r> Optional ceiling, e.g. 2M or 800k (default: none, which is
|
|
692
|
+
constant quality rather than constrained)
|
|
693
|
+
--audio Keep the audio track (default: dropped — the common case is
|
|
694
|
+
a muted autoplay loop)
|
|
695
|
+
--poster-time <s> Timestamp the poster frame is taken from (default: 0, which
|
|
696
|
+
is often black on a clip that fades in)
|
|
697
|
+
--outputs <list> Comma-separated: webm | mp4 | poster (default: all three)
|
|
698
|
+
--manifest <path> Cache file (default: .vitops/media-manifest.json)
|
|
699
|
+
--force Re-encode everything, ignoring the cache
|
|
700
|
+
--dry Print the plan and exit. Encodes nothing.
|
|
701
|
+
|
|
702
|
+
Needs ffmpeg on PATH — it is an external tool, not an npm dependency, and this
|
|
703
|
+
command fails rather than quietly skipping. Commit the outputs and the manifest:
|
|
704
|
+
ffmpeg output is not reproducible across versions, so re-encoding in CI churns
|
|
705
|
+
the diff on every toolchain bump. Use --force when you mean to re-encode.
|
|
706
|
+
|
|
647
707
|
Common:
|
|
648
708
|
-h, --help Show this help
|
|
649
709
|
`;
|
|
@@ -748,10 +808,10 @@ function cmdValidate(argv) {
|
|
|
748
808
|
} catch (err) {
|
|
749
809
|
fail(`could not parse JSON: ${err.message}`);
|
|
750
810
|
}
|
|
751
|
-
if (
|
|
811
|
+
if (isConfig(raw)) {
|
|
752
812
|
let site;
|
|
753
813
|
try {
|
|
754
|
-
site =
|
|
814
|
+
site = resolveConfig(raw, values["site-env"]);
|
|
755
815
|
} catch (err) {
|
|
756
816
|
console.error(`✖ ${path} is invalid as a site config:`);
|
|
757
817
|
console.error(err.message.replace(/^Invalid site config:\n/, ""));
|
|
@@ -815,6 +875,99 @@ async function cmdFavicon(argv) {
|
|
|
815
875
|
fail(err.message);
|
|
816
876
|
}
|
|
817
877
|
}
|
|
878
|
+
const OUTPUT_KINDS = new Set([
|
|
879
|
+
"webm",
|
|
880
|
+
"mp4",
|
|
881
|
+
"poster"
|
|
882
|
+
]);
|
|
883
|
+
/**
|
|
884
|
+
* `vitops media` — encode a directory of raw video into web-ready outputs.
|
|
885
|
+
*
|
|
886
|
+
* Flags only, with no config file, following `favicon`: `site.favicon` exists in
|
|
887
|
+
* the schema and `cmdFavicon` still doesn't read it. The same reasoning applies
|
|
888
|
+
* more strongly here — `legal`, `icons` and `indexing` are anchored to a
|
|
889
|
+
* `Config` because what they emit *describes the site*, and an encoder setting
|
|
890
|
+
* describes a build step.
|
|
891
|
+
*/
|
|
892
|
+
async function cmdMedia(argv) {
|
|
893
|
+
const { values } = parseArgs({
|
|
894
|
+
args: argv,
|
|
895
|
+
options: {
|
|
896
|
+
raw: {
|
|
897
|
+
type: "string",
|
|
898
|
+
default: "raw"
|
|
899
|
+
},
|
|
900
|
+
out: {
|
|
901
|
+
type: "string",
|
|
902
|
+
short: "o",
|
|
903
|
+
default: "src/assets/processed"
|
|
904
|
+
},
|
|
905
|
+
"max-width": { type: "string" },
|
|
906
|
+
crf: { type: "string" },
|
|
907
|
+
"max-bitrate": { type: "string" },
|
|
908
|
+
audio: {
|
|
909
|
+
type: "boolean",
|
|
910
|
+
default: false
|
|
911
|
+
},
|
|
912
|
+
"poster-time": { type: "string" },
|
|
913
|
+
outputs: { type: "string" },
|
|
914
|
+
manifest: {
|
|
915
|
+
type: "string",
|
|
916
|
+
default: MEDIA_MANIFEST_PATH
|
|
917
|
+
},
|
|
918
|
+
force: {
|
|
919
|
+
type: "boolean",
|
|
920
|
+
default: false
|
|
921
|
+
},
|
|
922
|
+
dry: {
|
|
923
|
+
type: "boolean",
|
|
924
|
+
default: false
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
allowPositionals: false
|
|
928
|
+
});
|
|
929
|
+
const num = (flag, raw) => {
|
|
930
|
+
if (raw === void 0) return void 0;
|
|
931
|
+
const n = Number(raw);
|
|
932
|
+
if (!Number.isFinite(n) || n < 0) fail(`media: --${flag} must be a non-negative number`);
|
|
933
|
+
return n;
|
|
934
|
+
};
|
|
935
|
+
let outputs;
|
|
936
|
+
if (values.outputs) {
|
|
937
|
+
outputs = values.outputs.split(",").map((s) => s.trim());
|
|
938
|
+
for (const kind of outputs) if (!OUTPUT_KINDS.has(kind)) fail(`media: unknown output "${kind}" (expected: webm | mp4 | poster)`);
|
|
939
|
+
}
|
|
940
|
+
const maxWidth = num("max-width", values["max-width"]);
|
|
941
|
+
const crf = num("crf", values.crf);
|
|
942
|
+
const posterTime = num("poster-time", values["poster-time"]);
|
|
943
|
+
const config = {
|
|
944
|
+
...maxWidth !== void 0 ? { maxWidth } : {},
|
|
945
|
+
...crf !== void 0 ? { crf } : {},
|
|
946
|
+
...values["max-bitrate"] ? { maxBitrate: values["max-bitrate"] } : {},
|
|
947
|
+
...values.audio ? { audio: true } : {},
|
|
948
|
+
...posterTime !== void 0 ? { posterTime } : {},
|
|
949
|
+
...outputs ? { outputs } : {}
|
|
950
|
+
};
|
|
951
|
+
try {
|
|
952
|
+
const result = await processMedia({
|
|
953
|
+
raw: values.raw,
|
|
954
|
+
out: values.out,
|
|
955
|
+
config,
|
|
956
|
+
manifest: values.manifest,
|
|
957
|
+
force: values.force,
|
|
958
|
+
dry: values.dry,
|
|
959
|
+
onProgress: (message) => console.error(` … ${message}`)
|
|
960
|
+
});
|
|
961
|
+
if (values.dry) {
|
|
962
|
+
console.log(formatPlan$1(result.plan));
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
console.log(`✓ media → ${values.out} (${result.written.length} written, ${result.skipped} unchanged)`);
|
|
966
|
+
for (const note of result.plan.notes) console.error(` ! ${note}`);
|
|
967
|
+
} catch (err) {
|
|
968
|
+
fail(err.message);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
818
971
|
/**
|
|
819
972
|
* Read + parse + validate the design system at `path`, or exit with a message.
|
|
820
973
|
*
|
|
@@ -824,7 +977,7 @@ async function cmdFavicon(argv) {
|
|
|
824
977
|
* `company.json` doesn't have to maintain a second file for the tooling's sake.
|
|
825
978
|
* `resolveInput` tells them apart by shape and resolves the theme.
|
|
826
979
|
*/
|
|
827
|
-
function
|
|
980
|
+
function loadDesignSystem(path, theme) {
|
|
828
981
|
if (!existsSync(path)) fail(`config not found: ${path}`);
|
|
829
982
|
let raw;
|
|
830
983
|
try {
|
|
@@ -852,11 +1005,11 @@ function loadConfig(path, theme) {
|
|
|
852
1005
|
* Read + resolve a site config, or exit with a message.
|
|
853
1006
|
*
|
|
854
1007
|
* Accepts JSON, or a JS/TS module with a default export — deliberately not YAML,
|
|
855
|
-
* which would mean a parser dependency for one command. `
|
|
1008
|
+
* which would mean a parser dependency for one command. `resolveConfig` is
|
|
856
1009
|
* documented as taking an already-parsed object precisely so the loader is the
|
|
857
1010
|
* consumer's choice; anyone on YAML can convert it or export from a module.
|
|
858
1011
|
*/
|
|
859
|
-
async function
|
|
1012
|
+
async function loadConfig(path, siteEnv) {
|
|
860
1013
|
if (!existsSync(path)) fail(`site config not found: ${path}`);
|
|
861
1014
|
let raw;
|
|
862
1015
|
if (/\.(json)$/.test(path)) try {
|
|
@@ -873,7 +1026,7 @@ async function loadSiteConfig(path, siteEnv) {
|
|
|
873
1026
|
if (raw == null) fail(`${path} has no default export`);
|
|
874
1027
|
}
|
|
875
1028
|
try {
|
|
876
|
-
return
|
|
1029
|
+
return resolveConfig(raw, siteEnv);
|
|
877
1030
|
} catch (err) {
|
|
878
1031
|
fail(err.message);
|
|
879
1032
|
}
|
|
@@ -891,7 +1044,7 @@ async function loadSiteConfig(path, siteEnv) {
|
|
|
891
1044
|
* sprite from it.
|
|
892
1045
|
*
|
|
893
1046
|
* A sibling of `legal` rather than a widening of `lint`: `lint` judges classes
|
|
894
|
-
* against a design-system.json, whereas icons are anchored to a
|
|
1047
|
+
* against a design-system.json, whereas icons are anchored to a Config.
|
|
895
1048
|
*
|
|
896
1049
|
* Exit codes carry the same distinction the Astro integration makes. A name the
|
|
897
1050
|
* config DECLARES but that doesn't resolve is a config error and fails the
|
|
@@ -933,7 +1086,7 @@ async function cmdIcons(argv) {
|
|
|
933
1086
|
allowPositionals: false
|
|
934
1087
|
});
|
|
935
1088
|
const sitePath = resolve(values.site);
|
|
936
|
-
const icons = existsSync(sitePath) ? (await
|
|
1089
|
+
const icons = existsSync(sitePath) ? (await loadConfig(sitePath, values["site-env"])).site.icons ?? {} : {};
|
|
937
1090
|
const ui = icons.ui ?? "fa7-solid";
|
|
938
1091
|
const brand = icons.brand ?? "simple-icons";
|
|
939
1092
|
const weight = icons.weight;
|
|
@@ -1042,9 +1195,9 @@ async function cmdLegal(argv) {
|
|
|
1042
1195
|
if (!LEGAL_OUTPUTS.has(output)) fail(`unknown format "${output}" (expected: ${[...LEGAL_OUTPUTS].join(" | ")})`);
|
|
1043
1196
|
const requested = values.doc ?? [];
|
|
1044
1197
|
for (const d of requested) if (!LEGAL_DOCS.has(d)) fail(`unknown doc "${d}" (expected: ${[...LEGAL_DOCS].join(" | ")})`);
|
|
1045
|
-
const site = await
|
|
1198
|
+
const site = await loadConfig(resolve(values.input), values["site-env"]);
|
|
1046
1199
|
const docs = requested.length ? requested : enabledDocs(site);
|
|
1047
|
-
if (docs.length === 0) fail("no legal documents are enabled — set legal.privacyPolicy.enabled (or termsOfService / cookieConsent) in your
|
|
1200
|
+
if (docs.length === 0) fail("no legal documents are enabled — set site.legal.privacyPolicy.enabled (or termsOfService / cookieConsent) in your config, or name one with --doc");
|
|
1048
1201
|
const files = generateLegal(site, {
|
|
1049
1202
|
docs,
|
|
1050
1203
|
output
|
|
@@ -1059,6 +1212,210 @@ async function cmdLegal(argv) {
|
|
|
1059
1212
|
console.log(`✓ legal → ${outDir} (${Object.keys(files).length} files)`);
|
|
1060
1213
|
console.log(" These are generated from your config and are not legal advice — review them before publishing.");
|
|
1061
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Adapt a `Config` to the indexing module's own option shape.
|
|
1217
|
+
*
|
|
1218
|
+
* A flat field map, as intended: `@getvitops/utils` cannot import from the
|
|
1219
|
+
* generator (the generator already depends on it), so `IndexingConfig` mirrors the
|
|
1220
|
+
* `seo.indexing` block rather than being it. The one piece of judgment here is the
|
|
1221
|
+
* origin — see below.
|
|
1222
|
+
*/
|
|
1223
|
+
function toIndexingConfig(cfg, siteEnv) {
|
|
1224
|
+
const site = cfg.site;
|
|
1225
|
+
const indexing = site.seo?.indexing ?? {};
|
|
1226
|
+
const env = site.environments?.[siteEnv];
|
|
1227
|
+
const canonical = env?.url ?? site.domains?.canonical;
|
|
1228
|
+
return {
|
|
1229
|
+
...canonical ? { canonical } : {},
|
|
1230
|
+
...indexing.sitemapUrl ? { sitemapUrl: indexing.sitemapUrl } : {},
|
|
1231
|
+
...indexing.indexNow ? { indexNow: indexing.indexNow } : {},
|
|
1232
|
+
...indexing.searchConsole ? { searchConsole: indexing.searchConsole } : {},
|
|
1233
|
+
...indexing.priorityUrls ? { priorityUrls: indexing.priorityUrls } : {},
|
|
1234
|
+
...env?.robots ?? site.seo?.robots ? { robots: env?.robots ?? site.seo?.robots } : {}
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* The Search Console credential, from either supported source.
|
|
1239
|
+
*
|
|
1240
|
+
* Never from the config file: it is the one genuine secret in this command, and a
|
|
1241
|
+
* `site.json` is committed. Returns `undefined` rather than failing, so a site
|
|
1242
|
+
* running IndexNow alone doesn't need a GCP project at all.
|
|
1243
|
+
*/
|
|
1244
|
+
function loadServiceAccount() {
|
|
1245
|
+
const inline = process.env.VITOPS_GSC_SERVICE_ACCOUNT;
|
|
1246
|
+
const path = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
1247
|
+
const raw = inline ?? (path && existsSync(path) ? readFileSync(path, "utf8") : void 0);
|
|
1248
|
+
if (!raw) return void 0;
|
|
1249
|
+
try {
|
|
1250
|
+
return parseServiceAccount(raw);
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
fail(`${inline ? "VITOPS_GSC_SERVICE_ACCOUNT" : `GOOGLE_APPLICATION_CREDENTIALS (${path})`}: ${err.message}`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
/** Read a sitemap from a URL or a local path. */
|
|
1256
|
+
const readSitemap = async (source) => {
|
|
1257
|
+
if (/^https?:\/\//i.test(source)) {
|
|
1258
|
+
const res = await fetch(source);
|
|
1259
|
+
if (!res.ok) fail(`could not fetch ${source}: ${res.status}`);
|
|
1260
|
+
return res.text();
|
|
1261
|
+
}
|
|
1262
|
+
if (!existsSync(source)) fail(`sitemap not found: ${source}`);
|
|
1263
|
+
return readFileSync(source, "utf8");
|
|
1264
|
+
};
|
|
1265
|
+
/**
|
|
1266
|
+
* Tell search engines a deploy happened.
|
|
1267
|
+
*
|
|
1268
|
+
* Worth being precise about what this is, because the obvious expectation is
|
|
1269
|
+
* wrong: **Google has no API that requests indexing.** The Search Console button
|
|
1270
|
+
* is not exposed anywhere, URL Inspection is read-only, and the sitemap ping
|
|
1271
|
+
* endpoint was removed in 2023. So the Google half of this command resubmits the
|
|
1272
|
+
* sitemap and then *verifies* the outcome with `--check`; the immediate-push half
|
|
1273
|
+
* is IndexNow, which Google does not participate in.
|
|
1274
|
+
*
|
|
1275
|
+
* The Indexing API is deliberately absent — see `@getvitops/utils/indexing`'s
|
|
1276
|
+
* `gsc.ts`.
|
|
1277
|
+
*/
|
|
1278
|
+
async function cmdIndexing(argv) {
|
|
1279
|
+
const { values } = parseArgs({
|
|
1280
|
+
args: argv,
|
|
1281
|
+
options: {
|
|
1282
|
+
input: {
|
|
1283
|
+
type: "string",
|
|
1284
|
+
short: "i",
|
|
1285
|
+
default: "site.json"
|
|
1286
|
+
},
|
|
1287
|
+
"site-env": {
|
|
1288
|
+
type: "string",
|
|
1289
|
+
default: "production"
|
|
1290
|
+
},
|
|
1291
|
+
sitemap: { type: "string" },
|
|
1292
|
+
urls: { type: "string" },
|
|
1293
|
+
all: {
|
|
1294
|
+
type: "boolean",
|
|
1295
|
+
default: false
|
|
1296
|
+
},
|
|
1297
|
+
dry: {
|
|
1298
|
+
type: "boolean",
|
|
1299
|
+
default: false
|
|
1300
|
+
},
|
|
1301
|
+
check: {
|
|
1302
|
+
type: "boolean",
|
|
1303
|
+
default: false
|
|
1304
|
+
},
|
|
1305
|
+
"new-key": {
|
|
1306
|
+
type: "boolean",
|
|
1307
|
+
default: false
|
|
1308
|
+
},
|
|
1309
|
+
"write-key": { type: "string" },
|
|
1310
|
+
snapshot: {
|
|
1311
|
+
type: "string",
|
|
1312
|
+
default: SNAPSHOT_PATH
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
allowPositionals: false
|
|
1316
|
+
});
|
|
1317
|
+
if (values["new-key"]) {
|
|
1318
|
+
const key = newKey();
|
|
1319
|
+
console.log(key);
|
|
1320
|
+
console.log(`\n Add to your site config:\n "seo": { "indexing": { "indexNow": { "key": "${key}" } } }`);
|
|
1321
|
+
console.log(` Then serve it at /${key}.txt — \`vitops indexing --write-key public\`.`);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const config = toIndexingConfig(await loadConfig(resolve(values.input), values["site-env"]), values["site-env"]);
|
|
1325
|
+
if (values["write-key"] !== void 0) {
|
|
1326
|
+
const key = config.indexNow?.key;
|
|
1327
|
+
if (!key) fail("no site.seo.indexing.indexNow.key in the config — generate one with --new-key");
|
|
1328
|
+
const dir = resolve(values["write-key"]);
|
|
1329
|
+
mkdirSync(dir, { recursive: true });
|
|
1330
|
+
const file = join(dir, `${key}.txt`);
|
|
1331
|
+
writeFileSync(file, keyFileContents(key));
|
|
1332
|
+
console.log(`✓ IndexNow key file → ${file}`);
|
|
1333
|
+
console.log(` It must be served at ${resolveKeyLocation(config) ?? `/${key}.txt`}`);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const sitemapUrl = values.sitemap ?? resolveSitemapUrl(config);
|
|
1337
|
+
if (!sitemapUrl) fail("no sitemap to read — set seo.indexing.sitemapUrl or domains.canonical, or pass --sitemap");
|
|
1338
|
+
const explicitUrls = values.urls ? values.urls.split(",").map((u) => u.trim()).filter(Boolean) : void 0;
|
|
1339
|
+
const current = explicitUrls?.length && !values.check ? [] : (await collectEntries(sitemapUrl, readSitemap)).entries;
|
|
1340
|
+
const snapshotPath = resolve(values.snapshot);
|
|
1341
|
+
const p = plan({
|
|
1342
|
+
config: {
|
|
1343
|
+
...config,
|
|
1344
|
+
sitemapUrl
|
|
1345
|
+
},
|
|
1346
|
+
current,
|
|
1347
|
+
previous: readSnapshot(snapshotPath),
|
|
1348
|
+
explicitUrls,
|
|
1349
|
+
all: values.all
|
|
1350
|
+
});
|
|
1351
|
+
console.log(formatPlan(p));
|
|
1352
|
+
if (p.blocked) return;
|
|
1353
|
+
if (values.dry) {
|
|
1354
|
+
console.log("\n(--dry: nothing was submitted)");
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
const account = loadServiceAccount();
|
|
1358
|
+
let failed = false;
|
|
1359
|
+
if (values.check) {
|
|
1360
|
+
if (!p.searchConsole.siteUrl) fail("--check needs seo.indexing.searchConsole.siteUrl (the Search Console property)");
|
|
1361
|
+
if (!account) fail("--check needs a Search Console service account in VITOPS_GSC_SERVICE_ACCOUNT or GOOGLE_APPLICATION_CREDENTIALS");
|
|
1362
|
+
if (p.check.length === 0) fail("--check needs seo.indexing.priorityUrls — the pages whose indexing matters");
|
|
1363
|
+
const token = await getAccessToken(account);
|
|
1364
|
+
console.log(`\nInspecting ${p.check.length} priority URL(s):`);
|
|
1365
|
+
for (const url of p.check) {
|
|
1366
|
+
const r = await inspectUrl(token, p.searchConsole.siteUrl, url);
|
|
1367
|
+
if (r.error) {
|
|
1368
|
+
console.log(` ? ${url} — ${r.error}`);
|
|
1369
|
+
failed = true;
|
|
1370
|
+
} else {
|
|
1371
|
+
console.log(` ${r.indexed ? "✓" : "✗"} ${url} — ${r.coverageState ?? r.verdict ?? "unknown"}`);
|
|
1372
|
+
if (!r.indexed) failed = true;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (failed) {
|
|
1376
|
+
console.error("\n✖ one or more priority URLs are not indexed");
|
|
1377
|
+
process.exit(1);
|
|
1378
|
+
}
|
|
1379
|
+
console.log("\n✓ every priority URL is indexed");
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (p.indexNow.enabled) {
|
|
1383
|
+
const { key, keyLocation } = p.indexNow;
|
|
1384
|
+
const check = await verifyKeyFile(keyLocation, key);
|
|
1385
|
+
if (!check.ok) {
|
|
1386
|
+
console.error(`✖ IndexNow key file check failed — ${check.reason}`);
|
|
1387
|
+
console.error(` Write it with: vitops indexing --write-key <public dir>`);
|
|
1388
|
+
failed = true;
|
|
1389
|
+
} else {
|
|
1390
|
+
let sent = 0;
|
|
1391
|
+
for (const batch of p.indexNow.batches) {
|
|
1392
|
+
const r = await submitBatch(p.indexNow, batch);
|
|
1393
|
+
if (!r.ok) {
|
|
1394
|
+
console.error(`✖ IndexNow: ${r.message ?? r.status}`);
|
|
1395
|
+
failed = true;
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
sent += r.urls;
|
|
1399
|
+
}
|
|
1400
|
+
if (!failed) console.log(`✓ IndexNow: ${sent} URL(s) submitted`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (p.searchConsole.enabled) if (!account) console.log("· Search Console: skipped — no VITOPS_GSC_SERVICE_ACCOUNT / GOOGLE_APPLICATION_CREDENTIALS");
|
|
1404
|
+
else {
|
|
1405
|
+
const r = await submitSitemap(await getAccessToken(account), p.searchConsole.siteUrl, sitemapUrl);
|
|
1406
|
+
if (r.ok) console.log(`✓ Search Console: sitemap resubmitted`);
|
|
1407
|
+
else {
|
|
1408
|
+
console.error(`✖ Search Console: ${r.message}`);
|
|
1409
|
+
failed = true;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (failed) process.exit(1);
|
|
1413
|
+
if (current.length > 0) {
|
|
1414
|
+
writeSnapshot(snapshotPath, toSnapshot(sitemapUrl, current, (/* @__PURE__ */ new Date()).toISOString()));
|
|
1415
|
+
console.log(` state → ${relative(process.cwd(), snapshotPath)}`);
|
|
1416
|
+
}
|
|
1417
|
+
if (p.check.length > 0) console.log(`\n Indexing is not instant. Run \`vitops indexing --check\` in a day or two to see what Google actually did.`);
|
|
1418
|
+
}
|
|
1062
1419
|
function cliVersion() {
|
|
1063
1420
|
try {
|
|
1064
1421
|
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version ?? "0.0.0";
|
|
@@ -1096,7 +1453,7 @@ function cmdDocs(argv) {
|
|
|
1096
1453
|
return;
|
|
1097
1454
|
}
|
|
1098
1455
|
if (topic && !TOPICS[topic]) fail(`unknown topic "${topic}". Valid topics:\n${topicList()}`);
|
|
1099
|
-
const docs = generateDocs(
|
|
1456
|
+
const docs = generateDocs(loadDesignSystem(resolve(values.input), values.theme), generatorAssetsDir());
|
|
1100
1457
|
const paths = values.all ? Object.values(TOPICS).map((t) => t.path) : [TOPICS[topic].path];
|
|
1101
1458
|
process.stdout.write(paths.map((p) => docs[p]).join("\n"));
|
|
1102
1459
|
}
|
|
@@ -1129,7 +1486,7 @@ function cmdLint(argv) {
|
|
|
1129
1486
|
});
|
|
1130
1487
|
const format = values.format;
|
|
1131
1488
|
if (!LINT_FORMATS.has(format)) fail(`unknown format "${format}" (expected: bricks | css | tailwind)`);
|
|
1132
|
-
const ds =
|
|
1489
|
+
const ds = loadDesignSystem(resolve(values.input), values.theme);
|
|
1133
1490
|
const srcDir = resolve(values.src);
|
|
1134
1491
|
if (!existsSync(srcDir)) fail(`source directory not found: ${srcDir}`);
|
|
1135
1492
|
const palette = expandPalette(ds.colors.palette);
|
|
@@ -1182,7 +1539,7 @@ function cmdAgents(argv) {
|
|
|
1182
1539
|
});
|
|
1183
1540
|
const inputRel = values.input;
|
|
1184
1541
|
let ds = null;
|
|
1185
|
-
if (existsSync(resolve(inputRel))) ds =
|
|
1542
|
+
if (existsSync(resolve(inputRel))) ds = loadDesignSystem(resolve(inputRel), values.theme);
|
|
1186
1543
|
else console.warn(` ⚠ ${inputRel} not found — run \`vitops init\` to scaffold one`);
|
|
1187
1544
|
const legacyDocsDir = values["docs-dir"];
|
|
1188
1545
|
let summary;
|
|
@@ -1284,7 +1641,9 @@ async function main() {
|
|
|
1284
1641
|
case "lint": return cmdLint(rest);
|
|
1285
1642
|
case "legal": return cmdLegal(rest);
|
|
1286
1643
|
case "icons": return cmdIcons(rest);
|
|
1287
|
-
|
|
1644
|
+
case "indexing": return cmdIndexing(rest);
|
|
1645
|
+
case "media": return cmdMedia(rest);
|
|
1646
|
+
default: fail(`unknown command "${command}" (expected: generate | init | validate | favicon | agents | docs | lint | legal | icons | indexing | media). Try: vitops --help`);
|
|
1288
1647
|
}
|
|
1289
1648
|
}
|
|
1290
1649
|
main().catch((err) => fail(err.message));
|