aem-eds-kit 0.4.7 → 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
@@ -1424,16 +1424,582 @@ var init_init = __esm({
1424
1424
  }
1425
1425
  });
1426
1426
 
1427
+ // src/lib/migrate/clientlib.ts
1428
+ import { existsSync as existsSync5 } from "fs";
1429
+ import { readFile as readFile3, readdir } from "fs/promises";
1430
+ import path7 from "path";
1431
+ async function listFiles(dir) {
1432
+ const out = [];
1433
+ const walk3 = async (d) => {
1434
+ for (const e of await readdir(d, { withFileTypes: true })) {
1435
+ const p = path7.join(d, e.name);
1436
+ if (e.isDirectory()) await walk3(p);
1437
+ else out.push(p);
1438
+ }
1439
+ };
1440
+ if (existsSync5(dir)) await walk3(dir);
1441
+ return out;
1442
+ }
1443
+ async function findClientlibDir(componentDir) {
1444
+ if (!existsSync5(componentDir)) return null;
1445
+ const entries = await readdir(componentDir, { withFileTypes: true });
1446
+ const match = entries.find((e) => e.isDirectory() && /clientlib/i.test(e.name));
1447
+ if (match) return path7.join(componentDir, match.name);
1448
+ const hasCss = entries.some((e) => e.isFile() && e.name.endsWith(".css"));
1449
+ return hasCss ? componentDir : null;
1450
+ }
1451
+ async function readClientlibCss(clientlibDir) {
1452
+ const all = await listFiles(clientlibDir);
1453
+ const preprocessed = all.filter((f) => /\.(less|scss|sass)$/i.test(f)).map((f) => path7.relative(clientlibDir, f));
1454
+ let cssFiles;
1455
+ const cssTxt = path7.join(clientlibDir, "css.txt");
1456
+ if (existsSync5(cssTxt)) {
1457
+ const lines = (await readFile3(cssTxt, "utf-8")).split("\n").map((l) => l.trim());
1458
+ let base = clientlibDir;
1459
+ const listed = [];
1460
+ for (const l of lines) {
1461
+ if (!l) continue;
1462
+ const baseMatch = l.match(/^#base\s*=\s*(.+)$/);
1463
+ if (baseMatch) base = path7.join(clientlibDir, baseMatch[1].trim());
1464
+ else if (!l.startsWith("#")) listed.push(l);
1465
+ }
1466
+ cssFiles = listed.map((rel) => path7.join(base, rel)).filter((f) => existsSync5(f) && f.endsWith(".css"));
1467
+ } else {
1468
+ cssFiles = all.filter((f) => f.endsWith(".css"));
1469
+ }
1470
+ const parts = [];
1471
+ for (const f of cssFiles) parts.push(await readFile3(f, "utf-8"));
1472
+ return {
1473
+ css: parts.join("\n\n"),
1474
+ files: cssFiles.map((f) => path7.relative(clientlibDir, f)),
1475
+ preprocessed
1476
+ };
1477
+ }
1478
+ var init_clientlib = __esm({
1479
+ "src/lib/migrate/clientlib.ts"() {
1480
+ "use strict";
1481
+ init_esm_shims();
1482
+ }
1483
+ });
1484
+
1485
+ // src/lib/migrate/dialog.ts
1486
+ import { XMLParser } from "fast-xml-parser";
1487
+ function tagOf(n) {
1488
+ return Object.keys(n).find((k) => k !== ":@") ?? "";
1489
+ }
1490
+ function childrenOf(n) {
1491
+ const t = tagOf(n);
1492
+ const v = n[t];
1493
+ return Array.isArray(v) ? v : [];
1494
+ }
1495
+ function attrsOf(n) {
1496
+ return n[":@"] ?? {};
1497
+ }
1498
+ function stripType(v) {
1499
+ return typeof v === "string" ? v.replace(/^\{[^}]+\}/, "") : v;
1500
+ }
1501
+ function attr(n, name) {
1502
+ return stripType(attrsOf(n)[`@_${name}`]);
1503
+ }
1504
+ function leafComponent(rt, n) {
1505
+ const multiple = attr(n, "multiple") === "true" || attr(n, "multiple") === "{Boolean}true";
1506
+ if (rt.endsWith("/form/textfield")) return "text";
1507
+ if (rt.endsWith("/form/textarea")) return "textarea";
1508
+ if (rt.includes("/richtext")) return "richtext";
1509
+ if (rt.endsWith("/form/numberfield")) return "number";
1510
+ if (rt.endsWith("/form/checkbox")) return "boolean";
1511
+ if (rt.endsWith("/form/radiogroup")) return "radio-group";
1512
+ if (rt.endsWith("/form/select")) return multiple ? "multiselect" : "select";
1513
+ if (rt.endsWith("/form/datepicker")) return "date-time";
1514
+ if (rt.endsWith("/form/pathfield") || rt.endsWith("/form/pathbrowser")) return "aem-content";
1515
+ if (rt.includes("/fileupload") || rt.endsWith("/form/fileupload")) return "reference";
1516
+ if (rt.includes("tagfield") || rt.includes("tagpicker") || rt.includes("tagbrowser"))
1517
+ return "aem-tag";
1518
+ if (rt.includes("contentfragment")) return "aem-content-fragment";
1519
+ if (rt.includes("experiencefragment")) return "aem-experience-fragment";
1520
+ return null;
1521
+ }
1522
+ function isWrapper(rt) {
1523
+ return rt.includes("/foundation/container") || rt.includes("/foundation/fixedcolumns") || rt.includes("/foundation/well") || rt.includes("/foundation/tabs") || rt.includes("/foundation/form/fieldset") || rt.includes("/foundation/accordion");
1524
+ }
1525
+ function valueTypeFor(c) {
1526
+ switch (c) {
1527
+ case "number":
1528
+ return "number";
1529
+ case "boolean":
1530
+ return "boolean";
1531
+ case "date-time":
1532
+ return "date";
1533
+ case "checkbox-group":
1534
+ return "string[]";
1535
+ case "aem-content":
1536
+ case "aem-content-fragment":
1537
+ case "aem-experience-fragment":
1538
+ return void 0;
1539
+ default:
1540
+ return "string";
1541
+ }
1542
+ }
1543
+ function cleanName(raw, fallback) {
1544
+ const n = (raw ?? "").replace(/^\.\//, "").trim();
1545
+ return n || fallback;
1546
+ }
1547
+ function readOptions(fieldNode) {
1548
+ const items = childrenOf(fieldNode).find((c) => tagOf(c) === "items");
1549
+ if (!items) return [];
1550
+ return childrenOf(items).map((opt) => {
1551
+ const value = attr(opt, "value") ?? "";
1552
+ const name = attr(opt, "text") ?? attr(opt, "jcr:title") ?? value;
1553
+ return { name, value };
1554
+ });
1555
+ }
1556
+ function walk(nodes, out, unmapped, counter) {
1557
+ for (const node of nodes) {
1558
+ const rt = attr(node, "sling:resourceType") ?? "";
1559
+ if (rt.endsWith("/foundation/tabs")) {
1560
+ const items = childrenOf(node).find((c) => tagOf(c) === "items");
1561
+ for (const tab of items ? childrenOf(items) : []) {
1562
+ const label = attr(tab, "jcr:title");
1563
+ out.push({ component: "tab", name: cleanName(void 0, tagOf(tab)), label });
1564
+ walk(childrenOf(tab), out, unmapped, counter);
1565
+ }
1566
+ continue;
1567
+ }
1568
+ if (rt.endsWith("/form/multifield")) {
1569
+ const sub = [];
1570
+ walk(childrenOf(node), sub, unmapped, counter);
1571
+ const name = cleanName(attr(node, "name"), `items${counter.i++}`);
1572
+ out.push({
1573
+ component: "container",
1574
+ name,
1575
+ label: attr(node, "fieldLabel") ?? "Items",
1576
+ multi: true,
1577
+ collapsible: true,
1578
+ fields: sub
1579
+ });
1580
+ continue;
1581
+ }
1582
+ const comp = rt ? leafComponent(rt, node) : null;
1583
+ if (comp) {
1584
+ const name = cleanName(attr(node, "name"), `field${counter.i++}`);
1585
+ const field = { component: comp, name };
1586
+ const vt = valueTypeFor(comp);
1587
+ if (vt) field.valueType = vt;
1588
+ const label = attr(node, "fieldLabel") ?? attr(node, "text");
1589
+ if (label) field.label = label;
1590
+ if (comp === "select" || comp === "radio-group" || comp === "multiselect") {
1591
+ const options = readOptions(node);
1592
+ if (options.length) field.options = options;
1593
+ }
1594
+ const def = attr(node, "value") ?? (comp === "boolean" ? attr(node, "checked") : void 0);
1595
+ if (def !== void 0 && def !== "") field.value = comp === "boolean" ? def === "true" : def;
1596
+ out.push(field);
1597
+ continue;
1598
+ }
1599
+ if (rt && attr(node, "name") && !isWrapper(rt)) unmapped.add(rt);
1600
+ walk(childrenOf(node), out, unmapped, counter);
1601
+ }
1602
+ }
1603
+ function parseDialog(xml) {
1604
+ const parser = new XMLParser({
1605
+ ignoreAttributes: false,
1606
+ attributeNamePrefix: "@_",
1607
+ preserveOrder: true,
1608
+ removeNSPrefix: false,
1609
+ trimValues: true
1610
+ });
1611
+ const tree = parser.parse(xml);
1612
+ const root = tree.find((n) => tagOf(n) === "jcr:root");
1613
+ const fields = [];
1614
+ const unmapped = /* @__PURE__ */ new Set();
1615
+ if (root) walk(childrenOf(root), fields, unmapped, { i: 1 });
1616
+ return { fields, unmapped: [...unmapped] };
1617
+ }
1618
+ var init_dialog = __esm({
1619
+ "src/lib/migrate/dialog.ts"() {
1620
+ "use strict";
1621
+ init_esm_shims();
1622
+ }
1623
+ });
1624
+
1625
+ // src/lib/migrate/component.ts
1626
+ import { existsSync as existsSync6 } from "fs";
1627
+ import { readFile as readFile4 } from "fs/promises";
1628
+ import path8 from "path";
1629
+ import { XMLParser as XMLParser2 } from "fast-xml-parser";
1630
+ function findDialogFile(componentDir) {
1631
+ const candidates = [
1632
+ path8.join(componentDir, "_cq_dialog", ".content.xml"),
1633
+ path8.join(componentDir, "_cq_dialog.xml"),
1634
+ path8.join(componentDir, "cq:dialog", ".content.xml")
1635
+ ];
1636
+ return candidates.find((c) => existsSync6(c)) ?? null;
1637
+ }
1638
+ async function readComponentTitle(componentDir) {
1639
+ const file = path8.join(componentDir, ".content.xml");
1640
+ if (!existsSync6(file)) return void 0;
1641
+ try {
1642
+ const parser = new XMLParser2({ ignoreAttributes: false, attributeNamePrefix: "@_" });
1643
+ const parsed = parser.parse(await readFile4(file, "utf-8"));
1644
+ const root = parsed["jcr:root"] ?? {};
1645
+ const title = root["@_jcr:title"];
1646
+ return typeof title === "string" ? title.replace(/^\{[^}]+\}/, "") : void 0;
1647
+ } catch {
1648
+ return void 0;
1649
+ }
1650
+ }
1651
+ async function readDialogFields(componentDir) {
1652
+ const dialog = findDialogFile(componentDir);
1653
+ if (!dialog) return null;
1654
+ return parseDialog(await readFile4(dialog, "utf-8"));
1655
+ }
1656
+ function migratedModelFile(blockName, title, fields) {
1657
+ const model = {
1658
+ definitions: [
1659
+ {
1660
+ title,
1661
+ id: blockName,
1662
+ plugins: {
1663
+ xwalk: {
1664
+ page: {
1665
+ resourceType: "core/franklin/components/block/v1/block",
1666
+ template: { name: title, model: blockName }
1667
+ }
1668
+ }
1669
+ }
1670
+ }
1671
+ ],
1672
+ models: [{ id: blockName, fields }],
1673
+ filters: []
1674
+ };
1675
+ return `${JSON.stringify(model, null, 2)}
1676
+ `;
1677
+ }
1678
+ function migratedBlockJs(blockName, fields) {
1679
+ const hasContainer = fields.some((f) => f.component === "container" && f.multi);
1680
+ const body = hasContainer ? ` // This block was migrated from a classic AEM component with a multifield.
1681
+ // Each child row is one repeated item - enhance them here.
1682
+ [...block.children].forEach((row) => {
1683
+ row.classList.add('${blockName}-item');
1684
+ });` : ` // Migrated from a classic AEM component. The authored fields arrive as
1685
+ // rows; read and enhance them here (port the old HTL logic as needed).`;
1686
+ return `// Block: ${blockName}
1687
+ // Migrated from a classic AEM component by \`eds migrate component\`.
1688
+ // TODO: port the component's HTL/JS behaviour into this decorator.
1689
+ export default function decorate(block) {
1690
+ block.classList.add('${blockName}');
1691
+ ${body}
1692
+ }
1693
+ `;
1694
+ }
1695
+ function migratedBlockCss(blockName) {
1696
+ return `/* Block: ${blockName} */
1697
+ /* Migrated from a classic AEM component. Bring the component's styles here */
1698
+ /* (from its clientlib or the rendered page) and keep them scoped to the block. */
1699
+ .${blockName} {
1700
+ }
1701
+ `;
1702
+ }
1703
+ var init_component = __esm({
1704
+ "src/lib/migrate/component.ts"() {
1705
+ "use strict";
1706
+ init_esm_shims();
1707
+ init_dialog();
1708
+ }
1709
+ });
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
+
1831
+ // src/commands/migrate.ts
1832
+ var migrate_exports = {};
1833
+ __export(migrate_exports, {
1834
+ migrateComponent: () => migrateComponent
1835
+ });
1836
+ import { existsSync as existsSync8 } from "fs";
1837
+ import { mkdir as mkdir3, writeFile as writeFile6 } from "fs/promises";
1838
+ import path10 from "path";
1839
+ function clientlibBlockCss(blockName, css, files) {
1840
+ return `/* Block: ${blockName} */
1841
+ /* Migrated from the component clientlib: ${files.join(", ")}. */
1842
+ /* TODO: review selectors and scope them under .${blockName} (and port any LESS/SCSS). */
1843
+
1844
+ ${css.trim()}
1845
+ `;
1846
+ }
1847
+ function previewFields(fields, depth = 0) {
1848
+ const lines = [];
1849
+ for (const f of fields) {
1850
+ const indent = " ".repeat(depth);
1851
+ if (f.component === "tab") {
1852
+ lines.push(`${indent}${brand.blue("\u25B8")} ${f.label ?? f.name}`);
1853
+ } else {
1854
+ const opts = f.options?.length ? ` (${f.options.length} options)` : "";
1855
+ const multi = f.multi ? " \xB7multi" : "";
1856
+ const label = f.label ? `- ${f.label}` : "";
1857
+ lines.push(`${indent}${brand.accent(f.component)}${multi} ${f.name}${opts} ${label}`);
1858
+ if (f.fields?.length) lines.push(...previewFields(f.fields, depth + 1));
1859
+ }
1860
+ }
1861
+ return lines;
1862
+ }
1863
+ function countLeaves(fields) {
1864
+ let n = 0;
1865
+ for (const f of fields) {
1866
+ if (f.component === "tab") continue;
1867
+ n += 1;
1868
+ if (f.fields) n += countLeaves(f.fields);
1869
+ }
1870
+ return n;
1871
+ }
1872
+ async function migrateComponent(dirArg, options = {}) {
1873
+ const projectRoot = findProjectRoot();
1874
+ if (!projectRoot) {
1875
+ logger.error("Not inside an EDS project (no EDS project markers found).");
1876
+ process.exitCode = 1;
1877
+ return;
1878
+ }
1879
+ await logger.logoOnceAnimated("Migrate component");
1880
+ const { input, confirm } = await import("@inquirer/prompts");
1881
+ const interactive = !options.yes;
1882
+ let componentDir = dirArg;
1883
+ if (!componentDir && interactive) {
1884
+ componentDir = await input({
1885
+ message: "Path to the classic component folder",
1886
+ validate: (v) => existsSync8(v.trim()) || "Folder not found"
1887
+ });
1888
+ }
1889
+ if (!componentDir) {
1890
+ logger.error("Provide the path to the classic component folder.");
1891
+ process.exitCode = 1;
1892
+ return;
1893
+ }
1894
+ componentDir = path10.resolve(componentDir.trim());
1895
+ if (!findDialogFile(componentDir)) {
1896
+ logger.error(`No Touch UI dialog (_cq_dialog/.content.xml) found in ${componentDir}.`);
1897
+ process.exitCode = 1;
1898
+ return;
1899
+ }
1900
+ const parsed = await readDialogFields(componentDir);
1901
+ if (!parsed || parsed.fields.length === 0) {
1902
+ logger.warn("Dialog found but no mappable fields were detected.");
1903
+ if (parsed?.unmapped.length)
1904
+ logger.info(`Unmapped resourceTypes: ${parsed.unmapped.join(", ")}`);
1905
+ process.exitCode = 1;
1906
+ return;
1907
+ }
1908
+ const title = await readComponentTitle(componentDir) ?? path10.basename(componentDir);
1909
+ const blockName = options.name && slug2(options.name) || (interactive ? slug2(await input({ message: "Block name", default: slug2(title) })) : slug2(title));
1910
+ logger.info(heading("Mapped fields", `${countLeaves(parsed.fields)} from dialog`));
1911
+ for (const line of previewFields(parsed.fields)) logger.info(` ${line}`);
1912
+ if (parsed.unmapped.length) {
1913
+ logger.info("");
1914
+ logger.warn(`Skipped (unrecognised): ${parsed.unmapped.join(", ")}`);
1915
+ }
1916
+ logger.info("");
1917
+ if (interactive) {
1918
+ const go = await confirm({ message: `Generate block "${blockName}"?`, default: true });
1919
+ if (!go) {
1920
+ logger.info("Cancelled.");
1921
+ return;
1922
+ }
1923
+ }
1924
+ const blockDir = path10.join(projectRoot, "blocks", blockName);
1925
+ if (existsSync8(blockDir)) {
1926
+ logger.error(
1927
+ `Block "${blockName}" already exists at blocks/${blockName}. Use --name to pick another.`
1928
+ );
1929
+ process.exitCode = 1;
1930
+ return;
1931
+ }
1932
+ const clibDir = options.clientlib ? path10.resolve(options.clientlib.trim()) : await findClientlibDir(componentDir);
1933
+ let cssContent = migratedBlockCss(blockName);
1934
+ let cssMessage = "empty - add styles from clientlib / rendered CSS";
1935
+ let preprocessedNote = [];
1936
+ if (clibDir && existsSync8(clibDir)) {
1937
+ const lib = await readClientlibCss(clibDir);
1938
+ if (lib.css.trim()) {
1939
+ cssContent = clientlibBlockCss(blockName, lib.css, lib.files);
1940
+ cssMessage = `from clientlib (${lib.files.length} file(s)) - review & scope`;
1941
+ }
1942
+ preprocessedNote = lib.preprocessed;
1943
+ }
1944
+ await mkdir3(blockDir, { recursive: true });
1945
+ await writeFile6(
1946
+ path10.join(blockDir, `${blockName}.js`),
1947
+ migratedBlockJs(blockName, parsed.fields)
1948
+ );
1949
+ await writeFile6(path10.join(blockDir, `${blockName}.css`), cssContent);
1950
+ await writeFile6(
1951
+ path10.join(blockDir, `_${blockName}.json`),
1952
+ migratedModelFile(blockName, title, parsed.fields)
1953
+ );
1954
+ logger.info(heading("Created"));
1955
+ const files = [`${blockName}.js`, `${blockName}.css`, `_${blockName}.json`];
1956
+ const w = columnWidth(files);
1957
+ logger.info(accentLine(`${blockName}.js`, "decorator stub (port HTL logic)", w));
1958
+ logger.info(accentLine(`${blockName}.css`, cssMessage, w));
1959
+ logger.info(accentLine(`_${blockName}.json`, "Universal Editor model", w));
1960
+ if (preprocessedNote.length) {
1961
+ logger.info("");
1962
+ logger.warn(`LESS/SCSS found (needs a build, not copied): ${preprocessedNote.join(", ")}`);
1963
+ }
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
+ }
1975
+ logger.info(
1976
+ box([`"${blockName}" migrated. Next: add CSS, then \`eds block preview ${blockName}\``])
1977
+ );
1978
+ }
1979
+ var slug2;
1980
+ var init_migrate = __esm({
1981
+ "src/commands/migrate.ts"() {
1982
+ "use strict";
1983
+ init_esm_shims();
1984
+ init_logger();
1985
+ init_clientlib();
1986
+ init_component();
1987
+ init_project();
1988
+ init_ui();
1989
+ slug2 = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1990
+ }
1991
+ });
1992
+
1427
1993
  // src/commands/block/create.ts
1428
1994
  var create_exports = {};
1429
1995
  __export(create_exports, {
1430
1996
  createBlock: () => createBlock,
1431
1997
  ueModelFor: () => ueModelFor
1432
1998
  });
1433
- import { existsSync as existsSync5 } from "fs";
1434
- import { mkdir as mkdir3, writeFile as writeFile5 } from "fs/promises";
1435
- import path7 from "path";
1436
- 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";
1437
2003
  function titleCase(name) {
1438
2004
  return name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1439
2005
  }
@@ -1511,13 +2077,13 @@ async function createBlock(name, options = {}) {
1511
2077
  process.exitCode = 1;
1512
2078
  return;
1513
2079
  }
1514
- const blocksDir = path7.join(projectRoot, "blocks", name);
1515
- if (existsSync5(blocksDir)) {
2080
+ const blocksDir = path11.join(projectRoot, "blocks", name);
2081
+ if (existsSync9(blocksDir)) {
1516
2082
  logger.error(`Block "${name}" already exists at ${blocksDir}`);
1517
2083
  process.exitCode = 1;
1518
2084
  return;
1519
2085
  }
1520
- await mkdir3(blocksDir, { recursive: true });
2086
+ await mkdir4(blocksDir, { recursive: true });
1521
2087
  const jsContent = `export default function decorate(block) {
1522
2088
  // TODO: implement ${name} block
1523
2089
  }
@@ -1526,14 +2092,14 @@ async function createBlock(name, options = {}) {
1526
2092
  /* TODO: style ${name} block */
1527
2093
  }
1528
2094
  `;
1529
- await writeFile5(path7.join(blocksDir, `${name}.js`), jsContent);
1530
- await writeFile5(path7.join(blocksDir, `${name}.css`), cssContent);
2095
+ await writeFile7(path11.join(blocksDir, `${name}.js`), jsContent);
2096
+ await writeFile7(path11.join(blocksDir, `${name}.css`), cssContent);
1531
2097
  const withUe = options.ueModel !== false;
1532
2098
  if (withUe) {
1533
- await writeFile5(path7.join(blocksDir, `_${name}.json`), ueModelFor(name));
2099
+ await writeFile7(path11.join(blocksDir, `_${name}.json`), ueModelFor(name));
1534
2100
  }
1535
2101
  logger.success(
1536
- `Block "${name}" created at ${chalk4.cyan(path7.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)") : ""}`
1537
2103
  );
1538
2104
  }
1539
2105
  var init_create = __esm({
@@ -1547,10 +2113,10 @@ var init_create = __esm({
1547
2113
  });
1548
2114
 
1549
2115
  // src/lib/agents/detect.ts
1550
- import { existsSync as existsSync6 } from "fs";
1551
- import { readFile as readFile3 } from "fs/promises";
2116
+ import { existsSync as existsSync10 } from "fs";
2117
+ import { readFile as readFile6 } from "fs/promises";
1552
2118
  import { homedir } from "os";
1553
- import path8 from "path";
2119
+ import path12 from "path";
1554
2120
  async function whichBinary(name) {
1555
2121
  const { execaCommand } = await import("execa");
1556
2122
  try {
@@ -1578,8 +2144,8 @@ function findFigmaServerKey(node) {
1578
2144
  async function findFigmaMcpServer(configPaths) {
1579
2145
  for (const configPath of configPaths) {
1580
2146
  try {
1581
- if (!existsSync6(configPath)) continue;
1582
- const content = await readFile3(configPath, "utf-8");
2147
+ if (!existsSync10(configPath)) continue;
2148
+ const content = await readFile6(configPath, "utf-8");
1583
2149
  try {
1584
2150
  const key = findFigmaServerKey(JSON.parse(content));
1585
2151
  if (key) return key;
@@ -1601,7 +2167,7 @@ async function detectAgent(preferred) {
1601
2167
  return {
1602
2168
  type: def.type,
1603
2169
  path: binPath,
1604
- configDir: existsSync6(def.configDir) ? def.configDir : void 0,
2170
+ configDir: existsSync10(def.configDir) ? def.configDir : void 0,
1605
2171
  hasFigmaMcp: !!figmaMcpServerName,
1606
2172
  figmaMcpServerName
1607
2173
  };
@@ -1616,7 +2182,7 @@ async function detectAgent(preferred) {
1616
2182
  return {
1617
2183
  type: def.type,
1618
2184
  path: binPath,
1619
- configDir: existsSync6(def.configDir) ? def.configDir : void 0,
2185
+ configDir: existsSync10(def.configDir) ? def.configDir : void 0,
1620
2186
  hasFigmaMcp: !!figmaMcpServerName,
1621
2187
  figmaMcpServerName
1622
2188
  };
@@ -1633,7 +2199,7 @@ async function detectAllAgents() {
1633
2199
  agents.push({
1634
2200
  type: def.type,
1635
2201
  path: binPath,
1636
- configDir: existsSync6(def.configDir) ? def.configDir : void 0,
2202
+ configDir: existsSync10(def.configDir) ? def.configDir : void 0,
1637
2203
  hasFigmaMcp: !!figmaMcpServerName,
1638
2204
  figmaMcpServerName
1639
2205
  });
@@ -1651,22 +2217,22 @@ var init_detect = __esm({
1651
2217
  {
1652
2218
  type: "claude",
1653
2219
  binary: "claude",
1654
- configDir: path8.join(homedir(), ".claude"),
2220
+ configDir: path12.join(homedir(), ".claude"),
1655
2221
  mcpConfigPaths: [
1656
- path8.join(homedir(), ".claude", "plugins.json"),
1657
- path8.join(homedir(), ".claude.json")
2222
+ path12.join(homedir(), ".claude", "plugins.json"),
2223
+ path12.join(homedir(), ".claude.json")
1658
2224
  ]
1659
2225
  },
1660
2226
  {
1661
2227
  type: "cursor",
1662
2228
  binary: "cursor",
1663
- configDir: path8.join(homedir(), ".cursor"),
1664
- mcpConfigPaths: [path8.join(homedir(), ".cursor", "mcp.json")]
2229
+ configDir: path12.join(homedir(), ".cursor"),
2230
+ mcpConfigPaths: [path12.join(homedir(), ".cursor", "mcp.json")]
1665
2231
  },
1666
2232
  {
1667
2233
  type: "codex",
1668
2234
  binary: "codex",
1669
- configDir: path8.join(homedir(), ".codex"),
2235
+ configDir: path12.join(homedir(), ".codex"),
1670
2236
  mcpConfigPaths: []
1671
2237
  }
1672
2238
  ];
@@ -1674,27 +2240,27 @@ var init_detect = __esm({
1674
2240
  });
1675
2241
 
1676
2242
  // src/lib/agents/invoke.ts
1677
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile6 } from "fs/promises";
2243
+ import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
1678
2244
  import { tmpdir } from "os";
1679
- import path9 from "path";
2245
+ import path13 from "path";
1680
2246
  import readline from "readline";
1681
- import chalk5 from "chalk";
2247
+ import chalk6 from "chalk";
1682
2248
  async function savePromptToFile(prompt, projectRoot) {
1683
- const base = projectRoot ? path9.join(projectRoot, "tmp") : tmpdir();
1684
- const dir = path9.join(base, "eds-cli");
1685
- await mkdir4(dir, { recursive: true });
2249
+ const base = projectRoot ? path13.join(projectRoot, "tmp") : tmpdir();
2250
+ const dir = path13.join(base, "eds-cli");
2251
+ await mkdir5(dir, { recursive: true });
1686
2252
  const filename = `from-design-${Date.now()}.md`;
1687
- const filePath = path9.join(dir, filename);
1688
- await writeFile6(filePath, prompt, "utf-8");
2253
+ const filePath = path13.join(dir, filename);
2254
+ await writeFile8(filePath, prompt, "utf-8");
1689
2255
  return filePath;
1690
2256
  }
1691
2257
  async function invokeAgent(agent, promptFile, cwd) {
1692
2258
  const { execa } = await import("execa");
1693
- const prompt = await readFile4(promptFile, "utf-8");
2259
+ const prompt = await readFile7(promptFile, "utf-8");
1694
2260
  try {
1695
2261
  switch (agent.type) {
1696
2262
  case "claude": {
1697
- logger.info(`Running Claude Code ${chalk5.dim("- live log:")}`);
2263
+ logger.info(`Running Claude Code ${chalk6.dim("- live log:")}`);
1698
2264
  const figmaServers = [
1699
2265
  ...agent.figmaMcpServerName ? [agent.figmaMcpServerName] : [],
1700
2266
  "figma-sse",
@@ -1757,10 +2323,10 @@ async function renderClaudeStream(sub, cwd) {
1757
2323
  const ora4 = (await import("ora")).default;
1758
2324
  const start = Date.now();
1759
2325
  const elapsed = () => `${Math.round((Date.now() - start) / 1e3)}s`;
1760
- const spinner = ora4({ text: `thinking ${chalk5.dim(elapsed())}`, color: "yellow" }).start();
2326
+ const spinner = ora4({ text: `thinking ${chalk6.dim(elapsed())}`, color: "yellow" }).start();
1761
2327
  let activity = "thinking";
1762
2328
  const timer = setInterval(() => {
1763
- spinner.text = `${activity} ${chalk5.dim(elapsed())}`;
2329
+ spinner.text = `${activity} ${chalk6.dim(elapsed())}`;
1764
2330
  }, 300);
1765
2331
  const pending = /* @__PURE__ */ new Map();
1766
2332
  const emit = (line) => {
@@ -1786,7 +2352,7 @@ async function renderClaudeStream(sub, cwd) {
1786
2352
  for (const block2 of content) {
1787
2353
  if (block2.type === "text" && "text" in block2) {
1788
2354
  const first = block2.text.trim().split("\n")[0].slice(0, 100);
1789
- if (first) emit(chalk5.dim(first));
2355
+ if (first) emit(chalk6.dim(first));
1790
2356
  } else if (block2.type === "tool_use" && "name" in block2) {
1791
2357
  const id = "id" in block2 && block2.id ? block2.id : `${Math.random()}`;
1792
2358
  pending.set(id, {
@@ -1802,8 +2368,8 @@ async function renderClaudeStream(sub, cwd) {
1802
2368
  const p = pending.get(block2.tool_use_id);
1803
2369
  if (p) {
1804
2370
  const dur = `${((Date.now() - p.start) / 1e3).toFixed(1)}s`;
1805
- const failed = "is_error" in block2 && block2.is_error ? ` ${chalk5.red("failed")}` : "";
1806
- 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}`);
1807
2373
  pending.delete(block2.tool_use_id);
1808
2374
  }
1809
2375
  }
@@ -1815,9 +2381,9 @@ async function renderClaudeStream(sub, cwd) {
1815
2381
  }
1816
2382
  }
1817
2383
  }
1818
- 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")}`);
1819
2385
  clearInterval(timer);
1820
- if (result) spinner.succeed(`${brand.accent("agent finished")} ${chalk5.dim(result)}`);
2386
+ if (result) spinner.succeed(`${brand.accent("agent finished")} ${chalk6.dim(result)}`);
1821
2387
  else spinner.stop();
1822
2388
  } catch (err) {
1823
2389
  clearInterval(timer);
@@ -1830,22 +2396,22 @@ function toolLabel(name, input, cwd) {
1830
2396
  const rel = (key) => {
1831
2397
  const p = input?.[key];
1832
2398
  if (typeof p !== "string") return "";
1833
- return p.startsWith(cwd) ? path9.relative(cwd, p) : path9.basename(p);
2399
+ return p.startsWith(cwd) ? path13.relative(cwd, p) : path13.basename(p);
1834
2400
  };
1835
2401
  if (name.includes("figma")) {
1836
2402
  const short = name.split("__").pop() ?? name;
1837
- return `${icon.figma} ${chalk5.bold(short)} ${chalk5.dim("Figma MCP")}`;
2403
+ return `${icon.figma} ${chalk6.bold(short)} ${chalk6.dim("Figma MCP")}`;
1838
2404
  }
1839
2405
  switch (name) {
1840
2406
  case "Write":
1841
2407
  case "Edit":
1842
2408
  case "NotebookEdit":
1843
- return `${brand.accent("\u270E")} ${chalk5.cyan(rel("file_path"))}`;
2409
+ return `${brand.accent("\u270E")} ${chalk6.cyan(rel("file_path"))}`;
1844
2410
  case "Read":
1845
- return chalk5.dim(`\xB7 read ${rel("file_path")}`);
2411
+ return chalk6.dim(`\xB7 read ${rel("file_path")}`);
1846
2412
  case "Bash": {
1847
2413
  const cmd = typeof input?.command === "string" ? input.command.split("\n")[0].slice(0, 70) : "";
1848
- return chalk5.dim(`$ ${cmd}`);
2414
+ return chalk6.dim(`$ ${cmd}`);
1849
2415
  }
1850
2416
  default:
1851
2417
  return `${brand.blue("\u2192")} ${name}`;
@@ -1861,13 +2427,13 @@ var init_invoke = __esm({
1861
2427
  });
1862
2428
 
1863
2429
  // src/lib/config.ts
1864
- import { existsSync as existsSync7 } from "fs";
1865
- import { readFile as readFile5 } from "fs/promises";
2430
+ import { existsSync as existsSync11 } from "fs";
2431
+ import { readFile as readFile8 } from "fs/promises";
1866
2432
  import { homedir as homedir2 } from "os";
1867
- import path10 from "path";
2433
+ import path14 from "path";
1868
2434
  async function loadJsonFile(filePath) {
1869
2435
  try {
1870
- const content = await readFile5(filePath, "utf-8");
2436
+ const content = await readFile8(filePath, "utf-8");
1871
2437
  return JSON.parse(content);
1872
2438
  } catch {
1873
2439
  return void 0;
@@ -1875,7 +2441,7 @@ async function loadJsonFile(filePath) {
1875
2441
  }
1876
2442
  async function loadConfig(projectRoot) {
1877
2443
  const globalRaw = await loadJsonFile(GLOBAL_CONFIG_PATH);
1878
- const projectRaw = projectRoot ? await loadJsonFile(path10.join(projectRoot, PROJECT_CONFIG_NAME)) : void 0;
2444
+ const projectRaw = projectRoot ? await loadJsonFile(path14.join(projectRoot, PROJECT_CONFIG_NAME)) : void 0;
1879
2445
  const merged = {
1880
2446
  ...typeof globalRaw === "object" && globalRaw !== null ? globalRaw : {},
1881
2447
  ...typeof projectRaw === "object" && projectRaw !== null ? projectRaw : {}
@@ -1894,7 +2460,7 @@ function globalConfigPath() {
1894
2460
  return GLOBAL_CONFIG_PATH;
1895
2461
  }
1896
2462
  function globalConfigExists() {
1897
- return existsSync7(GLOBAL_CONFIG_PATH);
2463
+ return existsSync11(GLOBAL_CONFIG_PATH);
1898
2464
  }
1899
2465
  var GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_PATH, PROJECT_CONFIG_NAME;
1900
2466
  var init_config = __esm({
@@ -1903,8 +2469,8 @@ var init_config = __esm({
1903
2469
  init_esm_shims();
1904
2470
  init_logger();
1905
2471
  init_schemas();
1906
- GLOBAL_CONFIG_DIR = path10.join(homedir2(), ".eds");
1907
- GLOBAL_CONFIG_PATH = path10.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");
1908
2474
  PROJECT_CONFIG_NAME = ".edsrc.json";
1909
2475
  }
1910
2476
  });
@@ -1954,9 +2520,9 @@ var init_node_id = __esm({
1954
2520
  });
1955
2521
 
1956
2522
  // src/lib/figma/prompt-builder.ts
1957
- import { existsSync as existsSync8 } from "fs";
1958
- import { readFile as readFile6 } from "fs/promises";
1959
- import path11 from "path";
2523
+ import { existsSync as existsSync12 } from "fs";
2524
+ import { readFile as readFile9 } from "fs/promises";
2525
+ import path15 from "path";
1960
2526
  function contentSourceGuidance(source, cfHint, blockName) {
1961
2527
  if (!source || source === "document" || source === "ue") return "";
1962
2528
  const q = cfHint ? ` (model / persisted query: \`${cfHint}\`)` : "";
@@ -2168,10 +2734,10 @@ ${tokensHint}
2168
2734
  `;
2169
2735
  }
2170
2736
  async function getTokensHint(projectRoot) {
2171
- const stylesPath = path11.join(projectRoot, "styles", "styles.css");
2172
- if (!existsSync8(stylesPath)) return "";
2737
+ const stylesPath = path15.join(projectRoot, "styles", "styles.css");
2738
+ if (!existsSync12(stylesPath)) return "";
2173
2739
  try {
2174
- const css = await readFile6(stylesPath, "utf-8");
2740
+ const css = await readFile9(stylesPath, "utf-8");
2175
2741
  const rootMatch = css.match(/:root\s*\{([^}]+)\}/);
2176
2742
  if (!rootMatch) return "";
2177
2743
  const props = rootMatch[1].split("\n").filter((line) => line.trim().startsWith("--")).map((line) => line.trim()).join("\n ");
@@ -2201,7 +2767,8 @@ var init_prompt_builder = __esm({
2201
2767
  figma: "Figma",
2202
2768
  stitch: "Google Stitch",
2203
2769
  canva: "Canva",
2204
- sketch: "Sketch"
2770
+ sketch: "Sketch",
2771
+ framer: "Framer"
2205
2772
  };
2206
2773
  }
2207
2774
  });
@@ -2311,11 +2878,11 @@ var init_harness = __esm({
2311
2878
  });
2312
2879
 
2313
2880
  // src/lib/preview/server.ts
2314
- import { createReadStream, existsSync as existsSync9, statSync } from "fs";
2881
+ import { createReadStream, existsSync as existsSync13, statSync } from "fs";
2315
2882
  import http from "http";
2316
- import path12 from "path";
2883
+ import path16 from "path";
2317
2884
  function startServer(root, routes, port, host = "127.0.0.1") {
2318
- const resolvedRoot = path12.resolve(root);
2885
+ const resolvedRoot = path16.resolve(root);
2319
2886
  const clients = /* @__PURE__ */ new Set();
2320
2887
  const server = http.createServer((req, res) => {
2321
2888
  const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
@@ -2341,19 +2908,19 @@ function startServer(root, routes, port, host = "127.0.0.1") {
2341
2908
  res.end(route.body);
2342
2909
  return;
2343
2910
  }
2344
- const filePath = path12.join(resolvedRoot, urlPath);
2911
+ const filePath = path16.join(resolvedRoot, urlPath);
2345
2912
  if (!filePath.startsWith(resolvedRoot)) {
2346
2913
  res.writeHead(403);
2347
2914
  res.end("Forbidden");
2348
2915
  return;
2349
2916
  }
2350
- if (!existsSync9(filePath) || !statSync(filePath).isFile()) {
2917
+ if (!existsSync13(filePath) || !statSync(filePath).isFile()) {
2351
2918
  res.writeHead(404, { "content-type": "text/plain" });
2352
2919
  res.end(`Not found: ${urlPath}`);
2353
2920
  return;
2354
2921
  }
2355
2922
  res.writeHead(200, {
2356
- "content-type": MIME[path12.extname(filePath).toLowerCase()] ?? "application/octet-stream"
2923
+ "content-type": MIME[path16.extname(filePath).toLowerCase()] ?? "application/octet-stream"
2357
2924
  });
2358
2925
  createReadStream(filePath).pipe(res);
2359
2926
  });
@@ -2417,10 +2984,10 @@ var preview_exports = {};
2417
2984
  __export(preview_exports, {
2418
2985
  previewBlock: () => previewBlock
2419
2986
  });
2420
- import { existsSync as existsSync10, watch } from "fs";
2421
- import { readFile as readFile7 } from "fs/promises";
2422
- import path13 from "path";
2423
- 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";
2424
2991
  function labelFor(width) {
2425
2992
  if (width <= 480) return `${width} mobile`;
2426
2993
  if (width <= 1024) return `${width} tablet`;
@@ -2472,30 +3039,30 @@ async function previewBlock(name, options = {}) {
2472
3039
  process.exitCode = 1;
2473
3040
  return;
2474
3041
  }
2475
- const blockDir = path13.join(projectRoot, "blocks", name);
2476
- if (!existsSync10(blockDir)) {
3042
+ const blockDir = path17.join(projectRoot, "blocks", name);
3043
+ if (!existsSync14(blockDir)) {
2477
3044
  logger.error(`Block "${name}" not found at blocks/${name}/.`);
2478
3045
  process.exitCode = 1;
2479
3046
  return;
2480
3047
  }
2481
- if (!existsSync10(path13.join(blockDir, `${name}.css`)) && !existsSync10(path13.join(blockDir, `${name}.js`))) {
3048
+ if (!existsSync14(path17.join(blockDir, `${name}.css`)) && !existsSync14(path17.join(blockDir, `${name}.js`))) {
2482
3049
  logger.error(`Block "${name}" has no ${name}.css / ${name}.js to preview.`);
2483
3050
  process.exitCode = 1;
2484
3051
  return;
2485
3052
  }
2486
3053
  let meta = null;
2487
- const metaPath = path13.join(blockDir, ".eds-meta.json");
2488
- if (existsSync10(metaPath)) {
3054
+ const metaPath = path17.join(blockDir, ".eds-meta.json");
3055
+ if (existsSync14(metaPath)) {
2489
3056
  try {
2490
- meta = JSON.parse(await readFile7(metaPath, "utf-8"));
3057
+ meta = JSON.parse(await readFile10(metaPath, "utf-8"));
2491
3058
  } catch {
2492
3059
  }
2493
3060
  }
2494
- const samplePath = path13.join(blockDir, `_${name}.preview.html`);
3061
+ const samplePath = path17.join(blockDir, `_${name}.preview.html`);
2495
3062
  let sampleContent;
2496
3063
  let placeholder = false;
2497
- if (existsSync10(samplePath)) {
2498
- sampleContent = await readFile7(samplePath, "utf-8");
3064
+ if (existsSync14(samplePath)) {
3065
+ sampleContent = await readFile10(samplePath, "utf-8");
2499
3066
  } else {
2500
3067
  sampleContent = defaultSample(name);
2501
3068
  placeholder = true;
@@ -2545,9 +3112,9 @@ async function previewBlock(name, options = {}) {
2545
3112
  reloadTimer = setTimeout(() => server.reload(), 150);
2546
3113
  });
2547
3114
  logger.info("");
2548
- logger.info(` ${chalk6.bold("\u279C")} ${chalk6.cyan(server.url)}`);
3115
+ logger.info(` ${chalk7.bold("\u279C")} ${chalk7.cyan(server.url)}`);
2549
3116
  logger.info(statusLine("pass", "live reload", "edits to the block refresh the browser"));
2550
- logger.info(chalk6.dim(" Press Ctrl+C to stop.\n"));
3117
+ logger.info(chalk7.dim(" Press Ctrl+C to stop.\n"));
2551
3118
  if (options.open !== false) await openInBrowser(server.url);
2552
3119
  const stop = async () => {
2553
3120
  watcher.close();
@@ -2576,10 +3143,10 @@ var from_design_exports = {};
2576
3143
  __export(from_design_exports, {
2577
3144
  blockFromDesign: () => blockFromDesign
2578
3145
  });
2579
- import { existsSync as existsSync11 } from "fs";
2580
- import { readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
2581
- import path14 from "path";
2582
- 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";
2583
3150
  import ora from "ora";
2584
3151
  async function previewRunning(port = 8777) {
2585
3152
  try {
@@ -2602,7 +3169,8 @@ async function runWizard(figmaUrl) {
2602
3169
  { name: "Figma", value: "figma" },
2603
3170
  { name: "Google Stitch", value: "stitch" },
2604
3171
  { name: "Canva", value: "canva" },
2605
- { name: "Sketch", value: "sketch" }
3172
+ { name: "Sketch", value: "sketch" },
3173
+ { name: "Framer", value: "framer" }
2606
3174
  ],
2607
3175
  default: "figma"
2608
3176
  });
@@ -2728,7 +3296,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
2728
3296
  try {
2729
3297
  figma2 = parseFigmaUrl(designUrl);
2730
3298
  spinner.succeed(
2731
- `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}`)}`
2732
3300
  );
2733
3301
  } catch (err) {
2734
3302
  spinner.fail(err.message);
@@ -2736,7 +3304,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
2736
3304
  return;
2737
3305
  }
2738
3306
  } else {
2739
- logger.info(` ${icon.pass} ${providerName} design ${chalk7.dim(designRef)}`);
3307
+ logger.info(` ${icon.pass} ${providerName} design ${chalk8.dim(designRef)}`);
2740
3308
  }
2741
3309
  const blockName = options.name || (figma2 ? inferBlockName(figma2.fileName) : "");
2742
3310
  if (!kebabCaseRegex.test(blockName)) {
@@ -2759,20 +3327,20 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
2759
3327
  screenshot: options.screenshot !== false
2760
3328
  });
2761
3329
  const promptFile = await savePromptToFile(prompt, projectRoot);
2762
- const hasTokens = existsSync11(path14.join(projectRoot, "styles", "styles.css"));
3330
+ const hasTokens = existsSync15(path18.join(projectRoot, "styles", "styles.css"));
2763
3331
  promptSpinner.succeed(
2764
- 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")}`
2765
3333
  );
2766
- logger.info(chalk7.dim(` prompt \u2192 ${promptFile}`));
3334
+ logger.info(chalk8.dim(` prompt \u2192 ${promptFile}`));
2767
3335
  if (options.dryRun) {
2768
3336
  logger.info("");
2769
- logger.info(chalk7.yellow("Dry run mode \u2014 prompt generated but agent not invoked."));
2770
- logger.info(`Prompt file: ${chalk7.cyan(promptFile)}`);
2771
- 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)}`);
2772
3340
  logger.info(`Prompt version: ${PROMPT_VERSION}`);
2773
3341
  logger.info("");
2774
3342
  logger.info("Preview:");
2775
- logger.info(chalk7.dim("\u2500".repeat(60)));
3343
+ logger.info(chalk8.dim("\u2500".repeat(60)));
2776
3344
  console.log(prompt);
2777
3345
  return;
2778
3346
  }
@@ -2780,22 +3348,22 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
2780
3348
  const preferredAgent = options.agent || config.agent;
2781
3349
  if (options.agent === "none") {
2782
3350
  logger.info("Agent skipped (--agent none). Use the prompt file manually:");
2783
- logger.info(` ${chalk7.cyan(promptFile)}`);
3351
+ logger.info(` ${chalk8.cyan(promptFile)}`);
2784
3352
  return;
2785
3353
  }
2786
3354
  const agentSpinner = ora("Detecting AI agent...").start();
2787
3355
  const agent = await detectAgent(preferredAgent);
2788
3356
  if (!agent) {
2789
3357
  agentSpinner.fail("No AI agent found (claude, cursor, or codex).");
2790
- logger.info(`Install Claude Code: ${chalk7.cyan("npm install -g @anthropic-ai/claude-code")}`);
2791
- 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)}`);
2792
3360
  process.exitCode = 1;
2793
3361
  return;
2794
3362
  }
2795
- 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)}`);
2796
3364
  if (provider === "figma" && !agent.hasFigmaMcp) {
2797
3365
  logger.warn(`Figma MCP may not be configured for ${agent.type}.`);
2798
- logger.info(`Run ${chalk7.cyan("eds figma setup")} to configure it.`);
3366
+ logger.info(`Run ${chalk8.cyan("eds figma setup")} to configure it.`);
2799
3367
  }
2800
3368
  if (!options.yes) {
2801
3369
  const { confirm } = await import("@inquirer/prompts");
@@ -2804,24 +3372,24 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
2804
3372
  default: true
2805
3373
  });
2806
3374
  if (!proceed) {
2807
- logger.info(`Prompt saved to: ${chalk7.cyan(promptFile)}`);
3375
+ logger.info(`Prompt saved to: ${chalk8.cyan(promptFile)}`);
2808
3376
  return;
2809
3377
  }
2810
3378
  }
2811
3379
  logger.info(
2812
- ` ${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`)}`
2813
3381
  );
2814
3382
  if (provider === "figma") {
2815
- 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"));
2816
3384
  }
2817
3385
  const success = await invokeAgent(agent, promptFile, projectRoot);
2818
3386
  if (!success) {
2819
3387
  process.exitCode = 1;
2820
3388
  return;
2821
3389
  }
2822
- const blockDir = path14.join(projectRoot, "blocks", blockName);
3390
+ const blockDir = path18.join(projectRoot, "blocks", blockName);
2823
3391
  const ok = await verifyGeneratedBlock(blockDir, blockName, options.ueModel !== false);
2824
- if (existsSync11(blockDir)) {
3392
+ if (existsSync15(blockDir)) {
2825
3393
  const meta = {
2826
3394
  provider,
2827
3395
  designRef,
@@ -2831,7 +3399,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
2831
3399
  promptVersion: PROMPT_VERSION,
2832
3400
  agentUsed: agent.type
2833
3401
  };
2834
- await writeFile7(path14.join(blockDir, ".eds-meta.json"), `${JSON.stringify(meta, null, 2)}
3402
+ await writeFile9(path18.join(blockDir, ".eds-meta.json"), `${JSON.stringify(meta, null, 2)}
2835
3403
  `);
2836
3404
  }
2837
3405
  if (!ok) {
@@ -2842,10 +3410,21 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
2842
3410
  return;
2843
3411
  }
2844
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
+ }
2845
3424
  if (options.serve !== false) {
2846
3425
  if (await previewRunning()) {
2847
3426
  logger.info(
2848
- chalk7.dim(
3427
+ chalk8.dim(
2849
3428
  " A preview server is already running at http://127.0.0.1:8777 \u2014 your change will refresh there."
2850
3429
  )
2851
3430
  );
@@ -2855,15 +3434,15 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
2855
3434
  await previewBlock2(blockName, {});
2856
3435
  }
2857
3436
  } else {
2858
- 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}`)}`);
2859
3438
  }
2860
3439
  }
2861
3440
  async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
2862
3441
  const checks = [];
2863
- const jsPath = path14.join(blockDir, `${blockName}.js`);
2864
- const cssPath = path14.join(blockDir, `${blockName}.css`);
2865
- const hasJs = existsSync11(jsPath);
2866
- const hasCss = existsSync11(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);
2867
3446
  checks.push({
2868
3447
  status: hasJs ? "pass" : "fail",
2869
3448
  name: `${blockName}.js`,
@@ -2875,7 +3454,7 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
2875
3454
  message: hasCss ? "created" : "missing \u2014 agent did not write the CSS file"
2876
3455
  });
2877
3456
  if (hasJs) {
2878
- const js = await readFile8(jsPath, "utf-8");
3457
+ const js = await readFile11(jsPath, "utf-8");
2879
3458
  checks.push({
2880
3459
  status: /export\s+default/.test(js) ? "pass" : "fail",
2881
3460
  name: "decorate export",
@@ -2890,7 +3469,7 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
2890
3469
  }
2891
3470
  }
2892
3471
  if (hasCss) {
2893
- const css = await readFile8(cssPath, "utf-8");
3472
+ const css = await readFile11(cssPath, "utf-8");
2894
3473
  checks.push({
2895
3474
  status: css.includes(`.${blockName}`) ? "pass" : "warn",
2896
3475
  name: "scoped CSS",
@@ -2898,12 +3477,12 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
2898
3477
  });
2899
3478
  }
2900
3479
  if (withUeModel) {
2901
- const hasPerBlock = existsSync11(path14.join(blockDir, `_${blockName}.json`));
2902
- const modelsFile = path14.join(blockDir, "..", "..", "component-models.json");
3480
+ const hasPerBlock = existsSync15(path18.join(blockDir, `_${blockName}.json`));
3481
+ const modelsFile = path18.join(blockDir, "..", "..", "component-models.json");
2903
3482
  let hasAggregated = false;
2904
- if (!hasPerBlock && existsSync11(modelsFile)) {
3483
+ if (!hasPerBlock && existsSync15(modelsFile)) {
2905
3484
  try {
2906
- hasAggregated = (await readFile8(modelsFile, "utf-8")).includes(`"${blockName}"`);
3485
+ hasAggregated = (await readFile11(modelsFile, "utf-8")).includes(`"${blockName}"`);
2907
3486
  } catch {
2908
3487
  }
2909
3488
  }
@@ -2915,8 +3494,8 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
2915
3494
  });
2916
3495
  if (hasPerBlock && hasJs) {
2917
3496
  try {
2918
- const modelRaw = await readFile8(path14.join(blockDir, `_${blockName}.json`), "utf-8");
2919
- const js = await readFile8(jsPath, "utf-8");
3497
+ const modelRaw = await readFile11(path18.join(blockDir, `_${blockName}.json`), "utf-8");
3498
+ const js = await readFile11(jsPath, "utf-8");
2920
3499
  const isRepeatable = /"resourceType"\s*:\s*"[^"]*\/item"/.test(modelRaw) || /"filter"\s*:/.test(modelRaw) || /"multi"\s*:\s*true/.test(modelRaw);
2921
3500
  const iterates = /\.children/.test(js) || /\bfor\s*\(/.test(js) || /\.forEach\s*\(/.test(js);
2922
3501
  if (isRepeatable) {
@@ -2959,10 +3538,10 @@ var list_exports = {};
2959
3538
  __export(list_exports, {
2960
3539
  listBlocks: () => listBlocks
2961
3540
  });
2962
- import { existsSync as existsSync12 } from "fs";
2963
- import { readFile as readFile9, readdir } from "fs/promises";
2964
- import path15 from "path";
2965
- 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";
2966
3545
  async function listBlocks(options = {}) {
2967
3546
  const projectRoot = findProjectRoot();
2968
3547
  if (!projectRoot) {
@@ -2970,27 +3549,27 @@ async function listBlocks(options = {}) {
2970
3549
  process.exitCode = 1;
2971
3550
  return;
2972
3551
  }
2973
- const blocksDir = path15.join(projectRoot, "blocks");
2974
- if (!existsSync12(blocksDir)) {
3552
+ const blocksDir = path19.join(projectRoot, "blocks");
3553
+ if (!existsSync16(blocksDir)) {
2975
3554
  logger.info("No blocks/ directory found.");
2976
3555
  return;
2977
3556
  }
2978
- const entries = await readdir(blocksDir, { withFileTypes: true });
3557
+ const entries = await readdir2(blocksDir, { withFileTypes: true });
2979
3558
  const blocks = [];
2980
3559
  for (const entry of entries) {
2981
3560
  if (!entry.isDirectory()) continue;
2982
3561
  const name = entry.name;
2983
- const blockDir = path15.join(blocksDir, name);
3562
+ const blockDir = path19.join(blocksDir, name);
2984
3563
  const info = {
2985
3564
  name,
2986
- hasJs: existsSync12(path15.join(blockDir, `${name}.js`)),
2987
- hasCss: existsSync12(path15.join(blockDir, `${name}.css`)),
2988
- hasUeModel: existsSync12(path15.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`))
2989
3568
  };
2990
- const metaFile = path15.join(blockDir, ".eds-meta.json");
2991
- if (existsSync12(metaFile)) {
3569
+ const metaFile = path19.join(blockDir, ".eds-meta.json");
3570
+ if (existsSync16(metaFile)) {
2992
3571
  try {
2993
- const meta = JSON.parse(await readFile9(metaFile, "utf-8"));
3572
+ const meta = JSON.parse(await readFile12(metaFile, "utf-8"));
2994
3573
  info.figmaSource = {
2995
3574
  fileKey: meta.figmaFileKey,
2996
3575
  nodeId: meta.figmaNodeId,
@@ -3013,17 +3592,17 @@ async function listBlocks(options = {}) {
3013
3592
  const nameWidth = columnWidth(blocks.map((b) => b.name));
3014
3593
  for (const b of blocks) {
3015
3594
  const badges = [
3016
- b.hasJs ? brand.ok("js") : chalk8.dim.strikethrough("js"),
3017
- 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")
3018
3597
  ];
3019
3598
  if (b.hasUeModel) badges.push(brand.blue("ue-model"));
3020
3599
  if (b.figmaSource) badges.push(`${icon.figma} ${brand.dotPurple("figma")}`);
3021
- const name = padEndVisible(chalk8.bold(b.name), nameWidth);
3022
- 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("]")}`);
3023
3602
  if (b.figmaSource) {
3024
3603
  const synced = new Date(b.figmaSource.lastSyncedAt).toLocaleDateString();
3025
3604
  logger.info(
3026
- ` ${" ".repeat(nameWidth)} ${icon.arrow} ${chalk8.dim(
3605
+ ` ${" ".repeat(nameWidth)} ${icon.arrow} ${chalk9.dim(
3027
3606
  `figma:${b.figmaSource.fileKey} \xB7 synced ${synced}`
3028
3607
  )}`
3029
3608
  );
@@ -3063,22 +3642,85 @@ var init_integrations = __esm({
3063
3642
  id: "gtm",
3064
3643
  name: "Google Tag Manager",
3065
3644
  fields: [{ name: "id", message: "GTM container ID", placeholder: "GTM-XXXXXXX" }],
3066
- code: (v) => `// Google Tag Manager (${v.id}) \u2014 delayed phase, after LCP.
3067
- (function (w, d, s, l, i) {
3068
- w[l] = w[l] || [];
3069
- w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
3070
- const f = d.getElementsByTagName(s)[0];
3071
- const j = d.createElement(s);
3072
- j.async = true;
3073
- j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i;
3074
- f.parentNode.insertBefore(j, f);
3075
- })(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
+ ]
3076
3718
  },
3077
3719
  {
3078
3720
  id: "ga4",
3079
3721
  name: "Google Analytics 4",
3080
3722
  fields: [{ name: "id", message: "Measurement ID", placeholder: "G-XXXXXXXXXX" }],
3081
- code: (v) => `// Google Analytics 4 (${v.id}) \u2014 delayed phase.
3723
+ code: (v) => `// Google Analytics 4 (${v.id}) - delayed phase.
3082
3724
  const gaScript = document.createElement('script');
3083
3725
  gaScript.async = true;
3084
3726
  gaScript.src = 'https://www.googletagmanager.com/gtag/js?id=${v.id}';
@@ -3095,7 +3737,7 @@ gtag('config', '${v.id}');`
3095
3737
  { name: "label", message: "Button label", placeholder: "Chat with us" },
3096
3738
  { name: "src", message: "Chat widget script URL" }
3097
3739
  ],
3098
- 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.
3099
3741
  const chatBtn = document.createElement('button');
3100
3742
  chatBtn.type = 'button';
3101
3743
  chatBtn.className = 'chat-facade';
@@ -3113,7 +3755,7 @@ document.body.appendChild(chatBtn);`
3113
3755
  id: "cookiebot",
3114
3756
  name: "Cookie consent (Cookiebot)",
3115
3757
  fields: [{ name: "id", message: "Cookiebot domain group ID (CBID)" }],
3116
- 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.
3117
3759
  const cb = document.createElement('script');
3118
3760
  cb.id = 'Cookiebot';
3119
3761
  cb.src = 'https://consent.cookiebot.com/uc.js';
@@ -3125,7 +3767,7 @@ document.head.appendChild(cb);`
3125
3767
  id: "custom",
3126
3768
  name: "Custom third-party script",
3127
3769
  fields: [{ name: "url", message: "Script URL" }],
3128
- code: (v) => `// Custom third-party script \u2014 delayed phase.
3770
+ code: (v) => `// Custom third-party script - delayed phase.
3129
3771
  const s = document.createElement('script');
3130
3772
  s.async = true;
3131
3773
  s.src = ${JSON.stringify(v.url || "")};
@@ -3140,10 +3782,10 @@ var integrate_exports = {};
3140
3782
  __export(integrate_exports, {
3141
3783
  integrate: () => integrate
3142
3784
  });
3143
- import { existsSync as existsSync13 } from "fs";
3144
- import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
3145
- import path16 from "path";
3146
- 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";
3147
3789
  async function integrate(type) {
3148
3790
  const projectRoot = findProjectRoot();
3149
3791
  if (!projectRoot) {
@@ -3151,7 +3793,7 @@ async function integrate(type) {
3151
3793
  process.exitCode = 1;
3152
3794
  return;
3153
3795
  }
3154
- logger.logoOnce(logo("Integrate \u2014 third-party service"));
3796
+ logger.logoOnce(logo("Integrate - third-party service"));
3155
3797
  const { select, input } = await import("@inquirer/prompts");
3156
3798
  const chosenId = type || await select({
3157
3799
  message: "Which service do you want to add?",
@@ -3170,26 +3812,39 @@ async function integrate(type) {
3170
3812
  const answer = await input({ message: ` ${f.message}`, default: f.placeholder });
3171
3813
  values[f.name] = answer.trim();
3172
3814
  }
3173
- const block2 = wrapBlock(integration.name, integration.code(values));
3174
- const delayedPath = path16.join(projectRoot, "scripts", "delayed.js");
3175
- await mkdir5(path16.dirname(delayedPath), { recursive: true });
3176
- const existing = existsSync13(delayedPath) ? await readFile10(delayedPath, "utf-8") : DELAYED_HEADER;
3177
- await writeFile8(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*$/, "")}
3178
3832
  ${block2}`);
3179
3833
  logger.info(heading("Added"));
3180
- 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"));
3181
3836
  logger.info("");
3182
3837
  logger.info(
3183
- 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.")
3184
3839
  );
3185
3840
  if (integration.id === "gtm" || integration.id === "ga4") {
3186
3841
  logger.info(
3187
- 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).")
3188
3843
  );
3189
3844
  }
3190
3845
  if (integration.id === "chat") {
3191
3846
  logger.info(
3192
- 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).")
3193
3848
  );
3194
3849
  }
3195
3850
  logger.info("");
@@ -3204,7 +3859,7 @@ var init_integrate = __esm({
3204
3859
  init_logger();
3205
3860
  init_project();
3206
3861
  init_ui();
3207
- DELAYED_HEADER = `// delayed.js \u2014 third-party / non-critical scripts.
3862
+ DELAYED_HEADER = `// delayed.js - third-party / non-critical scripts.
3208
3863
  // Loaded in the EDS delayed phase (after LCP), so nothing here blocks the page.
3209
3864
  `;
3210
3865
  }
@@ -3215,9 +3870,9 @@ var setup_exports = {};
3215
3870
  __export(setup_exports, {
3216
3871
  figmaSetup: () => figmaSetup
3217
3872
  });
3218
- import { existsSync as existsSync14 } from "fs";
3219
- import { mkdir as mkdir6, writeFile as writeFile9 } from "fs/promises";
3220
- 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";
3221
3876
  async function figmaSetup() {
3222
3877
  const { select, confirm } = await import("@inquirer/prompts");
3223
3878
  logger.logoOnce(logo());
@@ -3227,15 +3882,15 @@ async function figmaSetup() {
3227
3882
  let selectedAgent;
3228
3883
  if (agents.length === 0) {
3229
3884
  logger.warn("No AI agents found (claude, cursor, codex).");
3230
- 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")}`);
3231
3886
  selectedAgent = "none";
3232
3887
  } else {
3233
- logger.success(`Found: ${agents.map((a) => chalk10.cyan(a.type)).join(", ")}`);
3888
+ logger.success(`Found: ${agents.map((a) => chalk11.cyan(a.type)).join(", ")}`);
3234
3889
  selectedAgent = await select({
3235
3890
  message: "Which agent do you want to use with eds-cli?",
3236
3891
  choices: [
3237
3892
  ...agents.map((a) => ({
3238
- name: `${a.type}${a.hasFigmaMcp ? chalk10.green(" (Figma MCP detected)") : ""}`,
3893
+ name: `${a.type}${a.hasFigmaMcp ? chalk11.green(" (Figma MCP detected)") : ""}`,
3239
3894
  value: a.type
3240
3895
  })),
3241
3896
  { name: "None (manual mode)", value: "none" }
@@ -3251,17 +3906,17 @@ async function figmaSetup() {
3251
3906
  switch (selectedAgent) {
3252
3907
  case "claude":
3253
3908
  logger.info(
3254
- ` ${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")}`
3255
3910
  );
3256
3911
  logger.info(" or for Figma Desktop app:");
3257
3912
  logger.info(
3258
- ` ${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")}`
3259
3914
  );
3260
3915
  break;
3261
3916
  case "cursor":
3262
- logger.info(` Add to ${chalk10.cyan("~/.cursor/mcp.json")}:`);
3917
+ logger.info(` Add to ${chalk11.cyan("~/.cursor/mcp.json")}:`);
3263
3918
  logger.info(
3264
- chalk10.dim(
3919
+ chalk11.dim(
3265
3920
  JSON.stringify(
3266
3921
  {
3267
3922
  mcpServers: {
@@ -3286,7 +3941,7 @@ async function figmaSetup() {
3286
3941
  }
3287
3942
  }
3288
3943
  logger.info(
3289
- chalk10.dim(
3944
+ chalk11.dim(
3290
3945
  "\nNote: Figma MCP requires a Figma seat (Dev or Full on paid plans for desktop server)."
3291
3946
  )
3292
3947
  );
@@ -3296,22 +3951,22 @@ async function figmaSetup() {
3296
3951
  });
3297
3952
  if (shouldSave) {
3298
3953
  const configDir = globalConfigDir();
3299
- if (!existsSync14(configDir)) {
3300
- await mkdir6(configDir, { recursive: true });
3954
+ if (!existsSync18(configDir)) {
3955
+ await mkdir7(configDir, { recursive: true });
3301
3956
  }
3302
3957
  const config = {};
3303
3958
  if (selectedAgent !== "none") {
3304
3959
  config.agent = selectedAgent;
3305
3960
  }
3306
- await writeFile9(globalConfigPath(), `${JSON.stringify(config, null, 2)}
3961
+ await writeFile11(globalConfigPath(), `${JSON.stringify(config, null, 2)}
3307
3962
  `, {
3308
3963
  mode: 384
3309
3964
  });
3310
- logger.success(`Config saved to ${chalk10.cyan(globalConfigPath())}`);
3965
+ logger.success(`Config saved to ${chalk11.cyan(globalConfigPath())}`);
3311
3966
  }
3312
- logger.info(chalk10.bold("\nSetup complete!"));
3967
+ logger.info(chalk11.bold("\nSetup complete!"));
3313
3968
  if (selectedAgent !== "none") {
3314
- 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')}`);
3315
3970
  }
3316
3971
  }
3317
3972
  var init_setup = __esm({
@@ -3330,9 +3985,9 @@ var doctor_exports = {};
3330
3985
  __export(doctor_exports, {
3331
3986
  doctor: () => doctor
3332
3987
  });
3333
- import { existsSync as existsSync15 } from "fs";
3334
- import { readFile as readFile11, readdir as readdir2 } from "fs/promises";
3335
- import path17 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";
3336
3991
  import { parse as parseYaml } from "yaml";
3337
3992
  async function doctor(options = {}) {
3338
3993
  const projectRoot = findProjectRoot();
@@ -3354,7 +4009,7 @@ async function doctor(options = {}) {
3354
4009
  });
3355
4010
  if (project.hasFstab) {
3356
4011
  try {
3357
- const content = await readFile11(path17.join(projectRoot, "fstab.yaml"), "utf-8");
4012
+ const content = await readFile14(path21.join(projectRoot, "fstab.yaml"), "utf-8");
3358
4013
  parseYaml(content);
3359
4014
  results.push({ name: "fstab.yaml valid YAML", status: "pass", message: "Valid" });
3360
4015
  } catch {
@@ -3376,28 +4031,28 @@ async function doctor(options = {}) {
3376
4031
  message: project.hasBlocks ? "Found" : "Missing - no blocks yet"
3377
4032
  });
3378
4033
  if (project.hasBlocks) {
3379
- const blocksDir = path17.join(projectRoot, "blocks");
3380
- const entries = await readdir2(blocksDir, { withFileTypes: true });
4034
+ const blocksDir = path21.join(projectRoot, "blocks");
4035
+ const entries = await readdir3(blocksDir, { withFileTypes: true });
3381
4036
  let aggregatedModels = "";
3382
- const aggregatedPath = path17.join(projectRoot, "component-models.json");
3383
- if (existsSync15(aggregatedPath)) {
4037
+ const aggregatedPath = path21.join(projectRoot, "component-models.json");
4038
+ if (existsSync19(aggregatedPath)) {
3384
4039
  try {
3385
- aggregatedModels = await readFile11(aggregatedPath, "utf-8");
4040
+ aggregatedModels = await readFile14(aggregatedPath, "utf-8");
3386
4041
  } catch {
3387
4042
  }
3388
4043
  }
3389
4044
  for (const entry of entries) {
3390
4045
  if (!entry.isDirectory()) continue;
3391
4046
  const blockName = entry.name;
3392
- const jsFile = path17.join(blocksDir, blockName, `${blockName}.js`);
3393
- const hasJs = existsSync15(jsFile);
4047
+ const jsFile = path21.join(blocksDir, blockName, `${blockName}.js`);
4048
+ const hasJs = existsSync19(jsFile);
3394
4049
  results.push({
3395
4050
  name: `blocks/${blockName} has JS`,
3396
4051
  status: hasJs ? "pass" : "warn",
3397
4052
  message: hasJs ? "Found" : `Missing ${blockName}.js`
3398
4053
  });
3399
4054
  if (hasJs) {
3400
- const jsContent = await readFile11(jsFile, "utf-8");
4055
+ const jsContent = await readFile14(jsFile, "utf-8");
3401
4056
  if (jsContent.includes("console.log")) {
3402
4057
  results.push({
3403
4058
  name: `blocks/${blockName} no console.log`,
@@ -3406,17 +4061,17 @@ async function doctor(options = {}) {
3406
4061
  });
3407
4062
  }
3408
4063
  }
3409
- const hasPerBlockModel = existsSync15(path17.join(blocksDir, blockName, `_${blockName}.json`));
4064
+ const hasPerBlockModel = existsSync19(path21.join(blocksDir, blockName, `_${blockName}.json`));
3410
4065
  const hasAggregatedModel = aggregatedModels.includes(`"${blockName}"`);
3411
4066
  results.push({
3412
4067
  name: `blocks/${blockName} UE model`,
3413
4068
  status: hasPerBlockModel || hasAggregatedModel ? "pass" : "warn",
3414
4069
  message: hasPerBlockModel || hasAggregatedModel ? hasPerBlockModel ? `_${blockName}.json` : "in component-models.json" : "no Universal Editor model - authors can't edit this block in UE"
3415
4070
  });
3416
- const metaFile = path17.join(blocksDir, blockName, ".eds-meta.json");
3417
- if (existsSync15(metaFile)) {
4071
+ const metaFile = path21.join(blocksDir, blockName, ".eds-meta.json");
4072
+ if (existsSync19(metaFile)) {
3418
4073
  try {
3419
- const meta = JSON.parse(await readFile11(metaFile, "utf-8"));
4074
+ const meta = JSON.parse(await readFile14(metaFile, "utf-8"));
3420
4075
  const syncDate = new Date(meta.lastSyncedAt);
3421
4076
  const daysSinceSync = Math.floor(
3422
4077
  (Date.now() - syncDate.getTime()) / (1e3 * 60 * 60 * 24)
@@ -3436,10 +4091,10 @@ async function doctor(options = {}) {
3436
4091
  }
3437
4092
  }
3438
4093
  }
3439
- const queryFile = path17.join(projectRoot, "helix-query.yaml");
3440
- if (existsSync15(queryFile)) {
4094
+ const queryFile = path21.join(projectRoot, "helix-query.yaml");
4095
+ if (existsSync19(queryFile)) {
3441
4096
  try {
3442
- const content = await readFile11(queryFile, "utf-8");
4097
+ const content = await readFile14(queryFile, "utf-8");
3443
4098
  parseYaml(content);
3444
4099
  results.push({ name: "helix-query.yaml valid", status: "pass", message: "Valid YAML" });
3445
4100
  } catch {
@@ -3496,10 +4151,10 @@ var audit_loading_exports = {};
3496
4151
  __export(audit_loading_exports, {
3497
4152
  auditLoading: () => auditLoading
3498
4153
  });
3499
- import { existsSync as existsSync16 } from "fs";
3500
- import { readFile as readFile12, readdir as readdir3, stat as stat3 } from "fs/promises";
3501
- import path18 from "path";
3502
- 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";
3503
4158
  async function auditLoading(options = {}) {
3504
4159
  const projectRoot = findProjectRoot();
3505
4160
  if (!projectRoot) {
@@ -3532,8 +4187,8 @@ async function auditLoading(options = {}) {
3532
4187
  printResults(results);
3533
4188
  }
3534
4189
  async function checkHeadHtml(root, results) {
3535
- const filePath = path18.join(root, "head.html");
3536
- if (!existsSync16(filePath)) {
4190
+ const filePath = path22.join(root, "head.html");
4191
+ if (!existsSync20(filePath)) {
3537
4192
  results.push({
3538
4193
  rule: "head.html exists",
3539
4194
  status: "warn",
@@ -3541,7 +4196,7 @@ async function checkHeadHtml(root, results) {
3541
4196
  });
3542
4197
  return;
3543
4198
  }
3544
- const content = await readFile12(filePath, "utf-8");
4199
+ const content = await readFile15(filePath, "utf-8");
3545
4200
  const lines = content.split("\n");
3546
4201
  for (let i = 0; i < lines.length; i++) {
3547
4202
  const line = lines[i];
@@ -3613,8 +4268,8 @@ async function checkHeadHtml(root, results) {
3613
4268
  }
3614
4269
  }
3615
4270
  async function checkScriptsJs(root, results) {
3616
- const filePath = path18.join(root, "scripts", "scripts.js");
3617
- if (!existsSync16(filePath)) {
4271
+ const filePath = path22.join(root, "scripts", "scripts.js");
4272
+ if (!existsSync20(filePath)) {
3618
4273
  results.push({
3619
4274
  rule: "scripts/scripts.js exists",
3620
4275
  status: "warn",
@@ -3622,7 +4277,7 @@ async function checkScriptsJs(root, results) {
3622
4277
  });
3623
4278
  return;
3624
4279
  }
3625
- const content = await readFile12(filePath, "utf-8");
4280
+ const content = await readFile15(filePath, "utf-8");
3626
4281
  const lines = content.split("\n");
3627
4282
  const hasLoadEager = /function\s+loadEager|loadEager\s*=/i.test(content);
3628
4283
  const hasLoadLazy = /function\s+loadLazy|loadLazy\s*=/i.test(content);
@@ -3700,8 +4355,8 @@ async function checkScriptsJs(root, results) {
3700
4355
  }
3701
4356
  }
3702
4357
  async function checkStylesCss(root, results) {
3703
- const filePath = path18.join(root, "styles", "styles.css");
3704
- if (!existsSync16(filePath)) return;
4358
+ const filePath = path22.join(root, "styles", "styles.css");
4359
+ if (!existsSync20(filePath)) return;
3705
4360
  const stats = await stat3(filePath);
3706
4361
  const sizeKb = stats.size / 1024;
3707
4362
  results.push({
@@ -3710,7 +4365,7 @@ async function checkStylesCss(root, results) {
3710
4365
  message: `${sizeKb.toFixed(1)} KB${sizeKb > 40 ? " \u2014 large CSS competes with 100KB LCP budget" : ""}`,
3711
4366
  file: "styles/styles.css"
3712
4367
  });
3713
- const content = await readFile12(filePath, "utf-8");
4368
+ const content = await readFile15(filePath, "utf-8");
3714
4369
  if (/@import\s/.test(content)) {
3715
4370
  results.push({
3716
4371
  rule: "No @import in styles.css",
@@ -3721,8 +4376,8 @@ async function checkStylesCss(root, results) {
3721
4376
  }
3722
4377
  }
3723
4378
  async function checkDelayedJs(root, results) {
3724
- const filePath = path18.join(root, "scripts", "delayed.js");
3725
- if (!existsSync16(filePath)) {
4379
+ const filePath = path22.join(root, "scripts", "delayed.js");
4380
+ if (!existsSync20(filePath)) {
3726
4381
  results.push({
3727
4382
  rule: "scripts/delayed.js exists",
3728
4383
  status: "info",
@@ -3730,7 +4385,7 @@ async function checkDelayedJs(root, results) {
3730
4385
  });
3731
4386
  return;
3732
4387
  }
3733
- const content = await readFile12(filePath, "utf-8");
4388
+ const content = await readFile15(filePath, "utf-8");
3734
4389
  const stripped = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").trim();
3735
4390
  if (!stripped) {
3736
4391
  results.push({
@@ -3742,15 +4397,15 @@ async function checkDelayedJs(root, results) {
3742
4397
  }
3743
4398
  }
3744
4399
  async function checkBlocks(root, results) {
3745
- const blocksDir = path18.join(root, "blocks");
3746
- if (!existsSync16(blocksDir)) return;
3747
- const entries = await readdir3(blocksDir, { withFileTypes: true });
4400
+ const blocksDir = path22.join(root, "blocks");
4401
+ if (!existsSync20(blocksDir)) return;
4402
+ const entries = await readdir4(blocksDir, { withFileTypes: true });
3748
4403
  for (const entry of entries) {
3749
4404
  if (!entry.isDirectory()) continue;
3750
4405
  const blockName = entry.name;
3751
- const jsFile = path18.join(blocksDir, blockName, `${blockName}.js`);
3752
- if (!existsSync16(jsFile)) continue;
3753
- const content = await readFile12(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");
3754
4409
  const lines = content.split("\n");
3755
4410
  const heavyLibs = [
3756
4411
  "jquery",
@@ -3800,9 +4455,9 @@ async function checkBlocks(root, results) {
3800
4455
  for (const entry of entries) {
3801
4456
  if (!entry.isDirectory()) continue;
3802
4457
  const blockName = entry.name;
3803
- const cssFile = path18.join(blocksDir, blockName, `${blockName}.css`);
3804
- if (!existsSync16(cssFile)) continue;
3805
- const content = await readFile12(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");
3806
4461
  const selectorPattern = /^([a-z][a-z0-9-]*)\s*\{/gm;
3807
4462
  for (const selectorMatch of content.matchAll(selectorPattern)) {
3808
4463
  const selector = selectorMatch[1];
@@ -3818,8 +4473,8 @@ async function checkBlocks(root, results) {
3818
4473
  }
3819
4474
  }
3820
4475
  async function checkAemJs(root, results) {
3821
- const filePath = path18.join(root, "scripts", "aem.js");
3822
- if (!existsSync16(filePath)) {
4476
+ const filePath = path22.join(root, "scripts", "aem.js");
4477
+ if (!existsSync20(filePath)) {
3823
4478
  results.push({
3824
4479
  rule: "scripts/aem.js exists",
3825
4480
  status: "warn",
@@ -3857,12 +4512,12 @@ function printResults(results) {
3857
4512
  grouped.set(key, bucket);
3858
4513
  }
3859
4514
  for (const [file, fileResults] of grouped) {
3860
- logger.info(` ${icon.bullet} ${chalk11.underline(file)}`);
4515
+ logger.info(` ${icon.bullet} ${chalk12.underline(file)}`);
3861
4516
  for (const r of fileResults) {
3862
- const loc = r.line ? chalk11.dim(`:${r.line}`) : "";
4517
+ const loc = r.line ? chalk12.dim(`:${r.line}`) : "";
3863
4518
  logger.info(` ${statusIcon(r.status)} ${r.rule}${loc}`);
3864
4519
  if (r.status !== "pass") {
3865
- logger.info(chalk11.dim(` ${r.message}`));
4520
+ logger.info(chalk12.dim(` ${r.message}`));
3866
4521
  }
3867
4522
  }
3868
4523
  logger.info("");
@@ -3891,17 +4546,17 @@ var audit_security_exports = {};
3891
4546
  __export(audit_security_exports, {
3892
4547
  auditSecurity: () => auditSecurity
3893
4548
  });
3894
- import { existsSync as existsSync17 } from "fs";
3895
- import { readFile as readFile13, readdir as readdir4 } from "fs/promises";
3896
- import path19 from "path";
3897
- import chalk12 from "chalk";
3898
- async function* walk(dir) {
3899
- const entries = await readdir4(dir, { withFileTypes: true });
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";
4553
+ async function* walk2(dir) {
4554
+ const entries = await readdir5(dir, { withFileTypes: true });
3900
4555
  for (const entry of entries) {
3901
- const full = path19.join(dir, entry.name);
4556
+ const full = path23.join(dir, entry.name);
3902
4557
  if (entry.isDirectory()) {
3903
4558
  if (IGNORE_DIRS.has(entry.name)) continue;
3904
- yield* walk(full);
4559
+ yield* walk2(full);
3905
4560
  } else if (entry.isFile()) {
3906
4561
  yield full;
3907
4562
  }
@@ -3932,11 +4587,11 @@ function scanContent(content, relFile, ext) {
3932
4587
  return findings;
3933
4588
  }
3934
4589
  async function auditDependencies(root) {
3935
- if (!existsSync17(path19.join(root, "package.json"))) {
4590
+ if (!existsSync21(path23.join(root, "package.json"))) {
3936
4591
  return { status: "skipped", note: "no package.json" };
3937
4592
  }
3938
- const hasLock = existsSync17(path19.join(root, "package-lock.json")) || existsSync17(path19.join(root, "npm-shrinkwrap.json"));
3939
- if (!hasLock && !existsSync17(path19.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"))) {
3940
4595
  return { status: "skipped", note: "no package-lock.json / node_modules \u2014 run npm install" };
3941
4596
  }
3942
4597
  try {
@@ -3967,12 +4622,12 @@ async function auditSecurity(options = {}) {
3967
4622
  return;
3968
4623
  }
3969
4624
  const findings = [];
3970
- for await (const file of walk(projectRoot)) {
3971
- const ext = path19.extname(file).slice(1).toLowerCase();
4625
+ for await (const file of walk2(projectRoot)) {
4626
+ const ext = path23.extname(file).slice(1).toLowerCase();
3972
4627
  if (!SCAN_EXT.has(ext)) continue;
3973
- const rel = path19.relative(projectRoot, file);
4628
+ const rel = path23.relative(projectRoot, file);
3974
4629
  if (rel.endsWith("aem.js") || rel.endsWith("lib-franklin.js")) continue;
3975
- const content = await readFile13(file, "utf-8");
4630
+ const content = await readFile16(file, "utf-8");
3976
4631
  findings.push(...scanContent(content, rel, ext));
3977
4632
  }
3978
4633
  const deps = await auditDependencies(projectRoot);
@@ -4020,9 +4675,9 @@ async function auditSecurity(options = {}) {
4020
4675
  );
4021
4676
  for (const f of sorted) {
4022
4677
  const meta = SEVERITY_META[f.severity];
4023
- const loc = chalk12.dim(`${f.file}:${f.line}`);
4024
- logger.info(` ${statusIcon(meta.status)} ${chalk12.bold(meta.label)} ${f.message} ${loc}`);
4025
- 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}`));
4026
4681
  }
4027
4682
  }
4028
4683
  const summaryLine = summary({
@@ -4229,7 +4884,7 @@ __export(preview_exports2, {
4229
4884
  preview: () => preview,
4230
4885
  resolveAdminConfig: () => resolveAdminConfig
4231
4886
  });
4232
- import chalk13 from "chalk";
4887
+ import chalk14 from "chalk";
4233
4888
  import ora2 from "ora";
4234
4889
  async function preview(paths, options) {
4235
4890
  const projectRoot = findProjectRoot();
@@ -4242,7 +4897,7 @@ async function preview(paths, options) {
4242
4897
  let hasErrors = false;
4243
4898
  for (const r of results) {
4244
4899
  if (r.ok) {
4245
- logger.success(`${r.path} \u2192 ${chalk13.cyan(r.previewUrl || "preview updated")}`);
4900
+ logger.success(`${r.path} \u2192 ${chalk14.cyan(r.previewUrl || "preview updated")}`);
4246
4901
  } else {
4247
4902
  logger.error(`${r.path} \u2014 ${r.message}`);
4248
4903
  hasErrors = true;
@@ -4256,7 +4911,7 @@ function resolveAdminConfig(config, options) {
4256
4911
  const ref = options.ref || config.admin?.ref || "main";
4257
4912
  if (!org || !site) {
4258
4913
  logger.error("Missing --org and --site. Set them via flags or in .edsrc.json:");
4259
- logger.info(chalk13.dim(' { "admin": { "org": "my-org", "site": "my-site" } }'));
4914
+ logger.info(chalk14.dim(' { "admin": { "org": "my-org", "site": "my-site" } }'));
4260
4915
  process.exitCode = 1;
4261
4916
  return void 0;
4262
4917
  }
@@ -4278,7 +4933,7 @@ var publish_exports = {};
4278
4933
  __export(publish_exports, {
4279
4934
  publish: () => publish
4280
4935
  });
4281
- import chalk14 from "chalk";
4936
+ import chalk15 from "chalk";
4282
4937
  import ora3 from "ora";
4283
4938
  async function publish(paths, options) {
4284
4939
  const projectRoot = findProjectRoot();
@@ -4291,7 +4946,7 @@ async function publish(paths, options) {
4291
4946
  let hasErrors = false;
4292
4947
  for (const r of results) {
4293
4948
  if (r.ok) {
4294
- logger.success(`${r.path} \u2192 ${chalk14.cyan(r.liveUrl || "published")}`);
4949
+ logger.success(`${r.path} \u2192 ${chalk15.cyan(r.liveUrl || "published")}`);
4295
4950
  } else {
4296
4951
  logger.error(`${r.path} \u2014 ${r.message}`);
4297
4952
  hasErrors = true;
@@ -4390,13 +5045,13 @@ var menu_exports = {};
4390
5045
  __export(menu_exports, {
4391
5046
  mainMenu: () => mainMenu
4392
5047
  });
4393
- import { existsSync as existsSync18 } from "fs";
4394
- import { readdir as readdir5 } from "fs/promises";
4395
- import path20 from "path";
5048
+ import { existsSync as existsSync22 } from "fs";
5049
+ import { readdir as readdir6 } from "fs/promises";
5050
+ import path24 from "path";
4396
5051
  async function listBlockNames(projectRoot) {
4397
- const dir = path20.join(projectRoot, "blocks");
4398
- if (!existsSync18(dir)) return [];
4399
- const entries = await readdir5(dir, { withFileTypes: true });
5052
+ const dir = path24.join(projectRoot, "blocks");
5053
+ if (!existsSync22(dir)) return [];
5054
+ const entries = await readdir6(dir, { withFileTypes: true });
4400
5055
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
4401
5056
  }
4402
5057
  async function mainMenu() {
@@ -4406,10 +5061,15 @@ async function mainMenu() {
4406
5061
  message: "Choose an action",
4407
5062
  choices: [
4408
5063
  { name: "Set up this project (authoring model, CI, naming)", value: "init" },
4409
- { 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
+ },
5068
+ { name: "Migrate a classic AEM component to EDS", value: "migrate" },
4410
5069
  { name: "Scaffold Universal Editor components", value: "scaffold" },
4411
5070
  { name: "Create an empty block", value: "create" },
4412
5071
  { name: "Add a third-party integration (GTM, chat, consent\u2026)", value: "integrate" },
5072
+ { name: "Instrument a block for analytics (dataLayer)", value: "track" },
4413
5073
  { name: "Preview a block in the browser", value: "preview-block" },
4414
5074
  { name: "Preview / publish pages (Admin API)", value: "admin" },
4415
5075
  { name: "Audit the project", value: "audit" },
@@ -4427,6 +5087,11 @@ async function mainMenu() {
4427
5087
  await blockFromDesign2(void 0, {});
4428
5088
  return;
4429
5089
  }
5090
+ case "migrate": {
5091
+ const { migrateComponent: migrateComponent2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
5092
+ await migrateComponent2(void 0, {});
5093
+ return;
5094
+ }
4430
5095
  case "scaffold": {
4431
5096
  const { scaffoldInteractive: scaffoldInteractive2 } = await Promise.resolve().then(() => (init_scaffold(), scaffold_exports));
4432
5097
  await scaffoldInteractive2();
@@ -4465,6 +5130,25 @@ async function mainMenu() {
4465
5130
  await previewBlock2(name, {});
4466
5131
  return;
4467
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
+ }
4468
5152
  case "admin": {
4469
5153
  const { adminWizard: adminWizard2 } = await Promise.resolve().then(() => (init_admin(), admin_exports));
4470
5154
  await adminWizard2();
@@ -4510,13 +5194,13 @@ var init_menu = __esm({
4510
5194
 
4511
5195
  // src/bin/eds.ts
4512
5196
  init_esm_shims();
4513
- import chalk15 from "chalk";
5197
+ import chalk16 from "chalk";
4514
5198
  import { Command } from "commander";
4515
5199
 
4516
5200
  // package.json
4517
5201
  var package_default = {
4518
5202
  name: "aem-eds-kit",
4519
- version: "0.4.7",
5203
+ version: "0.4.12",
4520
5204
  description: "CLI for AEM Edge Delivery Services with Figma MCP integration",
4521
5205
  type: "module",
4522
5206
  engines: {
@@ -4566,6 +5250,7 @@ var package_default = {
4566
5250
  chalk: "^5.4.0",
4567
5251
  commander: "^13.0.0",
4568
5252
  execa: "^9.5.0",
5253
+ "fast-xml-parser": "^5.11.2",
4569
5254
  ora: "^8.1.0",
4570
5255
  yaml: "^2.6.0",
4571
5256
  zod: "^3.24.0"
@@ -4591,13 +5276,23 @@ program.name("eds").description("CLI for AEM Edge Delivery Services with Figma i
4591
5276
  const opts = thisCommand.opts();
4592
5277
  if (opts.verbose) logger.setLevel("debug");
4593
5278
  if (opts.quiet) logger.setQuiet(true);
4594
- if (opts.color === false) chalk15.level = 0;
5279
+ if (opts.color === false) chalk16.level = 0;
4595
5280
  });
4596
5281
  program.addHelpText("beforeAll", () => logo());
4597
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) => {
4598
5283
  const { initProject: initProject2 } = await Promise.resolve().then(() => (init_init(), init_exports));
4599
5284
  await initProject2(options);
4600
5285
  });
5286
+ var migrate = program.command("migrate").description("Migrate from classic AEM to EDS");
5287
+ migrate.command("component [dir]").description("Migrate a classic AEM component's dialog into an EDS block + UE model").option("--name <name>", "Block name (otherwise inferred from the component)").option("--clientlib <dir>", "Path to the component's clientlib (otherwise auto-detected)").option("--yes", "Skip confirmation prompts").action(async (dir, options) => {
5288
+ const { migrateComponent: migrateComponent2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
5289
+ await migrateComponent2(dir, options);
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
+ });
4601
5296
  var block = program.command("block").description("Manage EDS blocks");
4602
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) => {
4603
5298
  const { createBlock: createBlock2 } = await Promise.resolve().then(() => (init_create(), create_exports));