@getvitops/cli 8.3.0-alpha.4 → 8.3.0-alpha.5

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,34 @@
1
1
  # @getvitops/cli
2
2
 
3
+ ## 8.3.0-alpha.5
4
+
5
+ ### Minor Changes
6
+
7
+ - 6b5ea17: **Leaner scaffolded `AGENTS.md`, and skills named by scope.** The EmDash template's `AGENTS.md` went from 212 to about 70 lines and the static one from 141 to about 50: only always-true rules stay (source of truth, generated CSS, branches, `vp`/`vpr`, where to look), and task-specific guidance moved into skills loaded when the task matches.
8
+
9
+ - **Breaking, `@getvitops/skills`: skills are renamed by scope.** `custom-blocks` → `emdash-custom-blocks`, `content-and-deploys` → `emdash-content-and-deploys`, `contact-form` → `emdash-contact-form`, `csp` → `vitops-csp`, `see-the-page` → `vitops-see-the-page`, `ads` → `vitops-ads`. `emdash-*` skills only apply to an EmDash site; `vitops-*` apply anywhere, and both prefixes are reserved for toolkit skills. New: `emdash-building-pages` (blocks, nesting, page opener, widget areas, escape hatches, layout primitives, `site.chrome`) and `vitops-consent` (split out of `vitops-design-system`, whose description and body now cover styling and tokens only). Existing sites: run `vp install` so `skills experimental_sync` relinks, and replace the per-skill `.agents/skills/<name>` / `.claude/skills/<name>` lines in `.gitignore` with `.agents/skills/vitops-*`, `.agents/skills/emdash-*` and the same two under `.claude/skills/`. A site's own skill must not use those prefixes.
10
+ - **`vitops agents sync`:** the block lists only skills that apply (a project without `emdash` in its dependencies gets no `emdash-*` ones, and they are not linked), summaries are cut at a clause rather than with an ellipsis, the pointer says `vp exec vitops docs`, and the version stamp is gone so the file no longer changes on every CLI bump.
11
+ - **`@getvitops/create`:** both templates ship the trimmed `AGENTS.md`; the project-naming steps moved into the `setup` skill and EmDash 1.x behaviours, legal-page replacement and the audit prerequisites into `emdash-content-and-deploys`. Existing sites: nothing to change beyond the skill rename; delete the sections you no longer need.
12
+
13
+ - bb61a35: **Call tracking numbers: show an ad visitor a number that attributes the phone call.** Two additive config blocks, no CSS, no breaking change — all formats unaffected.
14
+
15
+ - **`site.ads.google.callTracking`** — Google Ads _website call conversions_. `{ label, number, cache? }` makes the consent-gated Google tag fetch a Google forwarding number (Canada supported) and swap it in for visitors who arrived from a Google ad. Google replaces text only, so the capture script also rewrites the matching `tel:` href. Needs `pixelId` and `site.tracking.enabled`; rejected on any other platform (only Google can put a number on the website — Microsoft's exist only inside the ad, in the US/UK/AU).
16
+ - **`site.tracking.phoneNumbers`** — per-platform numbers for every other platform: the real `number`, `sources` keyed by `site.ads` platform (plus `default-paid`), and an informational `provider`. You provision the numbers with a call-tracking service (Twilio, CallRail, …). The landing URL decides first, then the `_ac` cookie, so a visitor who declines `marketing` sees a tracking number on the landing page only. An organic visitor is never swapped, and the server-rendered HTML always carries the real number.
17
+ - The `tel:` tap beacon now carries which number was shown (`phoneSource`), and the conversion notification says so.
18
+ - `vitops ads lint` flags a call-tracking provider that is not in `site.legal.privacyPolicy.processors`, and notes that `callTracking` plus `conversionLabel.call` can count one call twice (make one secondary in Google Ads).
19
+ - `@getvitops/utils/tracking` exports the pure `selectPhoneNumber()` and helpers; the capture script mirrors it and `tracking.test.ts` pins the two.
20
+ - `@getvitops/skills` gains a **`vitops-ads`** skill (overview plus a `call-tracking.md` topic). The templates gitignore synced skills by prefix (`vitops-*`, `emdash-*`), so it needs no `.gitignore` line of its own.
21
+
22
+ `@getvitops/core`, `@getvitops/vite` and `@getvitops/emdash` — no consumer-facing change in these packages.
23
+
24
+ ### Patch Changes
25
+
26
+ - Updated dependencies [785b8b3]
27
+ - Updated dependencies [bb61a35]
28
+ - Updated dependencies [4d94c8b]
29
+ - @getvitops/utils@8.3.0-alpha.5
30
+ - @getvitops/generator@8.3.0-alpha.5
31
+
3
32
  ## 8.3.0-alpha.4
4
33
 
5
34
  ### Minor Changes
package/dist/cli.mjs CHANGED
@@ -1714,11 +1714,13 @@ async function cmdAdsLint(argv) {
1714
1714
  const a = parse$1(argv, SITE_OPTS);
1715
1715
  const cfg = await siteConfig(a);
1716
1716
  const properties = toAdProperties(cfg, void 0);
1717
- if (properties.length === 0) {
1717
+ const phones = cfg.site.tracking?.phoneNumbers;
1718
+ if (properties.length === 0 && !phones) {
1718
1719
  console.log("· no site.ads properties — nothing to check");
1719
1720
  return;
1720
1721
  }
1721
1722
  const findings = [];
1723
+ const notes = [];
1722
1724
  const trackingOn = cfg.site.tracking?.enabled === true;
1723
1725
  for (const p of properties) {
1724
1726
  const platform = AD_PLATFORMS[p.provider];
@@ -1734,6 +1736,14 @@ async function cmdAdsLint(argv) {
1734
1736
  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`);
1735
1737
  }
1736
1738
  }
1739
+ if (phones) {
1740
+ const declared = (cfg.site.legal?.privacyPolicy?.processors ?? []).map((p) => p.name.toLowerCase());
1741
+ if (!phones.provider) findings.push("site.tracking.phoneNumbers has no provider — state which call-tracking service holds these numbers (twilio, callrail, …) so the privacy policy can name it");
1742
+ else if (!declared.some((n) => n.includes(phones.provider.toLowerCase()))) findings.push(`site.tracking.phoneNumbers.provider "${phones.provider}" records callers' numbers but is not in site.legal.privacyPolicy.processors — the generated privacy policy will not name it`);
1743
+ }
1744
+ const google = properties.find((p) => p.provider === "google" && p.enabled !== false);
1745
+ if (google?.callTracking && normalizeConversionLabels(google.conversionLabel).call) notes.push("google: callTracking and conversionLabel.call are both set — one call can count twice. In Google Ads, make one of the two conversion actions secondary.");
1746
+ for (const n of notes) console.warn(`! ${n}`);
1737
1747
  if (findings.length === 0) {
1738
1748
  console.log(`✓ ${properties.length} ad propert${properties.length === 1 ? "y" : "ies"} check out`);
1739
1749
  return;
@@ -3655,6 +3665,20 @@ function linkSkill(linkPath, targetDir) {
3655
3665
  }
3656
3666
  const AGENTS_START = "<!-- vitops:start -->";
3657
3667
  const AGENTS_END = "<!-- vitops:end -->";
3668
+ /** Whether the project depends on EmDash: the `emdash-*` skills only make sense there. */
3669
+ function isEmdashSite(cwd) {
3670
+ try {
3671
+ const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf8"));
3672
+ return Boolean({
3673
+ ...pkg.dependencies,
3674
+ ...pkg.devDependencies
3675
+ }["emdash"]);
3676
+ } catch {
3677
+ return false;
3678
+ }
3679
+ }
3680
+ /** Skill names carry their scope: `emdash-*` needs an EmDash site, `vitops-*` and the rest apply anywhere. */
3681
+ const appliesTo = (skill, emdash) => emdash || !skill.startsWith("emdash-");
3658
3682
  /** A frontmatter `description`, whether a plain line or a folded (`>-`) scalar. */
3659
3683
  function descriptionOf(skillMd) {
3660
3684
  const fm = /^---\n([\s\S]*?)\n---/.exec(skillMd)?.[1] ?? "";
@@ -3667,10 +3691,15 @@ function descriptionOf(skillMd) {
3667
3691
  * One short line saying when to load a skill: its "Use when/for/before …" sentence if it has one
3668
3692
  * (that is the part that answers "is this the one?"), else its first sentence.
3669
3693
  */
3670
- function skillSummary(skillMd, max = 90) {
3694
+ function skillSummary(skillMd, max = 120) {
3671
3695
  const description = descriptionOf(skillMd);
3672
3696
  const line = /\bUse (?:when|for|before|ONLY)\b[^.]*/.exec(description)?.[0] ?? description.split(/(?<=[.!?])\s/)[0] ?? description;
3673
- return line.length > max ? `${line.slice(0, max - 1).trimEnd()}…` : line;
3697
+ if (line.length <= max) return line;
3698
+ const head = line.slice(0, max);
3699
+ const cut = Math.max(head.lastIndexOf(", "), head.lastIndexOf(" or "), head.lastIndexOf(" — "));
3700
+ let out = cut > max / 2 ? head.slice(0, cut) : head.slice(0, head.lastIndexOf(" "));
3701
+ if ((out.match(/\(/g) ?? []).length > (out.match(/\)/g) ?? []).length) out = out.slice(0, out.lastIndexOf("("));
3702
+ return out.replace(/[\s,;:—-]+$/, "");
3674
3703
  }
3675
3704
  /**
3676
3705
  * The skills that belong in the block: those under `.agents/skills` that are links into an installed
@@ -3680,8 +3709,10 @@ function skillSummary(skillMd, max = 90) {
3680
3709
  function discoverBlockSkills(cwd) {
3681
3710
  const dir = resolve(cwd, ".agents", "skills");
3682
3711
  if (!existsSync(dir)) return [];
3712
+ const emdash = isEmdashSite(cwd);
3683
3713
  const out = [];
3684
3714
  for (const name of readdirSync(dir).sort()) {
3715
+ if (!appliesTo(name, emdash)) continue;
3685
3716
  const link = join(dir, name);
3686
3717
  const skillFile = join(link, "SKILL.md");
3687
3718
  if (!lstatSync(link).isSymbolicLink() || !existsSync(skillFile)) continue;
@@ -3701,6 +3732,7 @@ function discoverBlockSkills(cwd) {
3701
3732
  */
3702
3733
  function linkPackagedSkills(cwd) {
3703
3734
  const warnings = [];
3735
+ const emdash = isEmdashSite(cwd);
3704
3736
  let dir = resolve(cwd);
3705
3737
  const { root } = parse(dir);
3706
3738
  for (;;) {
@@ -3708,7 +3740,7 @@ function linkPackagedSkills(cwd) {
3708
3740
  if (existsSync(skills)) {
3709
3741
  for (const name of readdirSync(skills)) {
3710
3742
  const target = join(skills, name);
3711
- if (!existsSync(join(target, "SKILL.md"))) continue;
3743
+ if (!existsSync(join(target, "SKILL.md")) || !appliesTo(name, emdash)) continue;
3712
3744
  for (const base of [".agents", ".claude"]) {
3713
3745
  const warn = linkSkill(resolve(cwd, base, "skills", name), target);
3714
3746
  if (warn) warnings.push(warn);
@@ -3733,8 +3765,7 @@ function renderAgentsBlock(opts) {
3733
3765
  "",
3734
3766
  ...opts.skills.map((s) => `- [${s.name}](${s.path})${s.summary ? ` — ${s.summary}` : ""}`),
3735
3767
  ...opts.skills.length ? [""] : [],
3736
- "Reference, rendered from this project: `vitops docs <topic>` (no topic lists them).",
3737
- `<!-- managed by \`vitops agents sync\` · @getvitops/cli@${opts.version} — edit outside this block -->`,
3768
+ "Reference, rendered from this project: `vp exec vitops docs [topic]` (no topic lists them).",
3738
3769
  AGENTS_END
3739
3770
  ].join("\n");
3740
3771
  }
@@ -5212,13 +5243,6 @@ async function cmdReviews(argv) {
5212
5243
  }
5213
5244
  if (failed) process.exit(1);
5214
5245
  }
5215
- function cliVersion() {
5216
- try {
5217
- return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version ?? "0.0.0";
5218
- } catch {
5219
- return "0.0.0";
5220
- }
5221
- }
5222
5246
  function generatorAssetsDir() {
5223
5247
  const pkg = createRequire(import.meta.url).resolve("@getvitops/generator/package.json");
5224
5248
  return join(dirname(pkg), "assets");
@@ -5496,8 +5520,7 @@ function cmdAgents(rawArgv) {
5496
5520
  }
5497
5521
  const block = legacyBlock ?? renderAgentsBlock({
5498
5522
  config: inputRel,
5499
- skills: discoverBlockSkills(process.cwd()),
5500
- version: cliVersion()
5523
+ skills: discoverBlockSkills(process.cwd())
5501
5524
  });
5502
5525
  const outPath = resolve(values.out);
5503
5526
  const existing = existsSync(outPath) ? readFileSync(outPath, "utf8") : void 0;