aem-eds-kit 0.4.9 → 0.4.12

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/bin/eds.js CHANGED
@@ -1708,14 +1708,134 @@ var init_component = __esm({
1708
1708
  }
1709
1709
  });
1710
1710
 
1711
+ // src/lib/track.ts
1712
+ function trackSnippet(blockName, param, events) {
1713
+ const parts = [` // --- ${TRACK_MARKER} ---`];
1714
+ if (events.click) {
1715
+ parts.push(
1716
+ ` ${param}.addEventListener('click', (ev) => {`,
1717
+ " const el = ev.target.closest('a, button');",
1718
+ " if (!el) return;",
1719
+ ` trackEvent({ event: 'block_click', block: '${blockName}', label: el.dataset.track || el.textContent.trim() });`,
1720
+ " });"
1721
+ );
1722
+ }
1723
+ if (events.submit) {
1724
+ parts.push(
1725
+ ` ${param}.addEventListener('submit', () => {`,
1726
+ ` trackEvent({ event: 'form_submit', block: '${blockName}' });`,
1727
+ " });"
1728
+ );
1729
+ }
1730
+ return parts.join("\n");
1731
+ }
1732
+ function instrumentJs(js, blockName, events) {
1733
+ if (js.includes(TRACK_MARKER)) return { js, changed: false, reason: "already instrumented" };
1734
+ if (!events.click && !events.submit) return { js, changed: false, reason: "no events selected" };
1735
+ const decorate = js.match(
1736
+ /export default (?:async )?function decorate\s*\(\s*([A-Za-z_$][\w$]*)?[^)]*\)\s*\{/
1737
+ );
1738
+ if (!decorate || decorate.index === void 0) {
1739
+ return { js, changed: false, reason: "no `export default function decorate(...)` found" };
1740
+ }
1741
+ const param = decorate[1] || "block";
1742
+ let out = js;
1743
+ if (!out.includes("scripts/analytics.js")) {
1744
+ const imports = [...out.matchAll(/^import .*$/gm)];
1745
+ if (imports.length) {
1746
+ const last = imports[imports.length - 1];
1747
+ const at2 = (last.index ?? 0) + last[0].length;
1748
+ out = `${out.slice(0, at2)}
1749
+ ${IMPORT_LINE}${out.slice(at2)}`;
1750
+ } else {
1751
+ out = `${IMPORT_LINE}
1752
+
1753
+ ${out}`;
1754
+ }
1755
+ }
1756
+ const d2 = out.match(
1757
+ /export default (?:async )?function decorate\s*\(\s*([A-Za-z_$][\w$]*)?[^)]*\)\s*\{/
1758
+ );
1759
+ if (!d2 || d2.index === void 0)
1760
+ return { js: out, changed: false, reason: "decorate not found" };
1761
+ const at = d2.index + d2[0].length;
1762
+ const snippet = trackSnippet(blockName, param, events);
1763
+ out = `${out.slice(0, at)}
1764
+ ${snippet}
1765
+ ${out.slice(at)}`;
1766
+ return { js: out, changed: true };
1767
+ }
1768
+ var TRACK_MARKER, IMPORT_LINE;
1769
+ var init_track = __esm({
1770
+ "src/lib/track.ts"() {
1771
+ "use strict";
1772
+ init_esm_shims();
1773
+ TRACK_MARKER = "analytics (eds track)";
1774
+ IMPORT_LINE = "import { trackEvent } from '../../scripts/analytics.js';";
1775
+ }
1776
+ });
1777
+
1778
+ // src/commands/track.ts
1779
+ var track_exports = {};
1780
+ __export(track_exports, {
1781
+ trackBlock: () => trackBlock
1782
+ });
1783
+ import { existsSync as existsSync7 } from "fs";
1784
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
1785
+ import path9 from "path";
1786
+ import chalk4 from "chalk";
1787
+ async function trackBlock(blockName, options = {}) {
1788
+ const projectRoot = findProjectRoot();
1789
+ if (!projectRoot) {
1790
+ logger.error("Not inside an EDS project (no EDS project markers found).");
1791
+ process.exitCode = 1;
1792
+ return;
1793
+ }
1794
+ logger.logoOnce(logo("Track block"));
1795
+ const jsPath = path9.join(projectRoot, "blocks", blockName, `${blockName}.js`);
1796
+ if (!existsSync7(jsPath)) {
1797
+ logger.error(`Block "${blockName}" not found (no blocks/${blockName}/${blockName}.js).`);
1798
+ process.exitCode = 1;
1799
+ return;
1800
+ }
1801
+ const events = { click: options.click !== false, submit: options.submit !== false };
1802
+ const res = instrumentJs(await readFile5(jsPath, "utf-8"), blockName, events);
1803
+ if (!res.changed) {
1804
+ logger.warn(`Not instrumented: ${res.reason}.`);
1805
+ return;
1806
+ }
1807
+ await writeFile5(jsPath, res.js);
1808
+ logger.info(heading("Instrumented"));
1809
+ const picked = [events.click ? "block_click" : null, events.submit ? "form_submit" : null].filter(
1810
+ Boolean
1811
+ );
1812
+ logger.info(accentLine(`blocks/${blockName}/${blockName}.js`, picked.join(" + ")));
1813
+ if (!existsSync7(path9.join(projectRoot, "scripts", "analytics.js"))) {
1814
+ logger.info("");
1815
+ logger.warn("scripts/analytics.js is missing - run `eds integrate gtm` to create it.");
1816
+ }
1817
+ logger.info("");
1818
+ logger.info(chalk4.dim(" Events push to window.dataLayer via trackEvent - GTM picks them up."));
1819
+ }
1820
+ var init_track2 = __esm({
1821
+ "src/commands/track.ts"() {
1822
+ "use strict";
1823
+ init_esm_shims();
1824
+ init_logger();
1825
+ init_project();
1826
+ init_track();
1827
+ init_ui();
1828
+ }
1829
+ });
1830
+
1711
1831
  // src/commands/migrate.ts
1712
1832
  var migrate_exports = {};
1713
1833
  __export(migrate_exports, {
1714
1834
  migrateComponent: () => migrateComponent
1715
1835
  });
1716
- import { existsSync as existsSync7 } from "fs";
1717
- import { mkdir as mkdir3, writeFile as writeFile5 } from "fs/promises";
1718
- import path9 from "path";
1836
+ import { existsSync as existsSync8 } from "fs";
1837
+ import { mkdir as mkdir3, writeFile as writeFile6 } from "fs/promises";
1838
+ import path10 from "path";
1719
1839
  function clientlibBlockCss(blockName, css, files) {
1720
1840
  return `/* Block: ${blockName} */
1721
1841
  /* Migrated from the component clientlib: ${files.join(", ")}. */
@@ -1763,7 +1883,7 @@ async function migrateComponent(dirArg, options = {}) {
1763
1883
  if (!componentDir && interactive) {
1764
1884
  componentDir = await input({
1765
1885
  message: "Path to the classic component folder",
1766
- validate: (v) => existsSync7(v.trim()) || "Folder not found"
1886
+ validate: (v) => existsSync8(v.trim()) || "Folder not found"
1767
1887
  });
1768
1888
  }
1769
1889
  if (!componentDir) {
@@ -1771,7 +1891,7 @@ async function migrateComponent(dirArg, options = {}) {
1771
1891
  process.exitCode = 1;
1772
1892
  return;
1773
1893
  }
1774
- componentDir = path9.resolve(componentDir.trim());
1894
+ componentDir = path10.resolve(componentDir.trim());
1775
1895
  if (!findDialogFile(componentDir)) {
1776
1896
  logger.error(`No Touch UI dialog (_cq_dialog/.content.xml) found in ${componentDir}.`);
1777
1897
  process.exitCode = 1;
@@ -1785,7 +1905,7 @@ async function migrateComponent(dirArg, options = {}) {
1785
1905
  process.exitCode = 1;
1786
1906
  return;
1787
1907
  }
1788
- const title = await readComponentTitle(componentDir) ?? path9.basename(componentDir);
1908
+ const title = await readComponentTitle(componentDir) ?? path10.basename(componentDir);
1789
1909
  const blockName = options.name && slug2(options.name) || (interactive ? slug2(await input({ message: "Block name", default: slug2(title) })) : slug2(title));
1790
1910
  logger.info(heading("Mapped fields", `${countLeaves(parsed.fields)} from dialog`));
1791
1911
  for (const line of previewFields(parsed.fields)) logger.info(` ${line}`);
@@ -1801,19 +1921,19 @@ async function migrateComponent(dirArg, options = {}) {
1801
1921
  return;
1802
1922
  }
1803
1923
  }
1804
- const blockDir = path9.join(projectRoot, "blocks", blockName);
1805
- if (existsSync7(blockDir)) {
1924
+ const blockDir = path10.join(projectRoot, "blocks", blockName);
1925
+ if (existsSync8(blockDir)) {
1806
1926
  logger.error(
1807
1927
  `Block "${blockName}" already exists at blocks/${blockName}. Use --name to pick another.`
1808
1928
  );
1809
1929
  process.exitCode = 1;
1810
1930
  return;
1811
1931
  }
1812
- const clibDir = options.clientlib ? path9.resolve(options.clientlib.trim()) : await findClientlibDir(componentDir);
1932
+ const clibDir = options.clientlib ? path10.resolve(options.clientlib.trim()) : await findClientlibDir(componentDir);
1813
1933
  let cssContent = migratedBlockCss(blockName);
1814
1934
  let cssMessage = "empty - add styles from clientlib / rendered CSS";
1815
1935
  let preprocessedNote = [];
1816
- if (clibDir && existsSync7(clibDir)) {
1936
+ if (clibDir && existsSync8(clibDir)) {
1817
1937
  const lib = await readClientlibCss(clibDir);
1818
1938
  if (lib.css.trim()) {
1819
1939
  cssContent = clientlibBlockCss(blockName, lib.css, lib.files);
@@ -1822,13 +1942,13 @@ async function migrateComponent(dirArg, options = {}) {
1822
1942
  preprocessedNote = lib.preprocessed;
1823
1943
  }
1824
1944
  await mkdir3(blockDir, { recursive: true });
1825
- await writeFile5(
1826
- path9.join(blockDir, `${blockName}.js`),
1945
+ await writeFile6(
1946
+ path10.join(blockDir, `${blockName}.js`),
1827
1947
  migratedBlockJs(blockName, parsed.fields)
1828
1948
  );
1829
- await writeFile5(path9.join(blockDir, `${blockName}.css`), cssContent);
1830
- await writeFile5(
1831
- path9.join(blockDir, `_${blockName}.json`),
1949
+ await writeFile6(path10.join(blockDir, `${blockName}.css`), cssContent);
1950
+ await writeFile6(
1951
+ path10.join(blockDir, `_${blockName}.json`),
1832
1952
  migratedModelFile(blockName, title, parsed.fields)
1833
1953
  );
1834
1954
  logger.info(heading("Created"));
@@ -1842,6 +1962,16 @@ async function migrateComponent(dirArg, options = {}) {
1842
1962
  logger.warn(`LESS/SCSS found (needs a build, not copied): ${preprocessedNote.join(", ")}`);
1843
1963
  }
1844
1964
  logger.info("");
1965
+ if (interactive) {
1966
+ const addTracking = await confirm({
1967
+ message: "Instrument this block for analytics (dataLayer)?",
1968
+ default: false
1969
+ });
1970
+ if (addTracking) {
1971
+ const { trackBlock: trackBlock2 } = await Promise.resolve().then(() => (init_track2(), track_exports));
1972
+ await trackBlock2(blockName, {});
1973
+ }
1974
+ }
1845
1975
  logger.info(
1846
1976
  box([`"${blockName}" migrated. Next: add CSS, then \`eds block preview ${blockName}\``])
1847
1977
  );
@@ -1866,10 +1996,10 @@ __export(create_exports, {
1866
1996
  createBlock: () => createBlock,
1867
1997
  ueModelFor: () => ueModelFor
1868
1998
  });
1869
- import { existsSync as existsSync8 } from "fs";
1870
- import { mkdir as mkdir4, writeFile as writeFile6 } from "fs/promises";
1871
- import path10 from "path";
1872
- import chalk4 from "chalk";
1999
+ import { existsSync as existsSync9 } from "fs";
2000
+ import { mkdir as mkdir4, writeFile as writeFile7 } from "fs/promises";
2001
+ import path11 from "path";
2002
+ import chalk5 from "chalk";
1873
2003
  function titleCase(name) {
1874
2004
  return name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1875
2005
  }
@@ -1947,8 +2077,8 @@ async function createBlock(name, options = {}) {
1947
2077
  process.exitCode = 1;
1948
2078
  return;
1949
2079
  }
1950
- const blocksDir = path10.join(projectRoot, "blocks", name);
1951
- if (existsSync8(blocksDir)) {
2080
+ const blocksDir = path11.join(projectRoot, "blocks", name);
2081
+ if (existsSync9(blocksDir)) {
1952
2082
  logger.error(`Block "${name}" already exists at ${blocksDir}`);
1953
2083
  process.exitCode = 1;
1954
2084
  return;
@@ -1962,14 +2092,14 @@ async function createBlock(name, options = {}) {
1962
2092
  /* TODO: style ${name} block */
1963
2093
  }
1964
2094
  `;
1965
- await writeFile6(path10.join(blocksDir, `${name}.js`), jsContent);
1966
- await writeFile6(path10.join(blocksDir, `${name}.css`), cssContent);
2095
+ await writeFile7(path11.join(blocksDir, `${name}.js`), jsContent);
2096
+ await writeFile7(path11.join(blocksDir, `${name}.css`), cssContent);
1967
2097
  const withUe = options.ueModel !== false;
1968
2098
  if (withUe) {
1969
- await writeFile6(path10.join(blocksDir, `_${name}.json`), ueModelFor(name));
2099
+ await writeFile7(path11.join(blocksDir, `_${name}.json`), ueModelFor(name));
1970
2100
  }
1971
2101
  logger.success(
1972
- `Block "${name}" created at ${chalk4.cyan(path10.relative(projectRoot, blocksDir))}${withUe ? chalk4.dim(" (+ Universal Editor model)") : ""}`
2102
+ `Block "${name}" created at ${chalk5.cyan(path11.relative(projectRoot, blocksDir))}${withUe ? chalk5.dim(" (+ Universal Editor model)") : ""}`
1973
2103
  );
1974
2104
  }
1975
2105
  var init_create = __esm({
@@ -1983,10 +2113,10 @@ var init_create = __esm({
1983
2113
  });
1984
2114
 
1985
2115
  // src/lib/agents/detect.ts
1986
- import { existsSync as existsSync9 } from "fs";
1987
- import { readFile as readFile5 } from "fs/promises";
2116
+ import { existsSync as existsSync10 } from "fs";
2117
+ import { readFile as readFile6 } from "fs/promises";
1988
2118
  import { homedir } from "os";
1989
- import path11 from "path";
2119
+ import path12 from "path";
1990
2120
  async function whichBinary(name) {
1991
2121
  const { execaCommand } = await import("execa");
1992
2122
  try {
@@ -2014,8 +2144,8 @@ function findFigmaServerKey(node) {
2014
2144
  async function findFigmaMcpServer(configPaths) {
2015
2145
  for (const configPath of configPaths) {
2016
2146
  try {
2017
- if (!existsSync9(configPath)) continue;
2018
- const content = await readFile5(configPath, "utf-8");
2147
+ if (!existsSync10(configPath)) continue;
2148
+ const content = await readFile6(configPath, "utf-8");
2019
2149
  try {
2020
2150
  const key = findFigmaServerKey(JSON.parse(content));
2021
2151
  if (key) return key;
@@ -2037,7 +2167,7 @@ async function detectAgent(preferred) {
2037
2167
  return {
2038
2168
  type: def.type,
2039
2169
  path: binPath,
2040
- configDir: existsSync9(def.configDir) ? def.configDir : void 0,
2170
+ configDir: existsSync10(def.configDir) ? def.configDir : void 0,
2041
2171
  hasFigmaMcp: !!figmaMcpServerName,
2042
2172
  figmaMcpServerName
2043
2173
  };
@@ -2052,7 +2182,7 @@ async function detectAgent(preferred) {
2052
2182
  return {
2053
2183
  type: def.type,
2054
2184
  path: binPath,
2055
- configDir: existsSync9(def.configDir) ? def.configDir : void 0,
2185
+ configDir: existsSync10(def.configDir) ? def.configDir : void 0,
2056
2186
  hasFigmaMcp: !!figmaMcpServerName,
2057
2187
  figmaMcpServerName
2058
2188
  };
@@ -2069,7 +2199,7 @@ async function detectAllAgents() {
2069
2199
  agents.push({
2070
2200
  type: def.type,
2071
2201
  path: binPath,
2072
- configDir: existsSync9(def.configDir) ? def.configDir : void 0,
2202
+ configDir: existsSync10(def.configDir) ? def.configDir : void 0,
2073
2203
  hasFigmaMcp: !!figmaMcpServerName,
2074
2204
  figmaMcpServerName
2075
2205
  });
@@ -2087,22 +2217,22 @@ var init_detect = __esm({
2087
2217
  {
2088
2218
  type: "claude",
2089
2219
  binary: "claude",
2090
- configDir: path11.join(homedir(), ".claude"),
2220
+ configDir: path12.join(homedir(), ".claude"),
2091
2221
  mcpConfigPaths: [
2092
- path11.join(homedir(), ".claude", "plugins.json"),
2093
- path11.join(homedir(), ".claude.json")
2222
+ path12.join(homedir(), ".claude", "plugins.json"),
2223
+ path12.join(homedir(), ".claude.json")
2094
2224
  ]
2095
2225
  },
2096
2226
  {
2097
2227
  type: "cursor",
2098
2228
  binary: "cursor",
2099
- configDir: path11.join(homedir(), ".cursor"),
2100
- mcpConfigPaths: [path11.join(homedir(), ".cursor", "mcp.json")]
2229
+ configDir: path12.join(homedir(), ".cursor"),
2230
+ mcpConfigPaths: [path12.join(homedir(), ".cursor", "mcp.json")]
2101
2231
  },
2102
2232
  {
2103
2233
  type: "codex",
2104
2234
  binary: "codex",
2105
- configDir: path11.join(homedir(), ".codex"),
2235
+ configDir: path12.join(homedir(), ".codex"),
2106
2236
  mcpConfigPaths: []
2107
2237
  }
2108
2238
  ];
@@ -2110,27 +2240,27 @@ var init_detect = __esm({
2110
2240
  });
2111
2241
 
2112
2242
  // src/lib/agents/invoke.ts
2113
- import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
2243
+ import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
2114
2244
  import { tmpdir } from "os";
2115
- import path12 from "path";
2245
+ import path13 from "path";
2116
2246
  import readline from "readline";
2117
- import chalk5 from "chalk";
2247
+ import chalk6 from "chalk";
2118
2248
  async function savePromptToFile(prompt, projectRoot) {
2119
- const base = projectRoot ? path12.join(projectRoot, "tmp") : tmpdir();
2120
- const dir = path12.join(base, "eds-cli");
2249
+ const base = projectRoot ? path13.join(projectRoot, "tmp") : tmpdir();
2250
+ const dir = path13.join(base, "eds-cli");
2121
2251
  await mkdir5(dir, { recursive: true });
2122
2252
  const filename = `from-design-${Date.now()}.md`;
2123
- const filePath = path12.join(dir, filename);
2124
- await writeFile7(filePath, prompt, "utf-8");
2253
+ const filePath = path13.join(dir, filename);
2254
+ await writeFile8(filePath, prompt, "utf-8");
2125
2255
  return filePath;
2126
2256
  }
2127
2257
  async function invokeAgent(agent, promptFile, cwd) {
2128
2258
  const { execa } = await import("execa");
2129
- const prompt = await readFile6(promptFile, "utf-8");
2259
+ const prompt = await readFile7(promptFile, "utf-8");
2130
2260
  try {
2131
2261
  switch (agent.type) {
2132
2262
  case "claude": {
2133
- logger.info(`Running Claude Code ${chalk5.dim("- live log:")}`);
2263
+ logger.info(`Running Claude Code ${chalk6.dim("- live log:")}`);
2134
2264
  const figmaServers = [
2135
2265
  ...agent.figmaMcpServerName ? [agent.figmaMcpServerName] : [],
2136
2266
  "figma-sse",
@@ -2193,10 +2323,10 @@ async function renderClaudeStream(sub, cwd) {
2193
2323
  const ora4 = (await import("ora")).default;
2194
2324
  const start = Date.now();
2195
2325
  const elapsed = () => `${Math.round((Date.now() - start) / 1e3)}s`;
2196
- const spinner = ora4({ text: `thinking ${chalk5.dim(elapsed())}`, color: "yellow" }).start();
2326
+ const spinner = ora4({ text: `thinking ${chalk6.dim(elapsed())}`, color: "yellow" }).start();
2197
2327
  let activity = "thinking";
2198
2328
  const timer = setInterval(() => {
2199
- spinner.text = `${activity} ${chalk5.dim(elapsed())}`;
2329
+ spinner.text = `${activity} ${chalk6.dim(elapsed())}`;
2200
2330
  }, 300);
2201
2331
  const pending = /* @__PURE__ */ new Map();
2202
2332
  const emit = (line) => {
@@ -2222,7 +2352,7 @@ async function renderClaudeStream(sub, cwd) {
2222
2352
  for (const block2 of content) {
2223
2353
  if (block2.type === "text" && "text" in block2) {
2224
2354
  const first = block2.text.trim().split("\n")[0].slice(0, 100);
2225
- if (first) emit(chalk5.dim(first));
2355
+ if (first) emit(chalk6.dim(first));
2226
2356
  } else if (block2.type === "tool_use" && "name" in block2) {
2227
2357
  const id = "id" in block2 && block2.id ? block2.id : `${Math.random()}`;
2228
2358
  pending.set(id, {
@@ -2238,8 +2368,8 @@ async function renderClaudeStream(sub, cwd) {
2238
2368
  const p = pending.get(block2.tool_use_id);
2239
2369
  if (p) {
2240
2370
  const dur = `${((Date.now() - p.start) / 1e3).toFixed(1)}s`;
2241
- const failed = "is_error" in block2 && block2.is_error ? ` ${chalk5.red("failed")}` : "";
2242
- emit(`${p.label} ${chalk5.dim(dur)}${failed}`);
2371
+ const failed = "is_error" in block2 && block2.is_error ? ` ${chalk6.red("failed")}` : "";
2372
+ emit(`${p.label} ${chalk6.dim(dur)}${failed}`);
2243
2373
  pending.delete(block2.tool_use_id);
2244
2374
  }
2245
2375
  }
@@ -2251,9 +2381,9 @@ async function renderClaudeStream(sub, cwd) {
2251
2381
  }
2252
2382
  }
2253
2383
  }
2254
- for (const p of pending.values()) emit(`${p.label} ${chalk5.dim("\u2026")}`);
2384
+ for (const p of pending.values()) emit(`${p.label} ${chalk6.dim("\u2026")}`);
2255
2385
  clearInterval(timer);
2256
- if (result) spinner.succeed(`${brand.accent("agent finished")} ${chalk5.dim(result)}`);
2386
+ if (result) spinner.succeed(`${brand.accent("agent finished")} ${chalk6.dim(result)}`);
2257
2387
  else spinner.stop();
2258
2388
  } catch (err) {
2259
2389
  clearInterval(timer);
@@ -2266,22 +2396,22 @@ function toolLabel(name, input, cwd) {
2266
2396
  const rel = (key) => {
2267
2397
  const p = input?.[key];
2268
2398
  if (typeof p !== "string") return "";
2269
- return p.startsWith(cwd) ? path12.relative(cwd, p) : path12.basename(p);
2399
+ return p.startsWith(cwd) ? path13.relative(cwd, p) : path13.basename(p);
2270
2400
  };
2271
2401
  if (name.includes("figma")) {
2272
2402
  const short = name.split("__").pop() ?? name;
2273
- return `${icon.figma} ${chalk5.bold(short)} ${chalk5.dim("Figma MCP")}`;
2403
+ return `${icon.figma} ${chalk6.bold(short)} ${chalk6.dim("Figma MCP")}`;
2274
2404
  }
2275
2405
  switch (name) {
2276
2406
  case "Write":
2277
2407
  case "Edit":
2278
2408
  case "NotebookEdit":
2279
- return `${brand.accent("\u270E")} ${chalk5.cyan(rel("file_path"))}`;
2409
+ return `${brand.accent("\u270E")} ${chalk6.cyan(rel("file_path"))}`;
2280
2410
  case "Read":
2281
- return chalk5.dim(`\xB7 read ${rel("file_path")}`);
2411
+ return chalk6.dim(`\xB7 read ${rel("file_path")}`);
2282
2412
  case "Bash": {
2283
2413
  const cmd = typeof input?.command === "string" ? input.command.split("\n")[0].slice(0, 70) : "";
2284
- return chalk5.dim(`$ ${cmd}`);
2414
+ return chalk6.dim(`$ ${cmd}`);
2285
2415
  }
2286
2416
  default:
2287
2417
  return `${brand.blue("\u2192")} ${name}`;
@@ -2297,13 +2427,13 @@ var init_invoke = __esm({
2297
2427
  });
2298
2428
 
2299
2429
  // src/lib/config.ts
2300
- import { existsSync as existsSync10 } from "fs";
2301
- import { readFile as readFile7 } from "fs/promises";
2430
+ import { existsSync as existsSync11 } from "fs";
2431
+ import { readFile as readFile8 } from "fs/promises";
2302
2432
  import { homedir as homedir2 } from "os";
2303
- import path13 from "path";
2433
+ import path14 from "path";
2304
2434
  async function loadJsonFile(filePath) {
2305
2435
  try {
2306
- const content = await readFile7(filePath, "utf-8");
2436
+ const content = await readFile8(filePath, "utf-8");
2307
2437
  return JSON.parse(content);
2308
2438
  } catch {
2309
2439
  return void 0;
@@ -2311,7 +2441,7 @@ async function loadJsonFile(filePath) {
2311
2441
  }
2312
2442
  async function loadConfig(projectRoot) {
2313
2443
  const globalRaw = await loadJsonFile(GLOBAL_CONFIG_PATH);
2314
- const projectRaw = projectRoot ? await loadJsonFile(path13.join(projectRoot, PROJECT_CONFIG_NAME)) : void 0;
2444
+ const projectRaw = projectRoot ? await loadJsonFile(path14.join(projectRoot, PROJECT_CONFIG_NAME)) : void 0;
2315
2445
  const merged = {
2316
2446
  ...typeof globalRaw === "object" && globalRaw !== null ? globalRaw : {},
2317
2447
  ...typeof projectRaw === "object" && projectRaw !== null ? projectRaw : {}
@@ -2330,7 +2460,7 @@ function globalConfigPath() {
2330
2460
  return GLOBAL_CONFIG_PATH;
2331
2461
  }
2332
2462
  function globalConfigExists() {
2333
- return existsSync10(GLOBAL_CONFIG_PATH);
2463
+ return existsSync11(GLOBAL_CONFIG_PATH);
2334
2464
  }
2335
2465
  var GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_PATH, PROJECT_CONFIG_NAME;
2336
2466
  var init_config = __esm({
@@ -2339,8 +2469,8 @@ var init_config = __esm({
2339
2469
  init_esm_shims();
2340
2470
  init_logger();
2341
2471
  init_schemas();
2342
- GLOBAL_CONFIG_DIR = path13.join(homedir2(), ".eds");
2343
- GLOBAL_CONFIG_PATH = path13.join(GLOBAL_CONFIG_DIR, "config.json");
2472
+ GLOBAL_CONFIG_DIR = path14.join(homedir2(), ".eds");
2473
+ GLOBAL_CONFIG_PATH = path14.join(GLOBAL_CONFIG_DIR, "config.json");
2344
2474
  PROJECT_CONFIG_NAME = ".edsrc.json";
2345
2475
  }
2346
2476
  });
@@ -2390,9 +2520,9 @@ var init_node_id = __esm({
2390
2520
  });
2391
2521
 
2392
2522
  // src/lib/figma/prompt-builder.ts
2393
- import { existsSync as existsSync11 } from "fs";
2394
- import { readFile as readFile8 } from "fs/promises";
2395
- import path14 from "path";
2523
+ import { existsSync as existsSync12 } from "fs";
2524
+ import { readFile as readFile9 } from "fs/promises";
2525
+ import path15 from "path";
2396
2526
  function contentSourceGuidance(source, cfHint, blockName) {
2397
2527
  if (!source || source === "document" || source === "ue") return "";
2398
2528
  const q = cfHint ? ` (model / persisted query: \`${cfHint}\`)` : "";
@@ -2604,10 +2734,10 @@ ${tokensHint}
2604
2734
  `;
2605
2735
  }
2606
2736
  async function getTokensHint(projectRoot) {
2607
- const stylesPath = path14.join(projectRoot, "styles", "styles.css");
2608
- if (!existsSync11(stylesPath)) return "";
2737
+ const stylesPath = path15.join(projectRoot, "styles", "styles.css");
2738
+ if (!existsSync12(stylesPath)) return "";
2609
2739
  try {
2610
- const css = await readFile8(stylesPath, "utf-8");
2740
+ const css = await readFile9(stylesPath, "utf-8");
2611
2741
  const rootMatch = css.match(/:root\s*\{([^}]+)\}/);
2612
2742
  if (!rootMatch) return "";
2613
2743
  const props = rootMatch[1].split("\n").filter((line) => line.trim().startsWith("--")).map((line) => line.trim()).join("\n ");
@@ -2637,7 +2767,8 @@ var init_prompt_builder = __esm({
2637
2767
  figma: "Figma",
2638
2768
  stitch: "Google Stitch",
2639
2769
  canva: "Canva",
2640
- sketch: "Sketch"
2770
+ sketch: "Sketch",
2771
+ framer: "Framer"
2641
2772
  };
2642
2773
  }
2643
2774
  });
@@ -2747,11 +2878,11 @@ var init_harness = __esm({
2747
2878
  });
2748
2879
 
2749
2880
  // src/lib/preview/server.ts
2750
- import { createReadStream, existsSync as existsSync12, statSync } from "fs";
2881
+ import { createReadStream, existsSync as existsSync13, statSync } from "fs";
2751
2882
  import http from "http";
2752
- import path15 from "path";
2883
+ import path16 from "path";
2753
2884
  function startServer(root, routes, port, host = "127.0.0.1") {
2754
- const resolvedRoot = path15.resolve(root);
2885
+ const resolvedRoot = path16.resolve(root);
2755
2886
  const clients = /* @__PURE__ */ new Set();
2756
2887
  const server = http.createServer((req, res) => {
2757
2888
  const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
@@ -2777,19 +2908,19 @@ function startServer(root, routes, port, host = "127.0.0.1") {
2777
2908
  res.end(route.body);
2778
2909
  return;
2779
2910
  }
2780
- const filePath = path15.join(resolvedRoot, urlPath);
2911
+ const filePath = path16.join(resolvedRoot, urlPath);
2781
2912
  if (!filePath.startsWith(resolvedRoot)) {
2782
2913
  res.writeHead(403);
2783
2914
  res.end("Forbidden");
2784
2915
  return;
2785
2916
  }
2786
- if (!existsSync12(filePath) || !statSync(filePath).isFile()) {
2917
+ if (!existsSync13(filePath) || !statSync(filePath).isFile()) {
2787
2918
  res.writeHead(404, { "content-type": "text/plain" });
2788
2919
  res.end(`Not found: ${urlPath}`);
2789
2920
  return;
2790
2921
  }
2791
2922
  res.writeHead(200, {
2792
- "content-type": MIME[path15.extname(filePath).toLowerCase()] ?? "application/octet-stream"
2923
+ "content-type": MIME[path16.extname(filePath).toLowerCase()] ?? "application/octet-stream"
2793
2924
  });
2794
2925
  createReadStream(filePath).pipe(res);
2795
2926
  });
@@ -2853,10 +2984,10 @@ var preview_exports = {};
2853
2984
  __export(preview_exports, {
2854
2985
  previewBlock: () => previewBlock
2855
2986
  });
2856
- import { existsSync as existsSync13, watch } from "fs";
2857
- import { readFile as readFile9 } from "fs/promises";
2858
- import path16 from "path";
2859
- import chalk6 from "chalk";
2987
+ import { existsSync as existsSync14, watch } from "fs";
2988
+ import { readFile as readFile10 } from "fs/promises";
2989
+ import path17 from "path";
2990
+ import chalk7 from "chalk";
2860
2991
  function labelFor(width) {
2861
2992
  if (width <= 480) return `${width} mobile`;
2862
2993
  if (width <= 1024) return `${width} tablet`;
@@ -2908,30 +3039,30 @@ async function previewBlock(name, options = {}) {
2908
3039
  process.exitCode = 1;
2909
3040
  return;
2910
3041
  }
2911
- const blockDir = path16.join(projectRoot, "blocks", name);
2912
- if (!existsSync13(blockDir)) {
3042
+ const blockDir = path17.join(projectRoot, "blocks", name);
3043
+ if (!existsSync14(blockDir)) {
2913
3044
  logger.error(`Block "${name}" not found at blocks/${name}/.`);
2914
3045
  process.exitCode = 1;
2915
3046
  return;
2916
3047
  }
2917
- if (!existsSync13(path16.join(blockDir, `${name}.css`)) && !existsSync13(path16.join(blockDir, `${name}.js`))) {
3048
+ if (!existsSync14(path17.join(blockDir, `${name}.css`)) && !existsSync14(path17.join(blockDir, `${name}.js`))) {
2918
3049
  logger.error(`Block "${name}" has no ${name}.css / ${name}.js to preview.`);
2919
3050
  process.exitCode = 1;
2920
3051
  return;
2921
3052
  }
2922
3053
  let meta = null;
2923
- const metaPath = path16.join(blockDir, ".eds-meta.json");
2924
- if (existsSync13(metaPath)) {
3054
+ const metaPath = path17.join(blockDir, ".eds-meta.json");
3055
+ if (existsSync14(metaPath)) {
2925
3056
  try {
2926
- meta = JSON.parse(await readFile9(metaPath, "utf-8"));
3057
+ meta = JSON.parse(await readFile10(metaPath, "utf-8"));
2927
3058
  } catch {
2928
3059
  }
2929
3060
  }
2930
- const samplePath = path16.join(blockDir, `_${name}.preview.html`);
3061
+ const samplePath = path17.join(blockDir, `_${name}.preview.html`);
2931
3062
  let sampleContent;
2932
3063
  let placeholder = false;
2933
- if (existsSync13(samplePath)) {
2934
- sampleContent = await readFile9(samplePath, "utf-8");
3064
+ if (existsSync14(samplePath)) {
3065
+ sampleContent = await readFile10(samplePath, "utf-8");
2935
3066
  } else {
2936
3067
  sampleContent = defaultSample(name);
2937
3068
  placeholder = true;
@@ -2981,9 +3112,9 @@ async function previewBlock(name, options = {}) {
2981
3112
  reloadTimer = setTimeout(() => server.reload(), 150);
2982
3113
  });
2983
3114
  logger.info("");
2984
- logger.info(` ${chalk6.bold("\u279C")} ${chalk6.cyan(server.url)}`);
3115
+ logger.info(` ${chalk7.bold("\u279C")} ${chalk7.cyan(server.url)}`);
2985
3116
  logger.info(statusLine("pass", "live reload", "edits to the block refresh the browser"));
2986
- logger.info(chalk6.dim(" Press Ctrl+C to stop.\n"));
3117
+ logger.info(chalk7.dim(" Press Ctrl+C to stop.\n"));
2987
3118
  if (options.open !== false) await openInBrowser(server.url);
2988
3119
  const stop = async () => {
2989
3120
  watcher.close();
@@ -3012,10 +3143,10 @@ var from_design_exports = {};
3012
3143
  __export(from_design_exports, {
3013
3144
  blockFromDesign: () => blockFromDesign
3014
3145
  });
3015
- import { existsSync as existsSync14 } from "fs";
3016
- import { readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
3017
- import path17 from "path";
3018
- import chalk7 from "chalk";
3146
+ import { existsSync as existsSync15 } from "fs";
3147
+ import { readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
3148
+ import path18 from "path";
3149
+ import chalk8 from "chalk";
3019
3150
  import ora from "ora";
3020
3151
  async function previewRunning(port = 8777) {
3021
3152
  try {
@@ -3038,7 +3169,8 @@ async function runWizard(figmaUrl) {
3038
3169
  { name: "Figma", value: "figma" },
3039
3170
  { name: "Google Stitch", value: "stitch" },
3040
3171
  { name: "Canva", value: "canva" },
3041
- { name: "Sketch", value: "sketch" }
3172
+ { name: "Sketch", value: "sketch" },
3173
+ { name: "Framer", value: "framer" }
3042
3174
  ],
3043
3175
  default: "figma"
3044
3176
  });
@@ -3164,7 +3296,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3164
3296
  try {
3165
3297
  figma2 = parseFigmaUrl(designUrl);
3166
3298
  spinner.succeed(
3167
- `Parsed Figma URL ${chalk7.dim(figma2.nodeId ? `node ${figma2.nodeId}` : `file ${figma2.fileKey}`)}`
3299
+ `Parsed Figma URL ${chalk8.dim(figma2.nodeId ? `node ${figma2.nodeId}` : `file ${figma2.fileKey}`)}`
3168
3300
  );
3169
3301
  } catch (err) {
3170
3302
  spinner.fail(err.message);
@@ -3172,7 +3304,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3172
3304
  return;
3173
3305
  }
3174
3306
  } else {
3175
- logger.info(` ${icon.pass} ${providerName} design ${chalk7.dim(designRef)}`);
3307
+ logger.info(` ${icon.pass} ${providerName} design ${chalk8.dim(designRef)}`);
3176
3308
  }
3177
3309
  const blockName = options.name || (figma2 ? inferBlockName(figma2.fileName) : "");
3178
3310
  if (!kebabCaseRegex.test(blockName)) {
@@ -3195,20 +3327,20 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3195
3327
  screenshot: options.screenshot !== false
3196
3328
  });
3197
3329
  const promptFile = await savePromptToFile(prompt, projectRoot);
3198
- const hasTokens = existsSync14(path17.join(projectRoot, "styles", "styles.css"));
3330
+ const hasTokens = existsSync15(path18.join(projectRoot, "styles", "styles.css"));
3199
3331
  promptSpinner.succeed(
3200
- hasTokens ? `Loaded design tokens ${chalk7.dim("styles/styles.css")}` : `Built prompt ${chalk7.dim("no project tokens found")}`
3332
+ hasTokens ? `Loaded design tokens ${chalk8.dim("styles/styles.css")}` : `Built prompt ${chalk8.dim("no project tokens found")}`
3201
3333
  );
3202
- logger.info(chalk7.dim(` prompt \u2192 ${promptFile}`));
3334
+ logger.info(chalk8.dim(` prompt \u2192 ${promptFile}`));
3203
3335
  if (options.dryRun) {
3204
3336
  logger.info("");
3205
- logger.info(chalk7.yellow("Dry run mode \u2014 prompt generated but agent not invoked."));
3206
- logger.info(`Prompt file: ${chalk7.cyan(promptFile)}`);
3207
- logger.info(`Block name: ${chalk7.cyan(blockName)}`);
3337
+ logger.info(chalk8.yellow("Dry run mode \u2014 prompt generated but agent not invoked."));
3338
+ logger.info(`Prompt file: ${chalk8.cyan(promptFile)}`);
3339
+ logger.info(`Block name: ${chalk8.cyan(blockName)}`);
3208
3340
  logger.info(`Prompt version: ${PROMPT_VERSION}`);
3209
3341
  logger.info("");
3210
3342
  logger.info("Preview:");
3211
- logger.info(chalk7.dim("\u2500".repeat(60)));
3343
+ logger.info(chalk8.dim("\u2500".repeat(60)));
3212
3344
  console.log(prompt);
3213
3345
  return;
3214
3346
  }
@@ -3216,22 +3348,22 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3216
3348
  const preferredAgent = options.agent || config.agent;
3217
3349
  if (options.agent === "none") {
3218
3350
  logger.info("Agent skipped (--agent none). Use the prompt file manually:");
3219
- logger.info(` ${chalk7.cyan(promptFile)}`);
3351
+ logger.info(` ${chalk8.cyan(promptFile)}`);
3220
3352
  return;
3221
3353
  }
3222
3354
  const agentSpinner = ora("Detecting AI agent...").start();
3223
3355
  const agent = await detectAgent(preferredAgent);
3224
3356
  if (!agent) {
3225
3357
  agentSpinner.fail("No AI agent found (claude, cursor, or codex).");
3226
- logger.info(`Install Claude Code: ${chalk7.cyan("npm install -g @anthropic-ai/claude-code")}`);
3227
- logger.info(`Or use the prompt file manually: ${chalk7.cyan(promptFile)}`);
3358
+ logger.info(`Install Claude Code: ${chalk8.cyan("npm install -g @anthropic-ai/claude-code")}`);
3359
+ logger.info(`Or use the prompt file manually: ${chalk8.cyan(promptFile)}`);
3228
3360
  process.exitCode = 1;
3229
3361
  return;
3230
3362
  }
3231
- agentSpinner.succeed(`Agent ready ${chalk7.cyan(agent.type)} ${chalk7.dim(agent.path)}`);
3363
+ agentSpinner.succeed(`Agent ready ${chalk8.cyan(agent.type)} ${chalk8.dim(agent.path)}`);
3232
3364
  if (provider === "figma" && !agent.hasFigmaMcp) {
3233
3365
  logger.warn(`Figma MCP may not be configured for ${agent.type}.`);
3234
- logger.info(`Run ${chalk7.cyan("eds figma setup")} to configure it.`);
3366
+ logger.info(`Run ${chalk8.cyan("eds figma setup")} to configure it.`);
3235
3367
  }
3236
3368
  if (!options.yes) {
3237
3369
  const { confirm } = await import("@inquirer/prompts");
@@ -3240,24 +3372,24 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3240
3372
  default: true
3241
3373
  });
3242
3374
  if (!proceed) {
3243
- logger.info(`Prompt saved to: ${chalk7.cyan(promptFile)}`);
3375
+ logger.info(`Prompt saved to: ${chalk8.cyan(promptFile)}`);
3244
3376
  return;
3245
3377
  }
3246
3378
  }
3247
3379
  logger.info(
3248
- ` ${chalk7.dim("\u2192")} Handing off to ${chalk7.cyan(agent.type)} ${chalk7.dim(`via ${providerName} MCP`)}`
3380
+ ` ${chalk8.dim("\u2192")} Handing off to ${chalk8.cyan(agent.type)} ${chalk8.dim(`via ${providerName} MCP`)}`
3249
3381
  );
3250
3382
  if (provider === "figma") {
3251
- logger.info(chalk7.dim(" get_design_context \xB7 get_variable_defs \xB7 get_screenshot"));
3383
+ logger.info(chalk8.dim(" get_design_context \xB7 get_variable_defs \xB7 get_screenshot"));
3252
3384
  }
3253
3385
  const success = await invokeAgent(agent, promptFile, projectRoot);
3254
3386
  if (!success) {
3255
3387
  process.exitCode = 1;
3256
3388
  return;
3257
3389
  }
3258
- const blockDir = path17.join(projectRoot, "blocks", blockName);
3390
+ const blockDir = path18.join(projectRoot, "blocks", blockName);
3259
3391
  const ok = await verifyGeneratedBlock(blockDir, blockName, options.ueModel !== false);
3260
- if (existsSync14(blockDir)) {
3392
+ if (existsSync15(blockDir)) {
3261
3393
  const meta = {
3262
3394
  provider,
3263
3395
  designRef,
@@ -3267,7 +3399,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3267
3399
  promptVersion: PROMPT_VERSION,
3268
3400
  agentUsed: agent.type
3269
3401
  };
3270
- await writeFile8(path17.join(blockDir, ".eds-meta.json"), `${JSON.stringify(meta, null, 2)}
3402
+ await writeFile9(path18.join(blockDir, ".eds-meta.json"), `${JSON.stringify(meta, null, 2)}
3271
3403
  `);
3272
3404
  }
3273
3405
  if (!ok) {
@@ -3278,10 +3410,21 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3278
3410
  return;
3279
3411
  }
3280
3412
  logger.success(`Block "${blockName}" generation complete.`);
3413
+ if (!options.yes) {
3414
+ const { confirm } = await import("@inquirer/prompts");
3415
+ const addTracking = await confirm({
3416
+ message: "Instrument this block for analytics (dataLayer)?",
3417
+ default: false
3418
+ });
3419
+ if (addTracking) {
3420
+ const { trackBlock: trackBlock2 } = await Promise.resolve().then(() => (init_track2(), track_exports));
3421
+ await trackBlock2(blockName, {});
3422
+ }
3423
+ }
3281
3424
  if (options.serve !== false) {
3282
3425
  if (await previewRunning()) {
3283
3426
  logger.info(
3284
- chalk7.dim(
3427
+ chalk8.dim(
3285
3428
  " A preview server is already running at http://127.0.0.1:8777 \u2014 your change will refresh there."
3286
3429
  )
3287
3430
  );
@@ -3291,15 +3434,15 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3291
3434
  await previewBlock2(blockName, {});
3292
3435
  }
3293
3436
  } else {
3294
- logger.info(` ${chalk7.dim("\u2192")} Next: ${chalk7.cyan(`eds block preview ${blockName}`)}`);
3437
+ logger.info(` ${chalk8.dim("\u2192")} Next: ${chalk8.cyan(`eds block preview ${blockName}`)}`);
3295
3438
  }
3296
3439
  }
3297
3440
  async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3298
3441
  const checks = [];
3299
- const jsPath = path17.join(blockDir, `${blockName}.js`);
3300
- const cssPath = path17.join(blockDir, `${blockName}.css`);
3301
- const hasJs = existsSync14(jsPath);
3302
- const hasCss = existsSync14(cssPath);
3442
+ const jsPath = path18.join(blockDir, `${blockName}.js`);
3443
+ const cssPath = path18.join(blockDir, `${blockName}.css`);
3444
+ const hasJs = existsSync15(jsPath);
3445
+ const hasCss = existsSync15(cssPath);
3303
3446
  checks.push({
3304
3447
  status: hasJs ? "pass" : "fail",
3305
3448
  name: `${blockName}.js`,
@@ -3311,7 +3454,7 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3311
3454
  message: hasCss ? "created" : "missing \u2014 agent did not write the CSS file"
3312
3455
  });
3313
3456
  if (hasJs) {
3314
- const js = await readFile10(jsPath, "utf-8");
3457
+ const js = await readFile11(jsPath, "utf-8");
3315
3458
  checks.push({
3316
3459
  status: /export\s+default/.test(js) ? "pass" : "fail",
3317
3460
  name: "decorate export",
@@ -3326,7 +3469,7 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3326
3469
  }
3327
3470
  }
3328
3471
  if (hasCss) {
3329
- const css = await readFile10(cssPath, "utf-8");
3472
+ const css = await readFile11(cssPath, "utf-8");
3330
3473
  checks.push({
3331
3474
  status: css.includes(`.${blockName}`) ? "pass" : "warn",
3332
3475
  name: "scoped CSS",
@@ -3334,12 +3477,12 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3334
3477
  });
3335
3478
  }
3336
3479
  if (withUeModel) {
3337
- const hasPerBlock = existsSync14(path17.join(blockDir, `_${blockName}.json`));
3338
- const modelsFile = path17.join(blockDir, "..", "..", "component-models.json");
3480
+ const hasPerBlock = existsSync15(path18.join(blockDir, `_${blockName}.json`));
3481
+ const modelsFile = path18.join(blockDir, "..", "..", "component-models.json");
3339
3482
  let hasAggregated = false;
3340
- if (!hasPerBlock && existsSync14(modelsFile)) {
3483
+ if (!hasPerBlock && existsSync15(modelsFile)) {
3341
3484
  try {
3342
- hasAggregated = (await readFile10(modelsFile, "utf-8")).includes(`"${blockName}"`);
3485
+ hasAggregated = (await readFile11(modelsFile, "utf-8")).includes(`"${blockName}"`);
3343
3486
  } catch {
3344
3487
  }
3345
3488
  }
@@ -3351,8 +3494,8 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3351
3494
  });
3352
3495
  if (hasPerBlock && hasJs) {
3353
3496
  try {
3354
- const modelRaw = await readFile10(path17.join(blockDir, `_${blockName}.json`), "utf-8");
3355
- const js = await readFile10(jsPath, "utf-8");
3497
+ const modelRaw = await readFile11(path18.join(blockDir, `_${blockName}.json`), "utf-8");
3498
+ const js = await readFile11(jsPath, "utf-8");
3356
3499
  const isRepeatable = /"resourceType"\s*:\s*"[^"]*\/item"/.test(modelRaw) || /"filter"\s*:/.test(modelRaw) || /"multi"\s*:\s*true/.test(modelRaw);
3357
3500
  const iterates = /\.children/.test(js) || /\bfor\s*\(/.test(js) || /\.forEach\s*\(/.test(js);
3358
3501
  if (isRepeatable) {
@@ -3395,10 +3538,10 @@ var list_exports = {};
3395
3538
  __export(list_exports, {
3396
3539
  listBlocks: () => listBlocks
3397
3540
  });
3398
- import { existsSync as existsSync15 } from "fs";
3399
- import { readFile as readFile11, readdir as readdir2 } from "fs/promises";
3400
- import path18 from "path";
3401
- import chalk8 from "chalk";
3541
+ import { existsSync as existsSync16 } from "fs";
3542
+ import { readFile as readFile12, readdir as readdir2 } from "fs/promises";
3543
+ import path19 from "path";
3544
+ import chalk9 from "chalk";
3402
3545
  async function listBlocks(options = {}) {
3403
3546
  const projectRoot = findProjectRoot();
3404
3547
  if (!projectRoot) {
@@ -3406,8 +3549,8 @@ async function listBlocks(options = {}) {
3406
3549
  process.exitCode = 1;
3407
3550
  return;
3408
3551
  }
3409
- const blocksDir = path18.join(projectRoot, "blocks");
3410
- if (!existsSync15(blocksDir)) {
3552
+ const blocksDir = path19.join(projectRoot, "blocks");
3553
+ if (!existsSync16(blocksDir)) {
3411
3554
  logger.info("No blocks/ directory found.");
3412
3555
  return;
3413
3556
  }
@@ -3416,17 +3559,17 @@ async function listBlocks(options = {}) {
3416
3559
  for (const entry of entries) {
3417
3560
  if (!entry.isDirectory()) continue;
3418
3561
  const name = entry.name;
3419
- const blockDir = path18.join(blocksDir, name);
3562
+ const blockDir = path19.join(blocksDir, name);
3420
3563
  const info = {
3421
3564
  name,
3422
- hasJs: existsSync15(path18.join(blockDir, `${name}.js`)),
3423
- hasCss: existsSync15(path18.join(blockDir, `${name}.css`)),
3424
- hasUeModel: existsSync15(path18.join(blockDir, `_${name}.json`))
3565
+ hasJs: existsSync16(path19.join(blockDir, `${name}.js`)),
3566
+ hasCss: existsSync16(path19.join(blockDir, `${name}.css`)),
3567
+ hasUeModel: existsSync16(path19.join(blockDir, `_${name}.json`))
3425
3568
  };
3426
- const metaFile = path18.join(blockDir, ".eds-meta.json");
3427
- if (existsSync15(metaFile)) {
3569
+ const metaFile = path19.join(blockDir, ".eds-meta.json");
3570
+ if (existsSync16(metaFile)) {
3428
3571
  try {
3429
- const meta = JSON.parse(await readFile11(metaFile, "utf-8"));
3572
+ const meta = JSON.parse(await readFile12(metaFile, "utf-8"));
3430
3573
  info.figmaSource = {
3431
3574
  fileKey: meta.figmaFileKey,
3432
3575
  nodeId: meta.figmaNodeId,
@@ -3449,17 +3592,17 @@ async function listBlocks(options = {}) {
3449
3592
  const nameWidth = columnWidth(blocks.map((b) => b.name));
3450
3593
  for (const b of blocks) {
3451
3594
  const badges = [
3452
- b.hasJs ? brand.ok("js") : chalk8.dim.strikethrough("js"),
3453
- b.hasCss ? brand.ok("css") : chalk8.dim.strikethrough("css")
3595
+ b.hasJs ? brand.ok("js") : chalk9.dim.strikethrough("js"),
3596
+ b.hasCss ? brand.ok("css") : chalk9.dim.strikethrough("css")
3454
3597
  ];
3455
3598
  if (b.hasUeModel) badges.push(brand.blue("ue-model"));
3456
3599
  if (b.figmaSource) badges.push(`${icon.figma} ${brand.dotPurple("figma")}`);
3457
- const name = padEndVisible(chalk8.bold(b.name), nameWidth);
3458
- logger.info(` ${name} ${chalk8.dim("[")}${badges.join(" ")}${chalk8.dim("]")}`);
3600
+ const name = padEndVisible(chalk9.bold(b.name), nameWidth);
3601
+ logger.info(` ${name} ${chalk9.dim("[")}${badges.join(" ")}${chalk9.dim("]")}`);
3459
3602
  if (b.figmaSource) {
3460
3603
  const synced = new Date(b.figmaSource.lastSyncedAt).toLocaleDateString();
3461
3604
  logger.info(
3462
- ` ${" ".repeat(nameWidth)} ${icon.arrow} ${chalk8.dim(
3605
+ ` ${" ".repeat(nameWidth)} ${icon.arrow} ${chalk9.dim(
3463
3606
  `figma:${b.figmaSource.fileKey} \xB7 synced ${synced}`
3464
3607
  )}`
3465
3608
  );
@@ -3499,22 +3642,85 @@ var init_integrations = __esm({
3499
3642
  id: "gtm",
3500
3643
  name: "Google Tag Manager",
3501
3644
  fields: [{ name: "id", message: "GTM container ID", placeholder: "GTM-XXXXXXX" }],
3502
- code: (v) => `// Google Tag Manager (${v.id}) \u2014 delayed phase, after LCP.
3503
- (function (w, d, s, l, i) {
3504
- w[l] = w[l] || [];
3505
- w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
3506
- const f = d.getElementsByTagName(s)[0];
3507
- const j = d.createElement(s);
3508
- j.async = true;
3509
- j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i;
3510
- f.parentNode.insertBefore(j, f);
3511
- })(window, document, 'script', 'dataLayer', '${v.id}');`
3645
+ // delayed.js imports the loader + page-meta, pushes page metadata, then GTM.
3646
+ code: () => `// Google Tag Manager - loaded from ./analytics.js (delayed phase, after LCP).
3647
+ import { loadGTM } from './analytics.js';
3648
+ import pageMetaPush from './page-meta.js';
3649
+
3650
+ const pageMetaReady = pageMetaPush();
3651
+ pageMetaReady.then(loadGTM);`,
3652
+ module: (v) => [
3653
+ {
3654
+ file: "scripts/analytics.js",
3655
+ content: `// scripts/analytics.js - Google Tag Manager (generated by \`eds integrate gtm\`).
3656
+ // The container ID comes from the \`gtm-id\` metadata, falling back to the one
3657
+ // configured here. Import { trackEvent } from './analytics.js' to push events.
3658
+
3659
+ const FALLBACK_ID = ${JSON.stringify(v.id || "")};
3660
+
3661
+ function getMetadata(name) {
3662
+ const meta = document.querySelector(\`meta[name="\${name}"]\`);
3663
+ return meta ? meta.content : '';
3664
+ }
3665
+
3666
+ function isDev() {
3667
+ const h = window.location.hostname;
3668
+ return h === 'localhost' || h === '127.0.0.1' || h.endsWith('.aem.page') || h.endsWith('.hlx.page');
3669
+ }
3670
+
3671
+ // dataLayer must exist before anything pushes to it (trackEvent may run before loadGTM).
3672
+ window.dataLayer = window.dataLayer || [];
3673
+
3674
+ export function trackEvent(event) {
3675
+ if (isDev()) {
3676
+ // eslint-disable-next-line no-console
3677
+ console.log('[gtm]', JSON.stringify(event, null, 2));
3678
+ }
3679
+ window.dataLayer.push(event);
3680
+ }
3681
+
3682
+ let loaded = false;
3683
+
3684
+ export function loadGTM() {
3685
+ if (loaded) return;
3686
+ const raw = getMetadata('gtm-id') || FALLBACK_ID;
3687
+ if (!raw) return;
3688
+ loaded = true;
3689
+
3690
+ const id = raw.startsWith('GTM-') ? raw : \`GTM-\${raw}\`;
3691
+ window.dataLayer.push({ 'gtm.start': Date.now(), event: 'gtm.js' });
3692
+
3693
+ const script = document.createElement('script');
3694
+ script.async = true;
3695
+ script.src = \`https://www.googletagmanager.com/gtm.js?id=\${id}\`;
3696
+ document.head.append(script);
3697
+ }
3698
+ `
3699
+ },
3700
+ {
3701
+ file: "scripts/page-meta.js",
3702
+ content: `// scripts/page-meta.js - pushes page-level metadata to the dataLayer before
3703
+ // GTM initialises. Extend \`pageMeta\` with the fields your container expects.
3704
+ import { trackEvent } from './analytics.js';
3705
+
3706
+ export default async function pageMetaPush() {
3707
+ const pageMeta = {
3708
+ event: 'page_meta',
3709
+ };
3710
+
3711
+ trackEvent(pageMeta);
3712
+
3713
+ return pageMeta;
3714
+ }
3715
+ `
3716
+ }
3717
+ ]
3512
3718
  },
3513
3719
  {
3514
3720
  id: "ga4",
3515
3721
  name: "Google Analytics 4",
3516
3722
  fields: [{ name: "id", message: "Measurement ID", placeholder: "G-XXXXXXXXXX" }],
3517
- code: (v) => `// Google Analytics 4 (${v.id}) \u2014 delayed phase.
3723
+ code: (v) => `// Google Analytics 4 (${v.id}) - delayed phase.
3518
3724
  const gaScript = document.createElement('script');
3519
3725
  gaScript.async = true;
3520
3726
  gaScript.src = 'https://www.googletagmanager.com/gtag/js?id=${v.id}';
@@ -3531,7 +3737,7 @@ gtag('config', '${v.id}');`
3531
3737
  { name: "label", message: "Button label", placeholder: "Chat with us" },
3532
3738
  { name: "src", message: "Chat widget script URL" }
3533
3739
  ],
3534
- code: (v) => `// Chat widget \u2014 facade pattern: load the heavy script only on first interaction.
3740
+ code: (v) => `// Chat widget - facade pattern: load the heavy script only on first interaction.
3535
3741
  const chatBtn = document.createElement('button');
3536
3742
  chatBtn.type = 'button';
3537
3743
  chatBtn.className = 'chat-facade';
@@ -3549,7 +3755,7 @@ document.body.appendChild(chatBtn);`
3549
3755
  id: "cookiebot",
3550
3756
  name: "Cookie consent (Cookiebot)",
3551
3757
  fields: [{ name: "id", message: "Cookiebot domain group ID (CBID)" }],
3552
- code: (v) => `// Cookiebot consent \u2014 load early in delayed; configure GTM consent mode to respect it.
3758
+ code: (v) => `// Cookiebot consent - load early in delayed; configure GTM consent mode to respect it.
3553
3759
  const cb = document.createElement('script');
3554
3760
  cb.id = 'Cookiebot';
3555
3761
  cb.src = 'https://consent.cookiebot.com/uc.js';
@@ -3561,7 +3767,7 @@ document.head.appendChild(cb);`
3561
3767
  id: "custom",
3562
3768
  name: "Custom third-party script",
3563
3769
  fields: [{ name: "url", message: "Script URL" }],
3564
- code: (v) => `// Custom third-party script \u2014 delayed phase.
3770
+ code: (v) => `// Custom third-party script - delayed phase.
3565
3771
  const s = document.createElement('script');
3566
3772
  s.async = true;
3567
3773
  s.src = ${JSON.stringify(v.url || "")};
@@ -3576,10 +3782,10 @@ var integrate_exports = {};
3576
3782
  __export(integrate_exports, {
3577
3783
  integrate: () => integrate
3578
3784
  });
3579
- import { existsSync as existsSync16 } from "fs";
3580
- import { mkdir as mkdir6, readFile as readFile12, writeFile as writeFile9 } from "fs/promises";
3581
- import path19 from "path";
3582
- import chalk9 from "chalk";
3785
+ import { existsSync as existsSync17 } from "fs";
3786
+ import { mkdir as mkdir6, readFile as readFile13, writeFile as writeFile10 } from "fs/promises";
3787
+ import path20 from "path";
3788
+ import chalk10 from "chalk";
3583
3789
  async function integrate(type) {
3584
3790
  const projectRoot = findProjectRoot();
3585
3791
  if (!projectRoot) {
@@ -3587,7 +3793,7 @@ async function integrate(type) {
3587
3793
  process.exitCode = 1;
3588
3794
  return;
3589
3795
  }
3590
- logger.logoOnce(logo("Integrate \u2014 third-party service"));
3796
+ logger.logoOnce(logo("Integrate - third-party service"));
3591
3797
  const { select, input } = await import("@inquirer/prompts");
3592
3798
  const chosenId = type || await select({
3593
3799
  message: "Which service do you want to add?",
@@ -3606,26 +3812,39 @@ async function integrate(type) {
3606
3812
  const answer = await input({ message: ` ${f.message}`, default: f.placeholder });
3607
3813
  values[f.name] = answer.trim();
3608
3814
  }
3609
- const block2 = wrapBlock(integration.name, integration.code(values));
3610
- const delayedPath = path19.join(projectRoot, "scripts", "delayed.js");
3611
- await mkdir6(path19.dirname(delayedPath), { recursive: true });
3612
- const existing = existsSync16(delayedPath) ? await readFile12(delayedPath, "utf-8") : DELAYED_HEADER;
3613
- await writeFile9(delayedPath, `${existing.replace(/\s*$/, "")}
3815
+ const modulePaths = [];
3816
+ if (integration.module) {
3817
+ for (const mod of integration.module(values)) {
3818
+ const abs = path20.join(projectRoot, mod.file);
3819
+ await mkdir6(path20.dirname(abs), { recursive: true });
3820
+ await writeFile10(abs, mod.content);
3821
+ modulePaths.push(mod.file);
3822
+ }
3823
+ }
3824
+ const block2 = integration.module ? `
3825
+ // ${integration.name} (eds integrate)
3826
+ ${integration.code(values)}
3827
+ ` : wrapBlock(integration.name, integration.code(values));
3828
+ const delayedPath = path20.join(projectRoot, "scripts", "delayed.js");
3829
+ await mkdir6(path20.dirname(delayedPath), { recursive: true });
3830
+ const existing = existsSync17(delayedPath) ? await readFile13(delayedPath, "utf-8") : DELAYED_HEADER;
3831
+ await writeFile10(delayedPath, `${existing.replace(/\s*$/, "")}
3614
3832
  ${block2}`);
3615
3833
  logger.info(heading("Added"));
3616
- logger.info(accentLine(integration.name, "\u2192 scripts/delayed.js"));
3834
+ for (const p of modulePaths) logger.info(accentLine(integration.name, `wrote ${p}`));
3835
+ logger.info(accentLine(integration.name, "wired into scripts/delayed.js"));
3617
3836
  logger.info("");
3618
3837
  logger.info(
3619
- chalk9.dim(" Loads after LCP (delayed phase) \u2014 zero impact on your Core Web Vitals.")
3838
+ chalk10.dim(" Loads after LCP (delayed phase) - zero impact on your Core Web Vitals.")
3620
3839
  );
3621
3840
  if (integration.id === "gtm" || integration.id === "ga4") {
3622
3841
  logger.info(
3623
- chalk9.dim(" Tip: gate analytics behind user consent (add the Cookiebot integration).")
3842
+ chalk10.dim(" Tip: gate analytics behind user consent (add the Cookiebot integration).")
3624
3843
  );
3625
3844
  }
3626
3845
  if (integration.id === "chat") {
3627
3846
  logger.info(
3628
- chalk9.dim(" The widget loads only when the visitor clicks the button (facade pattern).")
3847
+ chalk10.dim(" The widget loads only when the visitor clicks the button (facade pattern).")
3629
3848
  );
3630
3849
  }
3631
3850
  logger.info("");
@@ -3640,7 +3859,7 @@ var init_integrate = __esm({
3640
3859
  init_logger();
3641
3860
  init_project();
3642
3861
  init_ui();
3643
- DELAYED_HEADER = `// delayed.js \u2014 third-party / non-critical scripts.
3862
+ DELAYED_HEADER = `// delayed.js - third-party / non-critical scripts.
3644
3863
  // Loaded in the EDS delayed phase (after LCP), so nothing here blocks the page.
3645
3864
  `;
3646
3865
  }
@@ -3651,9 +3870,9 @@ var setup_exports = {};
3651
3870
  __export(setup_exports, {
3652
3871
  figmaSetup: () => figmaSetup
3653
3872
  });
3654
- import { existsSync as existsSync17 } from "fs";
3655
- import { mkdir as mkdir7, writeFile as writeFile10 } from "fs/promises";
3656
- import chalk10 from "chalk";
3873
+ import { existsSync as existsSync18 } from "fs";
3874
+ import { mkdir as mkdir7, writeFile as writeFile11 } from "fs/promises";
3875
+ import chalk11 from "chalk";
3657
3876
  async function figmaSetup() {
3658
3877
  const { select, confirm } = await import("@inquirer/prompts");
3659
3878
  logger.logoOnce(logo());
@@ -3663,15 +3882,15 @@ async function figmaSetup() {
3663
3882
  let selectedAgent;
3664
3883
  if (agents.length === 0) {
3665
3884
  logger.warn("No AI agents found (claude, cursor, codex).");
3666
- logger.info(`Install Claude Code: ${chalk10.cyan("npm install -g @anthropic-ai/claude-code")}`);
3885
+ logger.info(`Install Claude Code: ${chalk11.cyan("npm install -g @anthropic-ai/claude-code")}`);
3667
3886
  selectedAgent = "none";
3668
3887
  } else {
3669
- logger.success(`Found: ${agents.map((a) => chalk10.cyan(a.type)).join(", ")}`);
3888
+ logger.success(`Found: ${agents.map((a) => chalk11.cyan(a.type)).join(", ")}`);
3670
3889
  selectedAgent = await select({
3671
3890
  message: "Which agent do you want to use with eds-cli?",
3672
3891
  choices: [
3673
3892
  ...agents.map((a) => ({
3674
- name: `${a.type}${a.hasFigmaMcp ? chalk10.green(" (Figma MCP detected)") : ""}`,
3893
+ name: `${a.type}${a.hasFigmaMcp ? chalk11.green(" (Figma MCP detected)") : ""}`,
3675
3894
  value: a.type
3676
3895
  })),
3677
3896
  { name: "None (manual mode)", value: "none" }
@@ -3687,17 +3906,17 @@ async function figmaSetup() {
3687
3906
  switch (selectedAgent) {
3688
3907
  case "claude":
3689
3908
  logger.info(
3690
- ` ${chalk10.cyan("claude mcp add --transport sse figma-sse https://mcp.figma.com/sse")}`
3909
+ ` ${chalk11.cyan("claude mcp add --transport sse figma-sse https://mcp.figma.com/sse")}`
3691
3910
  );
3692
3911
  logger.info(" or for Figma Desktop app:");
3693
3912
  logger.info(
3694
- ` ${chalk10.cyan("claude mcp add --transport http figma-desktop http://127.0.0.1:3845/mcp")}`
3913
+ ` ${chalk11.cyan("claude mcp add --transport http figma-desktop http://127.0.0.1:3845/mcp")}`
3695
3914
  );
3696
3915
  break;
3697
3916
  case "cursor":
3698
- logger.info(` Add to ${chalk10.cyan("~/.cursor/mcp.json")}:`);
3917
+ logger.info(` Add to ${chalk11.cyan("~/.cursor/mcp.json")}:`);
3699
3918
  logger.info(
3700
- chalk10.dim(
3919
+ chalk11.dim(
3701
3920
  JSON.stringify(
3702
3921
  {
3703
3922
  mcpServers: {
@@ -3722,7 +3941,7 @@ async function figmaSetup() {
3722
3941
  }
3723
3942
  }
3724
3943
  logger.info(
3725
- chalk10.dim(
3944
+ chalk11.dim(
3726
3945
  "\nNote: Figma MCP requires a Figma seat (Dev or Full on paid plans for desktop server)."
3727
3946
  )
3728
3947
  );
@@ -3732,22 +3951,22 @@ async function figmaSetup() {
3732
3951
  });
3733
3952
  if (shouldSave) {
3734
3953
  const configDir = globalConfigDir();
3735
- if (!existsSync17(configDir)) {
3954
+ if (!existsSync18(configDir)) {
3736
3955
  await mkdir7(configDir, { recursive: true });
3737
3956
  }
3738
3957
  const config = {};
3739
3958
  if (selectedAgent !== "none") {
3740
3959
  config.agent = selectedAgent;
3741
3960
  }
3742
- await writeFile10(globalConfigPath(), `${JSON.stringify(config, null, 2)}
3961
+ await writeFile11(globalConfigPath(), `${JSON.stringify(config, null, 2)}
3743
3962
  `, {
3744
3963
  mode: 384
3745
3964
  });
3746
- logger.success(`Config saved to ${chalk10.cyan(globalConfigPath())}`);
3965
+ logger.success(`Config saved to ${chalk11.cyan(globalConfigPath())}`);
3747
3966
  }
3748
- logger.info(chalk10.bold("\nSetup complete!"));
3967
+ logger.info(chalk11.bold("\nSetup complete!"));
3749
3968
  if (selectedAgent !== "none") {
3750
- logger.info(`Try: ${chalk10.cyan('eds block from-design "<figma-url>" --dry-run')}`);
3969
+ logger.info(`Try: ${chalk11.cyan('eds block from-design "<figma-url>" --dry-run')}`);
3751
3970
  }
3752
3971
  }
3753
3972
  var init_setup = __esm({
@@ -3766,9 +3985,9 @@ var doctor_exports = {};
3766
3985
  __export(doctor_exports, {
3767
3986
  doctor: () => doctor
3768
3987
  });
3769
- import { existsSync as existsSync18 } from "fs";
3770
- import { readFile as readFile13, readdir as readdir3 } from "fs/promises";
3771
- import path20 from "path";
3988
+ import { existsSync as existsSync19 } from "fs";
3989
+ import { readFile as readFile14, readdir as readdir3 } from "fs/promises";
3990
+ import path21 from "path";
3772
3991
  import { parse as parseYaml } from "yaml";
3773
3992
  async function doctor(options = {}) {
3774
3993
  const projectRoot = findProjectRoot();
@@ -3790,7 +4009,7 @@ async function doctor(options = {}) {
3790
4009
  });
3791
4010
  if (project.hasFstab) {
3792
4011
  try {
3793
- const content = await readFile13(path20.join(projectRoot, "fstab.yaml"), "utf-8");
4012
+ const content = await readFile14(path21.join(projectRoot, "fstab.yaml"), "utf-8");
3794
4013
  parseYaml(content);
3795
4014
  results.push({ name: "fstab.yaml valid YAML", status: "pass", message: "Valid" });
3796
4015
  } catch {
@@ -3812,28 +4031,28 @@ async function doctor(options = {}) {
3812
4031
  message: project.hasBlocks ? "Found" : "Missing - no blocks yet"
3813
4032
  });
3814
4033
  if (project.hasBlocks) {
3815
- const blocksDir = path20.join(projectRoot, "blocks");
4034
+ const blocksDir = path21.join(projectRoot, "blocks");
3816
4035
  const entries = await readdir3(blocksDir, { withFileTypes: true });
3817
4036
  let aggregatedModels = "";
3818
- const aggregatedPath = path20.join(projectRoot, "component-models.json");
3819
- if (existsSync18(aggregatedPath)) {
4037
+ const aggregatedPath = path21.join(projectRoot, "component-models.json");
4038
+ if (existsSync19(aggregatedPath)) {
3820
4039
  try {
3821
- aggregatedModels = await readFile13(aggregatedPath, "utf-8");
4040
+ aggregatedModels = await readFile14(aggregatedPath, "utf-8");
3822
4041
  } catch {
3823
4042
  }
3824
4043
  }
3825
4044
  for (const entry of entries) {
3826
4045
  if (!entry.isDirectory()) continue;
3827
4046
  const blockName = entry.name;
3828
- const jsFile = path20.join(blocksDir, blockName, `${blockName}.js`);
3829
- const hasJs = existsSync18(jsFile);
4047
+ const jsFile = path21.join(blocksDir, blockName, `${blockName}.js`);
4048
+ const hasJs = existsSync19(jsFile);
3830
4049
  results.push({
3831
4050
  name: `blocks/${blockName} has JS`,
3832
4051
  status: hasJs ? "pass" : "warn",
3833
4052
  message: hasJs ? "Found" : `Missing ${blockName}.js`
3834
4053
  });
3835
4054
  if (hasJs) {
3836
- const jsContent = await readFile13(jsFile, "utf-8");
4055
+ const jsContent = await readFile14(jsFile, "utf-8");
3837
4056
  if (jsContent.includes("console.log")) {
3838
4057
  results.push({
3839
4058
  name: `blocks/${blockName} no console.log`,
@@ -3842,17 +4061,17 @@ async function doctor(options = {}) {
3842
4061
  });
3843
4062
  }
3844
4063
  }
3845
- const hasPerBlockModel = existsSync18(path20.join(blocksDir, blockName, `_${blockName}.json`));
4064
+ const hasPerBlockModel = existsSync19(path21.join(blocksDir, blockName, `_${blockName}.json`));
3846
4065
  const hasAggregatedModel = aggregatedModels.includes(`"${blockName}"`);
3847
4066
  results.push({
3848
4067
  name: `blocks/${blockName} UE model`,
3849
4068
  status: hasPerBlockModel || hasAggregatedModel ? "pass" : "warn",
3850
4069
  message: hasPerBlockModel || hasAggregatedModel ? hasPerBlockModel ? `_${blockName}.json` : "in component-models.json" : "no Universal Editor model - authors can't edit this block in UE"
3851
4070
  });
3852
- const metaFile = path20.join(blocksDir, blockName, ".eds-meta.json");
3853
- if (existsSync18(metaFile)) {
4071
+ const metaFile = path21.join(blocksDir, blockName, ".eds-meta.json");
4072
+ if (existsSync19(metaFile)) {
3854
4073
  try {
3855
- const meta = JSON.parse(await readFile13(metaFile, "utf-8"));
4074
+ const meta = JSON.parse(await readFile14(metaFile, "utf-8"));
3856
4075
  const syncDate = new Date(meta.lastSyncedAt);
3857
4076
  const daysSinceSync = Math.floor(
3858
4077
  (Date.now() - syncDate.getTime()) / (1e3 * 60 * 60 * 24)
@@ -3872,10 +4091,10 @@ async function doctor(options = {}) {
3872
4091
  }
3873
4092
  }
3874
4093
  }
3875
- const queryFile = path20.join(projectRoot, "helix-query.yaml");
3876
- if (existsSync18(queryFile)) {
4094
+ const queryFile = path21.join(projectRoot, "helix-query.yaml");
4095
+ if (existsSync19(queryFile)) {
3877
4096
  try {
3878
- const content = await readFile13(queryFile, "utf-8");
4097
+ const content = await readFile14(queryFile, "utf-8");
3879
4098
  parseYaml(content);
3880
4099
  results.push({ name: "helix-query.yaml valid", status: "pass", message: "Valid YAML" });
3881
4100
  } catch {
@@ -3932,10 +4151,10 @@ var audit_loading_exports = {};
3932
4151
  __export(audit_loading_exports, {
3933
4152
  auditLoading: () => auditLoading
3934
4153
  });
3935
- import { existsSync as existsSync19 } from "fs";
3936
- import { readFile as readFile14, readdir as readdir4, stat as stat3 } from "fs/promises";
3937
- import path21 from "path";
3938
- import chalk11 from "chalk";
4154
+ import { existsSync as existsSync20 } from "fs";
4155
+ import { readFile as readFile15, readdir as readdir4, stat as stat3 } from "fs/promises";
4156
+ import path22 from "path";
4157
+ import chalk12 from "chalk";
3939
4158
  async function auditLoading(options = {}) {
3940
4159
  const projectRoot = findProjectRoot();
3941
4160
  if (!projectRoot) {
@@ -3968,8 +4187,8 @@ async function auditLoading(options = {}) {
3968
4187
  printResults(results);
3969
4188
  }
3970
4189
  async function checkHeadHtml(root, results) {
3971
- const filePath = path21.join(root, "head.html");
3972
- if (!existsSync19(filePath)) {
4190
+ const filePath = path22.join(root, "head.html");
4191
+ if (!existsSync20(filePath)) {
3973
4192
  results.push({
3974
4193
  rule: "head.html exists",
3975
4194
  status: "warn",
@@ -3977,7 +4196,7 @@ async function checkHeadHtml(root, results) {
3977
4196
  });
3978
4197
  return;
3979
4198
  }
3980
- const content = await readFile14(filePath, "utf-8");
4199
+ const content = await readFile15(filePath, "utf-8");
3981
4200
  const lines = content.split("\n");
3982
4201
  for (let i = 0; i < lines.length; i++) {
3983
4202
  const line = lines[i];
@@ -4049,8 +4268,8 @@ async function checkHeadHtml(root, results) {
4049
4268
  }
4050
4269
  }
4051
4270
  async function checkScriptsJs(root, results) {
4052
- const filePath = path21.join(root, "scripts", "scripts.js");
4053
- if (!existsSync19(filePath)) {
4271
+ const filePath = path22.join(root, "scripts", "scripts.js");
4272
+ if (!existsSync20(filePath)) {
4054
4273
  results.push({
4055
4274
  rule: "scripts/scripts.js exists",
4056
4275
  status: "warn",
@@ -4058,7 +4277,7 @@ async function checkScriptsJs(root, results) {
4058
4277
  });
4059
4278
  return;
4060
4279
  }
4061
- const content = await readFile14(filePath, "utf-8");
4280
+ const content = await readFile15(filePath, "utf-8");
4062
4281
  const lines = content.split("\n");
4063
4282
  const hasLoadEager = /function\s+loadEager|loadEager\s*=/i.test(content);
4064
4283
  const hasLoadLazy = /function\s+loadLazy|loadLazy\s*=/i.test(content);
@@ -4136,8 +4355,8 @@ async function checkScriptsJs(root, results) {
4136
4355
  }
4137
4356
  }
4138
4357
  async function checkStylesCss(root, results) {
4139
- const filePath = path21.join(root, "styles", "styles.css");
4140
- if (!existsSync19(filePath)) return;
4358
+ const filePath = path22.join(root, "styles", "styles.css");
4359
+ if (!existsSync20(filePath)) return;
4141
4360
  const stats = await stat3(filePath);
4142
4361
  const sizeKb = stats.size / 1024;
4143
4362
  results.push({
@@ -4146,7 +4365,7 @@ async function checkStylesCss(root, results) {
4146
4365
  message: `${sizeKb.toFixed(1)} KB${sizeKb > 40 ? " \u2014 large CSS competes with 100KB LCP budget" : ""}`,
4147
4366
  file: "styles/styles.css"
4148
4367
  });
4149
- const content = await readFile14(filePath, "utf-8");
4368
+ const content = await readFile15(filePath, "utf-8");
4150
4369
  if (/@import\s/.test(content)) {
4151
4370
  results.push({
4152
4371
  rule: "No @import in styles.css",
@@ -4157,8 +4376,8 @@ async function checkStylesCss(root, results) {
4157
4376
  }
4158
4377
  }
4159
4378
  async function checkDelayedJs(root, results) {
4160
- const filePath = path21.join(root, "scripts", "delayed.js");
4161
- if (!existsSync19(filePath)) {
4379
+ const filePath = path22.join(root, "scripts", "delayed.js");
4380
+ if (!existsSync20(filePath)) {
4162
4381
  results.push({
4163
4382
  rule: "scripts/delayed.js exists",
4164
4383
  status: "info",
@@ -4166,7 +4385,7 @@ async function checkDelayedJs(root, results) {
4166
4385
  });
4167
4386
  return;
4168
4387
  }
4169
- const content = await readFile14(filePath, "utf-8");
4388
+ const content = await readFile15(filePath, "utf-8");
4170
4389
  const stripped = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").trim();
4171
4390
  if (!stripped) {
4172
4391
  results.push({
@@ -4178,15 +4397,15 @@ async function checkDelayedJs(root, results) {
4178
4397
  }
4179
4398
  }
4180
4399
  async function checkBlocks(root, results) {
4181
- const blocksDir = path21.join(root, "blocks");
4182
- if (!existsSync19(blocksDir)) return;
4400
+ const blocksDir = path22.join(root, "blocks");
4401
+ if (!existsSync20(blocksDir)) return;
4183
4402
  const entries = await readdir4(blocksDir, { withFileTypes: true });
4184
4403
  for (const entry of entries) {
4185
4404
  if (!entry.isDirectory()) continue;
4186
4405
  const blockName = entry.name;
4187
- const jsFile = path21.join(blocksDir, blockName, `${blockName}.js`);
4188
- if (!existsSync19(jsFile)) continue;
4189
- const content = await readFile14(jsFile, "utf-8");
4406
+ const jsFile = path22.join(blocksDir, blockName, `${blockName}.js`);
4407
+ if (!existsSync20(jsFile)) continue;
4408
+ const content = await readFile15(jsFile, "utf-8");
4190
4409
  const lines = content.split("\n");
4191
4410
  const heavyLibs = [
4192
4411
  "jquery",
@@ -4236,9 +4455,9 @@ async function checkBlocks(root, results) {
4236
4455
  for (const entry of entries) {
4237
4456
  if (!entry.isDirectory()) continue;
4238
4457
  const blockName = entry.name;
4239
- const cssFile = path21.join(blocksDir, blockName, `${blockName}.css`);
4240
- if (!existsSync19(cssFile)) continue;
4241
- const content = await readFile14(cssFile, "utf-8");
4458
+ const cssFile = path22.join(blocksDir, blockName, `${blockName}.css`);
4459
+ if (!existsSync20(cssFile)) continue;
4460
+ const content = await readFile15(cssFile, "utf-8");
4242
4461
  const selectorPattern = /^([a-z][a-z0-9-]*)\s*\{/gm;
4243
4462
  for (const selectorMatch of content.matchAll(selectorPattern)) {
4244
4463
  const selector = selectorMatch[1];
@@ -4254,8 +4473,8 @@ async function checkBlocks(root, results) {
4254
4473
  }
4255
4474
  }
4256
4475
  async function checkAemJs(root, results) {
4257
- const filePath = path21.join(root, "scripts", "aem.js");
4258
- if (!existsSync19(filePath)) {
4476
+ const filePath = path22.join(root, "scripts", "aem.js");
4477
+ if (!existsSync20(filePath)) {
4259
4478
  results.push({
4260
4479
  rule: "scripts/aem.js exists",
4261
4480
  status: "warn",
@@ -4293,12 +4512,12 @@ function printResults(results) {
4293
4512
  grouped.set(key, bucket);
4294
4513
  }
4295
4514
  for (const [file, fileResults] of grouped) {
4296
- logger.info(` ${icon.bullet} ${chalk11.underline(file)}`);
4515
+ logger.info(` ${icon.bullet} ${chalk12.underline(file)}`);
4297
4516
  for (const r of fileResults) {
4298
- const loc = r.line ? chalk11.dim(`:${r.line}`) : "";
4517
+ const loc = r.line ? chalk12.dim(`:${r.line}`) : "";
4299
4518
  logger.info(` ${statusIcon(r.status)} ${r.rule}${loc}`);
4300
4519
  if (r.status !== "pass") {
4301
- logger.info(chalk11.dim(` ${r.message}`));
4520
+ logger.info(chalk12.dim(` ${r.message}`));
4302
4521
  }
4303
4522
  }
4304
4523
  logger.info("");
@@ -4327,14 +4546,14 @@ var audit_security_exports = {};
4327
4546
  __export(audit_security_exports, {
4328
4547
  auditSecurity: () => auditSecurity
4329
4548
  });
4330
- import { existsSync as existsSync20 } from "fs";
4331
- import { readFile as readFile15, readdir as readdir5 } from "fs/promises";
4332
- import path22 from "path";
4333
- import chalk12 from "chalk";
4549
+ import { existsSync as existsSync21 } from "fs";
4550
+ import { readFile as readFile16, readdir as readdir5 } from "fs/promises";
4551
+ import path23 from "path";
4552
+ import chalk13 from "chalk";
4334
4553
  async function* walk2(dir) {
4335
4554
  const entries = await readdir5(dir, { withFileTypes: true });
4336
4555
  for (const entry of entries) {
4337
- const full = path22.join(dir, entry.name);
4556
+ const full = path23.join(dir, entry.name);
4338
4557
  if (entry.isDirectory()) {
4339
4558
  if (IGNORE_DIRS.has(entry.name)) continue;
4340
4559
  yield* walk2(full);
@@ -4368,11 +4587,11 @@ function scanContent(content, relFile, ext) {
4368
4587
  return findings;
4369
4588
  }
4370
4589
  async function auditDependencies(root) {
4371
- if (!existsSync20(path22.join(root, "package.json"))) {
4590
+ if (!existsSync21(path23.join(root, "package.json"))) {
4372
4591
  return { status: "skipped", note: "no package.json" };
4373
4592
  }
4374
- const hasLock = existsSync20(path22.join(root, "package-lock.json")) || existsSync20(path22.join(root, "npm-shrinkwrap.json"));
4375
- if (!hasLock && !existsSync20(path22.join(root, "node_modules"))) {
4593
+ const hasLock = existsSync21(path23.join(root, "package-lock.json")) || existsSync21(path23.join(root, "npm-shrinkwrap.json"));
4594
+ if (!hasLock && !existsSync21(path23.join(root, "node_modules"))) {
4376
4595
  return { status: "skipped", note: "no package-lock.json / node_modules \u2014 run npm install" };
4377
4596
  }
4378
4597
  try {
@@ -4404,11 +4623,11 @@ async function auditSecurity(options = {}) {
4404
4623
  }
4405
4624
  const findings = [];
4406
4625
  for await (const file of walk2(projectRoot)) {
4407
- const ext = path22.extname(file).slice(1).toLowerCase();
4626
+ const ext = path23.extname(file).slice(1).toLowerCase();
4408
4627
  if (!SCAN_EXT.has(ext)) continue;
4409
- const rel = path22.relative(projectRoot, file);
4628
+ const rel = path23.relative(projectRoot, file);
4410
4629
  if (rel.endsWith("aem.js") || rel.endsWith("lib-franklin.js")) continue;
4411
- const content = await readFile15(file, "utf-8");
4630
+ const content = await readFile16(file, "utf-8");
4412
4631
  findings.push(...scanContent(content, rel, ext));
4413
4632
  }
4414
4633
  const deps = await auditDependencies(projectRoot);
@@ -4456,9 +4675,9 @@ async function auditSecurity(options = {}) {
4456
4675
  );
4457
4676
  for (const f of sorted) {
4458
4677
  const meta = SEVERITY_META[f.severity];
4459
- const loc = chalk12.dim(`${f.file}:${f.line}`);
4460
- logger.info(` ${statusIcon(meta.status)} ${chalk12.bold(meta.label)} ${f.message} ${loc}`);
4461
- logger.info(chalk12.dim(` ${f.snippet}`));
4678
+ const loc = chalk13.dim(`${f.file}:${f.line}`);
4679
+ logger.info(` ${statusIcon(meta.status)} ${chalk13.bold(meta.label)} ${f.message} ${loc}`);
4680
+ logger.info(chalk13.dim(` ${f.snippet}`));
4462
4681
  }
4463
4682
  }
4464
4683
  const summaryLine = summary({
@@ -4665,7 +4884,7 @@ __export(preview_exports2, {
4665
4884
  preview: () => preview,
4666
4885
  resolveAdminConfig: () => resolveAdminConfig
4667
4886
  });
4668
- import chalk13 from "chalk";
4887
+ import chalk14 from "chalk";
4669
4888
  import ora2 from "ora";
4670
4889
  async function preview(paths, options) {
4671
4890
  const projectRoot = findProjectRoot();
@@ -4678,7 +4897,7 @@ async function preview(paths, options) {
4678
4897
  let hasErrors = false;
4679
4898
  for (const r of results) {
4680
4899
  if (r.ok) {
4681
- logger.success(`${r.path} \u2192 ${chalk13.cyan(r.previewUrl || "preview updated")}`);
4900
+ logger.success(`${r.path} \u2192 ${chalk14.cyan(r.previewUrl || "preview updated")}`);
4682
4901
  } else {
4683
4902
  logger.error(`${r.path} \u2014 ${r.message}`);
4684
4903
  hasErrors = true;
@@ -4692,7 +4911,7 @@ function resolveAdminConfig(config, options) {
4692
4911
  const ref = options.ref || config.admin?.ref || "main";
4693
4912
  if (!org || !site) {
4694
4913
  logger.error("Missing --org and --site. Set them via flags or in .edsrc.json:");
4695
- logger.info(chalk13.dim(' { "admin": { "org": "my-org", "site": "my-site" } }'));
4914
+ logger.info(chalk14.dim(' { "admin": { "org": "my-org", "site": "my-site" } }'));
4696
4915
  process.exitCode = 1;
4697
4916
  return void 0;
4698
4917
  }
@@ -4714,7 +4933,7 @@ var publish_exports = {};
4714
4933
  __export(publish_exports, {
4715
4934
  publish: () => publish
4716
4935
  });
4717
- import chalk14 from "chalk";
4936
+ import chalk15 from "chalk";
4718
4937
  import ora3 from "ora";
4719
4938
  async function publish(paths, options) {
4720
4939
  const projectRoot = findProjectRoot();
@@ -4727,7 +4946,7 @@ async function publish(paths, options) {
4727
4946
  let hasErrors = false;
4728
4947
  for (const r of results) {
4729
4948
  if (r.ok) {
4730
- logger.success(`${r.path} \u2192 ${chalk14.cyan(r.liveUrl || "published")}`);
4949
+ logger.success(`${r.path} \u2192 ${chalk15.cyan(r.liveUrl || "published")}`);
4731
4950
  } else {
4732
4951
  logger.error(`${r.path} \u2014 ${r.message}`);
4733
4952
  hasErrors = true;
@@ -4826,12 +5045,12 @@ var menu_exports = {};
4826
5045
  __export(menu_exports, {
4827
5046
  mainMenu: () => mainMenu
4828
5047
  });
4829
- import { existsSync as existsSync21 } from "fs";
5048
+ import { existsSync as existsSync22 } from "fs";
4830
5049
  import { readdir as readdir6 } from "fs/promises";
4831
- import path23 from "path";
5050
+ import path24 from "path";
4832
5051
  async function listBlockNames(projectRoot) {
4833
- const dir = path23.join(projectRoot, "blocks");
4834
- if (!existsSync21(dir)) return [];
5052
+ const dir = path24.join(projectRoot, "blocks");
5053
+ if (!existsSync22(dir)) return [];
4835
5054
  const entries = await readdir6(dir, { withFileTypes: true });
4836
5055
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
4837
5056
  }
@@ -4842,11 +5061,15 @@ async function mainMenu() {
4842
5061
  message: "Choose an action",
4843
5062
  choices: [
4844
5063
  { name: "Set up this project (authoring model, CI, naming)", value: "init" },
4845
- { name: "Generate a block from a design (Figma/Stitch/Canva/Sketch)", value: "from-design" },
5064
+ {
5065
+ name: "Generate a block from a design (Figma/Stitch/Canva/Sketch/Framer)",
5066
+ value: "from-design"
5067
+ },
4846
5068
  { name: "Migrate a classic AEM component to EDS", value: "migrate" },
4847
5069
  { name: "Scaffold Universal Editor components", value: "scaffold" },
4848
5070
  { name: "Create an empty block", value: "create" },
4849
5071
  { name: "Add a third-party integration (GTM, chat, consent\u2026)", value: "integrate" },
5072
+ { name: "Instrument a block for analytics (dataLayer)", value: "track" },
4850
5073
  { name: "Preview a block in the browser", value: "preview-block" },
4851
5074
  { name: "Preview / publish pages (Admin API)", value: "admin" },
4852
5075
  { name: "Audit the project", value: "audit" },
@@ -4907,6 +5130,25 @@ async function mainMenu() {
4907
5130
  await previewBlock2(name, {});
4908
5131
  return;
4909
5132
  }
5133
+ case "track": {
5134
+ const projectRoot = findProjectRoot();
5135
+ if (!projectRoot) {
5136
+ logger.error("Not inside an EDS project (no EDS project markers found).");
5137
+ return;
5138
+ }
5139
+ const blocks = await listBlockNames(projectRoot);
5140
+ if (blocks.length === 0) {
5141
+ logger.warn("No blocks found in blocks/.");
5142
+ return;
5143
+ }
5144
+ const name = await select({
5145
+ message: "Which block to instrument?",
5146
+ choices: blocks.map((b) => ({ name: b, value: b }))
5147
+ });
5148
+ const { trackBlock: trackBlock2 } = await Promise.resolve().then(() => (init_track2(), track_exports));
5149
+ await trackBlock2(name, {});
5150
+ return;
5151
+ }
4910
5152
  case "admin": {
4911
5153
  const { adminWizard: adminWizard2 } = await Promise.resolve().then(() => (init_admin(), admin_exports));
4912
5154
  await adminWizard2();
@@ -4952,13 +5194,13 @@ var init_menu = __esm({
4952
5194
 
4953
5195
  // src/bin/eds.ts
4954
5196
  init_esm_shims();
4955
- import chalk15 from "chalk";
5197
+ import chalk16 from "chalk";
4956
5198
  import { Command } from "commander";
4957
5199
 
4958
5200
  // package.json
4959
5201
  var package_default = {
4960
5202
  name: "aem-eds-kit",
4961
- version: "0.4.9",
5203
+ version: "0.4.12",
4962
5204
  description: "CLI for AEM Edge Delivery Services with Figma MCP integration",
4963
5205
  type: "module",
4964
5206
  engines: {
@@ -5034,7 +5276,7 @@ program.name("eds").description("CLI for AEM Edge Delivery Services with Figma i
5034
5276
  const opts = thisCommand.opts();
5035
5277
  if (opts.verbose) logger.setLevel("debug");
5036
5278
  if (opts.quiet) logger.setQuiet(true);
5037
- if (opts.color === false) chalk15.level = 0;
5279
+ if (opts.color === false) chalk16.level = 0;
5038
5280
  });
5039
5281
  program.addHelpText("beforeAll", () => logo());
5040
5282
  program.command("init").description("Set up a freshly cloned EDS boilerplate for your authoring model (interactive)").option("--name <name>", "Project name (kebab-case)").option("--authoring <model>", "Authoring model: ue | da | gdrive | sharepoint").option("--mountpoint <url>", "Content source URL (document models)").option("--org <org>", "DA org (authoring=da)").option("--site <site>", "DA site (authoring=da)").option("--no-ci", "Skip the GitHub Actions CI workflow").option("--yes", "Skip prompts; apply with flags/defaults").action(async (options) => {
@@ -5046,6 +5288,11 @@ migrate.command("component [dir]").description("Migrate a classic AEM component'
5046
5288
  const { migrateComponent: migrateComponent2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
5047
5289
  await migrateComponent2(dir, options);
5048
5290
  });
5291
+ var track = program.command("track").description("Instrument blocks for analytics");
5292
+ track.command("block <name>").description("Add dataLayer tracking (click + form submit) to a block's decorate()").option("--no-click", "Skip click tracking").option("--no-submit", "Skip form-submit tracking").action(async (name, options) => {
5293
+ const { trackBlock: trackBlock2 } = await Promise.resolve().then(() => (init_track2(), track_exports));
5294
+ await trackBlock2(name, options);
5295
+ });
5049
5296
  var block = program.command("block").description("Manage EDS blocks");
5050
5297
  block.command("create <name>").description("Scaffold a new EDS block (with Universal Editor model by default)").option("--no-ue-model", "Skip the Universal Editor model (generated by default)").action(async (name, options) => {
5051
5298
  const { createBlock: createBlock2 } = await Promise.resolve().then(() => (init_create(), create_exports));