@getvitops/cli 8.0.1 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @getvitops/cli
2
2
 
3
+ ## 8.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **Added — `vitops init --site` and `vitops setup`, the scripted client-onboarding path.**
8
+
9
+ `vitops init --site [--name] [--url]` scaffolds a starter `site.json` (the umbrella `Config`,
10
+ embedding a starter design system at `designSystem.themes.default`) instead of a bare
11
+ `design-system.json` — exported as `defaultSiteConfig()` from `@getvitops/generator`, alongside a
12
+ new `CONFIG_SCHEMA_LOCAL_PATH` and an optional `$schema` field on `ConfigSchema` so a scaffolded
13
+ config gets editor autocomplete the same way `design-system.json` already does.
14
+
15
+ `vitops setup <repo|hosting|domain|search|ads|all>` (new `@getvitops/utils/hosting` subpath: a
16
+ pure planner + `wrangler`/`gh`-shelling executors, following the same idempotent
17
+ plan/observe/apply split as `onboarding`/`ads`) automates GitHub repo + branch policy and
18
+ Cloudflare dev D1/R2 + repo secrets, and plans/checks the workers.dev-vs-custom-domain
19
+ consistency without ever applying the cutover itself — see AGENTS.md's new "Client onboarding"
20
+ section for the full design rationale. `search`/`ads` forward to the existing `search setup`/
21
+ `ads setup` subcommands rather than reimplementing them.
22
+
23
+ No consumer-facing change in `@getvitops/vite` or `@getvitops/astro` in this release — bumped
24
+ only because they are in the `fixed` release group with `generator`/`utils`/`cli`.
25
+ `@getvitops/core` does have a real fix in this release; see the separate `banner-hidden-fix`
26
+ changeset.
27
+
28
+ ### Patch Changes
29
+
30
+ - Updated dependencies
31
+ - @getvitops/generator@8.1.0
32
+ - @getvitops/utils@8.1.0
33
+
3
34
  ## 8.0.1
4
35
 
5
36
  ### Patch Changes
package/dist/cli.mjs CHANGED
@@ -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:
@@ -574,6 +577,61 @@ Ads lint options:
574
577
  off (every conversion arrives unattributed), and a configured property with no
575
578
  tag ID. Exits non-zero on a finding.
576
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
+
577
635
  Media options:
578
636
  --raw <dir> Directory of unprocessed video, walked recursively
579
637
  (default: ./raw)
@@ -615,6 +673,18 @@ const ADS_HELP = `vitops ads — link this site to its ad properties
615
673
  vitops ads tags [opts] Print the consent-gated pixel snippets
616
674
  vitops ads lint [opts] Report ad properties the rest of the config can't see
617
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
+
618
688
  Run \`vitops --help\` for the full option list for each subcommand.
619
689
  `;
620
690
  /**
@@ -636,6 +706,7 @@ const COMMANDS = [
636
706
  "icons",
637
707
  "search",
638
708
  "ads",
709
+ "setup",
639
710
  "media"
640
711
  ];
641
712
  /**
@@ -665,7 +736,7 @@ const wantsHelp = (argv) => argv.includes("-h") || argv.includes("--help");
665
736
  /** The help text to print for `vitops <command> [sub] --help`. */
666
737
  function helpFor(command, argv) {
667
738
  const sub = argv[0] && !argv[0].startsWith("-") ? argv[0] : void 0;
668
- 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;
669
740
  }
670
741
 
671
742
  //#endregion
@@ -1480,6 +1551,421 @@ async function cmdAds(argv) {
1480
1551
  }
1481
1552
  }
1482
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
+
1483
1969
  //#endregion
1484
1970
  //#region src/agents.ts
1485
1971
  /**
@@ -2397,6 +2883,7 @@ function lintSource(files, v, format) {
2397
2883
  * vitops reviews [--input <site.json>] [--location <key>] [--dry] [--check] [--out <dir>]
2398
2884
  * vitops search setup [--domain <name>] [--dry] [--check] | notify [--dry] [--all] [--check]
2399
2885
  * vitops ads setup [--provider <name>] [--dry] [--check] | tags | lint
2886
+ * vitops setup repo | hosting | domain | search | ads | all [--dry] [--check]
2400
2887
  * vitops media [--raw <dir>] [--out <dir>] [--force] [--dry]
2401
2888
  *
2402
2889
  * Thin wrapper over @getvitops/generator (generation) and @getvitops/utils (favicons).
@@ -3349,6 +3836,7 @@ async function main() {
3349
3836
  case "icons": return cmdIcons(rest);
3350
3837
  case "search": return cmdSearch(rest);
3351
3838
  case "ads": return cmdAds(rest);
3839
+ case "setup": return cmdSetup(rest);
3352
3840
  case "media": return cmdMedia(rest);
3353
3841
  default: fail(`unknown command "${command}" (expected: ${COMMANDS.join(" | ")}). Try: vitops --help`);
3354
3842
  }