@getvitops/cli 8.0.0 → 8.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/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  import { parseArgs } from "node:util";
4
4
  import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
5
  import { dirname, extname, join, parse, relative, resolve } from "node:path";
6
- import { JURISDICTION_COUNTRIES, NUMERIC_STEPS, SCHEMA_LOCAL_PATH, buildIconSprite, defaultConfig, enabledDocs, expandPalette, functionalRole, generate, generateDocs, generateLegal, isConfig, movedTokens, processorsMissingLocation, renderRobotsTxt, resolveConfig, resolveInput, resolveTheme, roleColorUtilities, roleHue, roleKind, validate } from "@getvitops/generator";
6
+ 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";
7
7
  import { collectIconRefs, generateFavicons, generateIconInclude, resolveIcon, scanFiles } from "@getvitops/utils";
8
8
  import { SCOPES, SNAPSHOT_PATH, adcCredentialsPath, adcQuotaProject, collectEntries, formatPlan, googleAccessToken, inspectUrl, keyFileContents, newKey, parseAdcUser, parseServiceAccount, plan, readSnapshot, resolveKeyLocation, resolveSitemapUrl, submitBatch, submitSitemap, toSnapshot, verifyKeyFile, writeSnapshot } from "@getvitops/utils/indexing";
9
9
  import { REVIEWS_CACHE_PATH, fetchReviews, formatPlan as formatPlan$1, mergeCache, plan as plan$1, readCache, selectReviews, toCache, writeCache } from "@getvitops/utils/reviews";
@@ -14,6 +14,8 @@ import { addSite, backoffSchedule, createApexTxt, findZoneId, formatPlan as form
14
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";
15
15
  import { PLATFORM_PARAMS } from "@getvitops/utils/tracking";
16
16
  import { createInterface } from "node:readline/promises";
17
+ import { execFileSync } from "node:child_process";
18
+ import { adminCollaboratorIds, branches, createD1, createDevEnvironment, createProdEnvironment, createR2Bucket, createRepo, defaultBranch, environments, findD1, findR2Bucket, formatPlan as formatPlan$5, formatSummary as formatSummary$2, ghAuthed, mainProtected, originRemote, plan as plan$4, planDomainConsistency, protectMain, pushBranches, repoNwo, secretsPresent, setDefaultBranch, setSecret, wranglerWhoami } from "@getvitops/utils/hosting";
17
19
 
18
20
  //#region src/args.ts
19
21
  /**
@@ -326,6 +328,7 @@ Usage:
326
328
  vitops icons [options] Report which icons your source uses, and build the sprite
327
329
  vitops search <sub> [opts] Search Console: onboard domains (setup) + notify deploys (notify)
328
330
  vitops ads <sub> [opts] Ad properties: verify domains (setup) + emit pixels (tags) + lint
331
+ vitops setup <sub> [opts] Scripted onboarding: repo, hosting, domain, search, ads, all
329
332
  vitops media [options] Encode raw video into web-ready WebM + MP4 + poster
330
333
 
331
334
  Generate options:
@@ -344,8 +347,15 @@ Generate options:
344
347
  --site-env <env> Environment whose A/B variant applies (default: production)
345
348
 
346
349
  Init options:
347
- -o, --out <path> Where to write (default: ./design-system.json)
350
+ -o, --out <path> Where to write (default: ./design-system.json, or
351
+ ./site.json with --site)
348
352
  --force Overwrite an existing file
353
+ --site Write a site config (Config) instead of a bare
354
+ design-system.json — embeds a starter design system at
355
+ designSystem.themes.default, since a Config can't
356
+ merely reference a sibling file
357
+ --name <name> organization.name, when --site is set
358
+ --url <url> site.environments.{production,dev}.url, when --site is set
349
359
 
350
360
  Validate options:
351
361
  <file> Config to check (default: ./design-system.json). Either
@@ -381,6 +391,7 @@ Docs options:
381
391
 
382
392
  Icons options:
383
393
  -i, --input <path> Site config carrying the "icons" block (default: ./site.json)
394
+ --site-env <env> Environment whose A/B variant applies (default: production)
384
395
  --src <dir> Source to scan for icon usage (default: ./src)
385
396
  --sprite Also build the SVG sprite
386
397
  -o, --out <dir> Where to write icons.svg with --sprite (default: ./dist)
@@ -566,6 +577,61 @@ Ads lint options:
566
577
  off (every conversion arrives unattributed), and a configured property with no
567
578
  tag ID. Exits non-zero on a finding.
568
579
 
580
+ Setup repo options:
581
+ --dry Print the plan and exit
582
+ --check Report drift/blocked steps and exit non-zero, mutating nothing
583
+ --public Create the repo public (default: private)
584
+ --platform <p> cloudflare (default) | node — shown for parity with the hosting stage's plan
585
+ --no-dev-resources Same — this stage never provisions D1/R2 either way
586
+
587
+ Creates the GitHub repo (via \`gh\`), pushes main + dev, makes dev the default
588
+ branch, and requests main branch protection + the production/dev environments.
589
+ Idempotent — a re-run of a fully set-up repo is all skips. Branch protection and
590
+ environment reviewers 403 on a private repo without GitHub Pro/Team; that is
591
+ reported as blocked, with the manual Settings path, never as a failure.
592
+
593
+ Setup hosting options:
594
+ --dry Print the plan and exit
595
+ --check Report drift/blocked steps and exit non-zero, mutating nothing
596
+ --platform <p> cloudflare (default) | node
597
+ --no-dev-resources Skip D1/R2 — a pure static Workers Assets project has
598
+ nothing to provision, just the account id + token
599
+
600
+ Provisions the isolated dev D1 database + R2 bucket (cloudflare) and sets the
601
+ DEV_D1_ID / CLOUDFLARE_ACCOUNT_ID repo secrets, once \`vitops setup repo\` has run.
602
+ CLOUDFLARE_API_TOKEN is always reported as blocked — no API can create one; see
603
+ the printed dashboard link. Requires \`npx wrangler login\` first.
604
+
605
+ Setup domain options:
606
+ -i, --input <path> Site config (default: ./site.json)
607
+ --site-env <env> Environment to check (default: production)
608
+ --dry Print the plan and exit
609
+ --check Exit non-zero if the domain state is inconsistent
610
+ --domain <apex> Print the manual cutover steps for this apex
611
+
612
+ Plans and checks the consistency between wrangler.jsonc's workers.dev/custom-domain
613
+ state and site.json's environments/domains/robots — it NEVER edits wrangler.jsonc
614
+ or deploys. Cutover overwrites the zone's DNS record and can take an existing live
615
+ site down; that step is manual, on purpose. See SETUP.html's "Domain strategy" stage.
616
+
617
+ Setup search options:
618
+ Forwards every option to \`vitops search setup\` unchanged — see "Search setup
619
+ options:" above.
620
+
621
+ Setup ads options:
622
+ Forwards every option to \`vitops ads setup\` unchanged — see "Ads setup options:"
623
+ above.
624
+
625
+ Setup all options:
626
+ --dry Print every stage's plan and exit
627
+ --check Report drift across every stage and exit non-zero
628
+ --public Create the repo public, for the repo stage
629
+ --platform <p> cloudflare (default) | node, for the repo + hosting stages
630
+ --no-dev-resources Skip D1/R2 in the hosting stage (repo stage never provisions them)
631
+
632
+ Runs repo, hosting, search, ads in order. Stops at the first stage that fails;
633
+ a blocked step (the API token, GitHub Pro/Team gating) is reported, not fatal.
634
+
569
635
  Media options:
570
636
  --raw <dir> Directory of unprocessed video, walked recursively
571
637
  (default: ./raw)
@@ -607,6 +673,18 @@ const ADS_HELP = `vitops ads — link this site to its ad properties
607
673
  vitops ads tags [opts] Print the consent-gated pixel snippets
608
674
  vitops ads lint [opts] Report ad properties the rest of the config can't see
609
675
 
676
+ Run \`vitops --help\` for the full option list for each subcommand.
677
+ `;
678
+ const SETUP_HELP$1 = `vitops setup — the scripted path from an empty scaffold to a live site
679
+
680
+ vitops setup repo [opts] GitHub repo + branch policy (main protected, dev open)
681
+ vitops setup hosting [opts] Dev D1 + R2 + repo secrets (cloudflare) or a Node target
682
+ vitops setup domain [opts] Plan/check the workers.dev-vs-custom-domain consistency
683
+ (never applies the cutover — see SETUP.html)
684
+ vitops setup search [opts] Forwards to \`vitops search setup\`
685
+ vitops setup ads [opts] Forwards to \`vitops ads setup\`
686
+ vitops setup all [opts] Run repo, hosting, search, ads in order
687
+
610
688
  Run \`vitops --help\` for the full option list for each subcommand.
611
689
  `;
612
690
  /**
@@ -628,6 +706,7 @@ const COMMANDS = [
628
706
  "icons",
629
707
  "search",
630
708
  "ads",
709
+ "setup",
631
710
  "media"
632
711
  ];
633
712
  /**
@@ -657,7 +736,7 @@ const wantsHelp = (argv) => argv.includes("-h") || argv.includes("--help");
657
736
  /** The help text to print for `vitops <command> [sub] --help`. */
658
737
  function helpFor(command, argv) {
659
738
  const sub = argv[0] && !argv[0].startsWith("-") ? argv[0] : void 0;
660
- return (sub ? helpSection(command, sub) : void 0) ?? helpSection(command) ?? (command === "search" ? SEARCH_HELP : command === "ads" ? ADS_HELP : void 0) ?? HELP;
739
+ return (sub ? helpSection(command, sub) : void 0) ?? helpSection(command) ?? (command === "search" ? SEARCH_HELP : command === "ads" ? ADS_HELP : command === "setup" ? SETUP_HELP$1 : void 0) ?? HELP;
661
740
  }
662
741
 
663
742
  //#endregion
@@ -1472,6 +1551,421 @@ async function cmdAds(argv) {
1472
1551
  }
1473
1552
  }
1474
1553
 
1554
+ //#endregion
1555
+ //#region src/commands/setup.ts
1556
+ /**
1557
+ * `vitops setup` — the scripted path from an empty scaffold to a live site.
1558
+ * Subcommands: `repo | hosting | domain | search | ads | all`.
1559
+ *
1560
+ * Absorbs what each template's `scripts/init-{github,hosting}.mjs` used to do
1561
+ * template-locally, so the capability reaches every consumer, not just a fresh
1562
+ * `vp create` scaffold. `repo`/`hosting` follow the pure-planner/executor split in
1563
+ * `@getvitops/utils/hosting` — see that module's docblock. `search`/`ads` are not
1564
+ * reimplemented: this shells out to the CLI's own `search setup`/`ads setup`
1565
+ * subcommands as child processes, so their behaviour (prompting, `--dry`,
1566
+ * `--check`, writing the config patch back) stays exactly what it already is,
1567
+ * with no risk of the two copies drifting.
1568
+ *
1569
+ * `domain` only ever plans and checks the workers.dev-vs-custom-domain
1570
+ * consistency — it never touches `wrangler.jsonc` or deploys anything. Cutover
1571
+ * overwrites the zone's DNS record and can take an existing live site down; that
1572
+ * is a human's call, made in `SETUP.html`, not a step this command applies.
1573
+ */
1574
+ const SETUP_HELP = `vitops setup <sub> [options]
1575
+
1576
+ vitops setup repo [--dry] [--check] [--public]
1577
+ vitops setup hosting [--dry] [--check] [--platform cloudflare|node]
1578
+ vitops setup domain [--dry] [--check] [--domain <apex>]
1579
+ vitops setup search [options] (forwards to \`vitops search setup\`)
1580
+ vitops setup ads [options] (forwards to \`vitops ads setup\`)
1581
+ vitops setup all [--dry] [--check] [--public] [--platform <p>]
1582
+
1583
+ --dry prints the plan and stops. --check reports drift/blocked steps and exits
1584
+ non-zero, mutating nothing. Neither creates the Cloudflare API token or performs
1585
+ a custom-domain cutover — both are named as manual steps; see SETUP.html.`;
1586
+ function projectName() {
1587
+ const path = resolve("package.json");
1588
+ if (!existsSync(path)) fail("package.json not found — run this from the project root");
1589
+ const pkg = JSON.parse(readFileSync(path, "utf8"));
1590
+ if (!pkg.name) fail("package.json has no \"name\" field");
1591
+ return pkg.name.replace(/^@[^/]+\//, "");
1592
+ }
1593
+ async function gatherHostingState(name, platform) {
1594
+ const [wrangler, gh, origin] = await Promise.all([
1595
+ platform === "cloudflare" ? wranglerWhoami() : Promise.resolve({ authed: false }),
1596
+ ghAuthed(),
1597
+ originRemote()
1598
+ ]);
1599
+ const nwo = origin ? await repoNwo() : void 0;
1600
+ const [branchesList, def, protectedMain, envs, secrets, d1Id, r2] = await Promise.all([
1601
+ branches(),
1602
+ nwo ? defaultBranch(nwo) : Promise.resolve(void 0),
1603
+ nwo ? mainProtected(nwo) : Promise.resolve(false),
1604
+ nwo ? environments(nwo) : Promise.resolve([]),
1605
+ nwo ? secretsPresent(nwo) : Promise.resolve([]),
1606
+ platform === "cloudflare" ? findD1(`${name}-dev`) : Promise.resolve(void 0),
1607
+ platform === "cloudflare" ? findR2Bucket(`${name}-media-dev`) : Promise.resolve(false)
1608
+ ]);
1609
+ return {
1610
+ wranglerAuthed: wrangler.authed,
1611
+ ..."accountId" in wrangler && wrangler.accountId ? { accountId: wrangler.accountId } : {},
1612
+ d1Present: Boolean(d1Id),
1613
+ ...d1Id ? { d1Id } : {},
1614
+ r2Present: r2,
1615
+ ghAuthed: gh,
1616
+ ...origin ? { originRemote: origin } : {},
1617
+ ...nwo ? { repoNwo: nwo } : {},
1618
+ branches: branchesList,
1619
+ ...def ? { defaultBranch: def } : {},
1620
+ mainProtected: protectedMain,
1621
+ environments: envs,
1622
+ secretsPresent: secrets
1623
+ };
1624
+ }
1625
+ async function cmdSetupRepo(argv) {
1626
+ const a = parse$1(argv, {
1627
+ dry: {
1628
+ type: "boolean",
1629
+ default: false
1630
+ },
1631
+ check: {
1632
+ type: "boolean",
1633
+ default: false
1634
+ },
1635
+ public: {
1636
+ type: "boolean",
1637
+ default: false
1638
+ },
1639
+ platform: {
1640
+ type: "string",
1641
+ default: "cloudflare"
1642
+ },
1643
+ "no-dev-resources": {
1644
+ type: "boolean",
1645
+ default: false
1646
+ }
1647
+ });
1648
+ const platform = a.str("platform");
1649
+ if (platform !== "cloudflare" && platform !== "node") fail(`--platform "${platform}" must be cloudflare or node`);
1650
+ const name = projectName();
1651
+ const setup = {
1652
+ platform,
1653
+ projectName: name,
1654
+ repoVisibility: a.flag("public") ? "public" : "private",
1655
+ devResources: !a.flag("no-dev-resources")
1656
+ };
1657
+ const state = await gatherHostingState(name, setup.platform);
1658
+ const p = plan$4(setup, state);
1659
+ const dry = a.flag("dry");
1660
+ const check = a.flag("check");
1661
+ if (dry || check) {
1662
+ console.log(formatPlan$5(p));
1663
+ if (check && (p.repo.action !== "skip" || p.branches.action !== "skip" || p.defaultBranch.action !== "skip")) {
1664
+ console.error("\n✖ repo/branch setup is not complete");
1665
+ process.exit(1);
1666
+ }
1667
+ return;
1668
+ }
1669
+ const results = [];
1670
+ if (p.repo.action === "create") {
1671
+ const r = await createRepo(name, setup.repoVisibility);
1672
+ results.push({
1673
+ step: "repo",
1674
+ status: r.ok ? "created" : "failed",
1675
+ detail: r.ok ? name : r.message ?? "failed"
1676
+ });
1677
+ if (r.ok) {
1678
+ const push = await pushBranches();
1679
+ results.push({
1680
+ step: "branches",
1681
+ status: push.ok ? "created" : "failed",
1682
+ detail: push.ok ? "main + dev pushed" : push.message ?? "failed"
1683
+ });
1684
+ }
1685
+ } else results.push({
1686
+ step: "repo",
1687
+ status: "present",
1688
+ detail: p.repo.detail
1689
+ });
1690
+ const nwo = state.repoNwo ?? await repoNwo();
1691
+ if (nwo) {
1692
+ if (p.defaultBranch.action !== "skip") {
1693
+ const r = await setDefaultBranch(nwo, "dev");
1694
+ results.push({
1695
+ step: "default branch",
1696
+ status: r.ok ? "updated" : "failed",
1697
+ detail: r.ok ? "dev" : r.message ?? "failed"
1698
+ });
1699
+ } else results.push({
1700
+ step: "default branch",
1701
+ status: "present",
1702
+ detail: "dev"
1703
+ });
1704
+ if (p.mainProtection.action === "blocked") {
1705
+ const r = await protectMain(nwo);
1706
+ results.push({
1707
+ step: "main protection",
1708
+ status: r.ok ? "created" : "blocked",
1709
+ detail: r.ok ? "protected" : r.message ?? p.mainProtection.detail
1710
+ });
1711
+ } else results.push({
1712
+ step: "main protection",
1713
+ status: "present",
1714
+ detail: p.mainProtection.detail
1715
+ });
1716
+ if (p.prodEnvironment.action === "blocked") {
1717
+ const ids = await adminCollaboratorIds(nwo);
1718
+ const r = await createProdEnvironment(nwo, ids);
1719
+ results.push({
1720
+ step: "production env",
1721
+ status: r.ok ? "created" : "blocked",
1722
+ detail: r.ok ? `${ids.length} reviewer(s)` : r.message ?? p.prodEnvironment.detail
1723
+ });
1724
+ } else results.push({
1725
+ step: "production env",
1726
+ status: "present",
1727
+ detail: p.prodEnvironment.detail
1728
+ });
1729
+ if (p.devEnvironment.action === "create") {
1730
+ const r = await createDevEnvironment(nwo);
1731
+ results.push({
1732
+ step: "dev env",
1733
+ status: r.ok ? "created" : "failed",
1734
+ detail: r.ok ? "no required reviewers" : r.message ?? "failed"
1735
+ });
1736
+ } else results.push({
1737
+ step: "dev env",
1738
+ status: "present",
1739
+ detail: p.devEnvironment.detail
1740
+ });
1741
+ } else results.push({
1742
+ step: "branch policy",
1743
+ status: "blocked",
1744
+ detail: "no GitHub repo yet — run this again once \"repo\" succeeds"
1745
+ });
1746
+ console.log(formatSummary$2(results));
1747
+ if (results.some((r) => r.status === "failed")) process.exit(1);
1748
+ }
1749
+ async function cmdSetupHosting(argv) {
1750
+ const a = parse$1(argv, {
1751
+ dry: {
1752
+ type: "boolean",
1753
+ default: false
1754
+ },
1755
+ check: {
1756
+ type: "boolean",
1757
+ default: false
1758
+ },
1759
+ platform: {
1760
+ type: "string",
1761
+ default: "cloudflare"
1762
+ },
1763
+ "no-dev-resources": {
1764
+ type: "boolean",
1765
+ default: false
1766
+ }
1767
+ });
1768
+ const platform = a.str("platform");
1769
+ if (platform !== "cloudflare" && platform !== "node") fail(`--platform "${platform}" must be cloudflare or node`);
1770
+ const name = projectName();
1771
+ const setup = {
1772
+ platform,
1773
+ projectName: name,
1774
+ repoVisibility: "private",
1775
+ devResources: !a.flag("no-dev-resources")
1776
+ };
1777
+ const state = await gatherHostingState(name, platform);
1778
+ const p = plan$4(setup, state);
1779
+ const dry = a.flag("dry");
1780
+ const check = a.flag("check");
1781
+ if (platform === "node") {
1782
+ console.log("Hosting target \"node\" has no cloud resources to provision — see the template README's \"Hosting\" section.");
1783
+ return;
1784
+ }
1785
+ if (!state.wranglerAuthed && !dry) fail("wrangler is not logged in — run `npx wrangler login` first");
1786
+ if (dry || check) {
1787
+ console.log(formatPlan$5(p));
1788
+ if (check && (p.d1.action !== "skip" || p.r2.action !== "skip")) {
1789
+ console.error("\n✖ hosting resources are not fully provisioned");
1790
+ process.exit(1);
1791
+ }
1792
+ return;
1793
+ }
1794
+ const results = [];
1795
+ let d1Id = state.d1Id;
1796
+ if (p.d1.action === "create") {
1797
+ const r = await createD1(`${name}-dev`);
1798
+ results.push({
1799
+ step: "dev D1",
1800
+ status: r.ok ? "created" : "failed",
1801
+ detail: r.ok ? r.id ?? "" : r.message ?? "failed"
1802
+ });
1803
+ if (r.ok) d1Id = r.id;
1804
+ } else results.push({
1805
+ step: "dev D1",
1806
+ status: "present",
1807
+ detail: p.d1.detail
1808
+ });
1809
+ if (p.r2.action === "create") {
1810
+ const r = await createR2Bucket(`${name}-media-dev`);
1811
+ results.push({
1812
+ step: "dev R2",
1813
+ status: r.ok ? "created" : "failed",
1814
+ detail: r.ok ? `${name}-media-dev` : r.message ?? "failed"
1815
+ });
1816
+ } else results.push({
1817
+ step: "dev R2",
1818
+ status: "present",
1819
+ detail: p.r2.detail
1820
+ });
1821
+ if ((state.repoNwo ?? await repoNwo()) && state.ghAuthed) {
1822
+ if (d1Id && !state.secretsPresent.includes("DEV_D1_ID")) {
1823
+ const r = await setSecret("DEV_D1_ID", d1Id);
1824
+ results.push({
1825
+ step: "secret DEV_D1_ID",
1826
+ status: r.ok ? "created" : "failed",
1827
+ detail: r.ok ? "set" : r.message ?? "failed"
1828
+ });
1829
+ } else results.push({
1830
+ step: "secret DEV_D1_ID",
1831
+ status: state.secretsPresent.includes("DEV_D1_ID") ? "present" : "blocked",
1832
+ detail: d1Id ?? "D1 id unknown"
1833
+ });
1834
+ if (state.accountId && !state.secretsPresent.includes("CLOUDFLARE_ACCOUNT_ID")) {
1835
+ const r = await setSecret("CLOUDFLARE_ACCOUNT_ID", state.accountId);
1836
+ results.push({
1837
+ step: "secret CLOUDFLARE_ACCOUNT_ID",
1838
+ status: r.ok ? "created" : "failed",
1839
+ detail: r.ok ? "set" : r.message ?? "failed"
1840
+ });
1841
+ } else results.push({
1842
+ step: "secret CLOUDFLARE_ACCOUNT_ID",
1843
+ status: state.secretsPresent.includes("CLOUDFLARE_ACCOUNT_ID") ? "present" : "blocked",
1844
+ detail: state.accountId ?? "unknown"
1845
+ });
1846
+ } else results.push({
1847
+ step: "secrets",
1848
+ status: "blocked",
1849
+ detail: "no GitHub repo wired up yet — run `vitops setup repo` first"
1850
+ });
1851
+ results.push({
1852
+ step: "secret CLOUDFLARE_API_TOKEN",
1853
+ status: "blocked",
1854
+ detail: p.secrets["CLOUDFLARE_API_TOKEN"]?.detail ?? "create it by hand"
1855
+ });
1856
+ console.log(formatSummary$2(results));
1857
+ for (const n of p.notes) console.log(`! ${n}`);
1858
+ if (results.some((r) => r.status === "failed")) process.exit(1);
1859
+ }
1860
+ async function cmdSetupDomain(argv) {
1861
+ const a = parse$1(argv, {
1862
+ ...SITE_OPTS,
1863
+ dry: {
1864
+ type: "boolean",
1865
+ default: false
1866
+ },
1867
+ check: {
1868
+ type: "boolean",
1869
+ default: false
1870
+ },
1871
+ domain: { type: "string" }
1872
+ });
1873
+ const wranglerPath = resolve("wrangler.jsonc");
1874
+ if (!existsSync(wranglerPath)) fail("wrangler.jsonc not found — run this from the project root");
1875
+ const raw = readFileSync(wranglerPath, "utf8");
1876
+ const customDomainMatch = /^\s*"routes":\s*\[\s*\{\s*"pattern":\s*"([^"]+)"/m.exec(raw.split("\n").filter((l) => !l.trim().startsWith("//")).join("\n"));
1877
+ const customDomain = customDomainMatch?.[1] ?? (a.opt("domain") ? void 0 : void 0);
1878
+ const cfg = await loadConfig(a.path("input"), a.str("site-env"));
1879
+ const env = cfg.site.environments[a.str("site-env")] ?? cfg.site.environments["production"];
1880
+ const step = planDomainConsistency({
1881
+ wranglerDev: !raw.includes("\"routes\"") || Boolean(customDomainMatch) === false,
1882
+ ...customDomain ? { customDomain } : {},
1883
+ ...env?.url ? { configuredUrl: env.url } : {},
1884
+ ...cfg.site.domains?.canonical ? { canonical: cfg.site.domains.canonical } : {},
1885
+ ...env?.robots ? { robots: env.robots } : {}
1886
+ });
1887
+ console.log(`${step.action === "skip" ? "·" : "?"} domain consistency ${step.detail}`);
1888
+ if (a.opt("domain")) console.log(`\nCutover is never applied by this command — see SETUP.html stage 4b. To cut over: uncomment routes in wrangler.jsonc with "pattern": "${a.opt("domain")}", deploy, then update site.domains.canonical and site.environments.${a.str("site-env")}.url to match.`);
1889
+ if (a.flag("check") && step.action !== "skip") process.exit(1);
1890
+ }
1891
+ /** Forward to the CLI's own `search setup` / `ads setup` as a child process — see
1892
+ * this module's docblock for why (their behaviour must not be re-derived). */
1893
+ function forward(sub, argv) {
1894
+ try {
1895
+ execFileSync(process.execPath, [
1896
+ process.argv[1],
1897
+ sub,
1898
+ "setup",
1899
+ ...argv
1900
+ ], { stdio: "inherit" });
1901
+ return true;
1902
+ } catch (err) {
1903
+ if (err.status != null) return false;
1904
+ fail(msg(err));
1905
+ }
1906
+ }
1907
+ async function cmdSetup(argv) {
1908
+ const [sub, ...rest] = argv;
1909
+ switch (sub) {
1910
+ case "repo": return cmdSetupRepo(rest);
1911
+ case "hosting": return cmdSetupHosting(rest);
1912
+ case "domain": return cmdSetupDomain(rest);
1913
+ case "search":
1914
+ if (!forward("search", rest)) process.exit(1);
1915
+ return;
1916
+ case "ads":
1917
+ if (!forward("ads", rest)) process.exit(1);
1918
+ return;
1919
+ case "all": {
1920
+ const a = parse$1(rest, {
1921
+ dry: {
1922
+ type: "boolean",
1923
+ default: false
1924
+ },
1925
+ check: {
1926
+ type: "boolean",
1927
+ default: false
1928
+ },
1929
+ public: {
1930
+ type: "boolean",
1931
+ default: false
1932
+ },
1933
+ platform: {
1934
+ type: "string",
1935
+ default: "cloudflare"
1936
+ },
1937
+ "no-dev-resources": {
1938
+ type: "boolean",
1939
+ default: false
1940
+ }
1941
+ });
1942
+ const dryCheckFlags = [...a.flag("dry") ? ["--dry"] : [], ...a.flag("check") ? ["--check"] : []];
1943
+ const hostingFlags = [
1944
+ ...dryCheckFlags,
1945
+ "--platform",
1946
+ a.str("platform"),
1947
+ ...a.flag("no-dev-resources") ? ["--no-dev-resources"] : []
1948
+ ];
1949
+ console.log("── repo ──");
1950
+ await cmdSetupRepo([...hostingFlags, ...a.flag("public") ? ["--public"] : []]);
1951
+ console.log("\n── hosting ──");
1952
+ await cmdSetupHosting(hostingFlags);
1953
+ console.log("\n── search ──");
1954
+ const searchOk = forward("search", dryCheckFlags);
1955
+ console.log("\n── ads ──");
1956
+ const adsOk = forward("ads", dryCheckFlags);
1957
+ if (!searchOk || !adsOk) process.exit(1);
1958
+ return;
1959
+ }
1960
+ case void 0:
1961
+ case "-h":
1962
+ case "--help":
1963
+ console.log(SETUP_HELP);
1964
+ return;
1965
+ default: fail(`unknown "setup" subcommand "${sub}" (expected: repo | hosting | domain | search | ads | all). Try: vitops --help`);
1966
+ }
1967
+ }
1968
+
1475
1969
  //#endregion
1476
1970
  //#region src/agents.ts
1477
1971
  /**
@@ -2378,7 +2872,7 @@ function lintSource(files, v, format) {
2378
2872
  * `vitops` — CLI for the Vitops design-system generator.
2379
2873
  *
2380
2874
  * vitops generate --input <json> --format <bricks|css|tailwind|design[,…]> --out <dir>
2381
- * vitops init [--out design-system.json] [--force]
2875
+ * vitops init [--out design-system.json] [--force] [--site [--name <n>] [--url <u>]]
2382
2876
  * vitops validate <design-system.json|site.json>
2383
2877
  * vitops favicon --input <svg|png> --out <dir> [--low-res <svg|png>] [--background <hex>]
2384
2878
  * vitops agents [--input <json>] [--out AGENTS.md] [--docs-dir <dir>]
@@ -2389,6 +2883,7 @@ function lintSource(files, v, format) {
2389
2883
  * vitops reviews [--input <site.json>] [--location <key>] [--dry] [--check] [--out <dir>]
2390
2884
  * vitops search setup [--domain <name>] [--dry] [--check] | notify [--dry] [--all] [--check]
2391
2885
  * vitops ads setup [--provider <name>] [--dry] [--check] | tags | lint
2886
+ * vitops setup repo | hosting | domain | search | ads | all [--dry] [--check]
2392
2887
  * vitops media [--raw <dir>] [--out <dir>] [--force] [--dry]
2393
2888
  *
2394
2889
  * Thin wrapper over @getvitops/generator (generation) and @getvitops/utils (favicons).
@@ -2464,27 +2959,53 @@ async function cmdGenerate(argv) {
2464
2959
  fail(msg(err));
2465
2960
  }
2466
2961
  }
2962
+ /**
2963
+ * `vitops init` writes a starter `design-system.json`; `--site` writes a starter
2964
+ * `site.json` instead — the umbrella `Config` that embeds the design system under
2965
+ * `designSystem.themes.default`, since there is no file-level `extends` and a
2966
+ * scaffolded config can't merely reference a sibling file (see `defaultSiteConfig`'s
2967
+ * docblock in `@getvitops/generator`). `--name`/`--url` fill the two facts a
2968
+ * consumer almost certainly has on hand; everything else stays the documented
2969
+ * placeholder, edited by hand or via `vitops setup` (see AGENTS.md's "Client
2970
+ * onboarding" section).
2971
+ */
2467
2972
  function cmdInit(argv) {
2468
2973
  const { values } = parseArgs({
2469
2974
  args: argv,
2470
2975
  options: {
2471
2976
  out: {
2472
2977
  type: "string",
2473
- short: "o",
2474
- default: "design-system.json"
2978
+ short: "o"
2475
2979
  },
2476
2980
  force: {
2477
2981
  type: "boolean",
2478
2982
  default: false
2479
- }
2983
+ },
2984
+ site: {
2985
+ type: "boolean",
2986
+ default: false
2987
+ },
2988
+ name: { type: "string" },
2989
+ url: { type: "string" }
2480
2990
  },
2481
2991
  allowPositionals: false
2482
2992
  });
2483
- const out = resolve(values.out);
2993
+ const out = resolve(values.out ?? (values.site ? "site.json" : "design-system.json"));
2484
2994
  if (existsSync(out) && !values.force) fail(`${out} already exists (use --force to overwrite)`);
2995
+ if (values.site) {
2996
+ const config = defaultSiteConfig({
2997
+ name: values.name,
2998
+ url: values.url
2999
+ });
3000
+ writeFileSync(out, JSON.stringify(config, null, 2) + "\n");
3001
+ console.log(`✓ wrote ${out}`);
3002
+ console.log(` Edit it, then: vitops validate ${relative(".", out)}`);
3003
+ console.log(` Editors read the "$schema" (${CONFIG_SCHEMA_LOCAL_PATH}) for autocomplete + validation — the installed copy, so it matches the toolchain you build with.`);
3004
+ return;
3005
+ }
2485
3006
  writeFileSync(out, JSON.stringify(defaultConfig(), null, 2) + "\n");
2486
3007
  console.log(`✓ wrote ${out}`);
2487
- console.log(` Edit it, then: vitops generate --input ${values.out} --format tailwind --out src/styles`);
3008
+ console.log(` Edit it, then: vitops generate --input ${relative(".", out)} --format tailwind --out src/styles`);
2488
3009
  console.log(` Editors read the "$schema" (${SCHEMA_LOCAL_PATH}) for autocomplete + validation — the installed copy, so it matches the toolchain you build with.`);
2489
3010
  }
2490
3011
  /**
@@ -3315,6 +3836,7 @@ async function main() {
3315
3836
  case "icons": return cmdIcons(rest);
3316
3837
  case "search": return cmdSearch(rest);
3317
3838
  case "ads": return cmdAds(rest);
3839
+ case "setup": return cmdSetup(rest);
3318
3840
  case "media": return cmdMedia(rest);
3319
3841
  default: fail(`unknown command "${command}" (expected: ${COMMANDS.join(" | ")}). Try: vitops --help`);
3320
3842
  }