aem-eds-kit 0.4.9 → 0.4.16

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
@@ -67,7 +67,7 @@ function mix(a, b, t) {
67
67
  const r = Math.round(ar + (br - ar) * t);
68
68
  const g = Math.round(ag + (bg - ag) * t);
69
69
  const bl = Math.round(ab + (bb - ab) * t);
70
- return `#${[r, g, bl].map((n) => n.toString(16).padStart(2, "0")).join("")}`;
70
+ return `#${[r, g, bl].map((n2) => n2.toString(16).padStart(2, "0")).join("")}`;
71
71
  }
72
72
  function gradientLine(line, stops, width) {
73
73
  const chars = [...line];
@@ -127,8 +127,8 @@ function summary(counts) {
127
127
  if (counts.info) parts.push(`${icon.info} ${counts.info} info`);
128
128
  return parts.join(chalk.dim(" "));
129
129
  }
130
- function plural(n, word) {
131
- return n === 1 ? word : `${word}s`;
130
+ function plural(n2, word) {
131
+ return n2 === 1 ? word : `${word}s`;
132
132
  }
133
133
  var brand, icon, ANSI, DOTS_N, LOGO_ART, LOGO_STOPS;
134
134
  var init_ui = __esm({
@@ -355,117 +355,6 @@ var init_project = __esm({
355
355
  }
356
356
  });
357
357
 
358
- // src/lib/scaffold/ci.ts
359
- import { existsSync as existsSync2 } from "fs";
360
- import { mkdir, writeFile } from "fs/promises";
361
- import path3 from "path";
362
- async function writeCiWorkflow(projectRoot) {
363
- const file = path3.join(projectRoot, ".github", "workflows", "eds.yml");
364
- if (existsSync2(file)) return { path: CI_WORKFLOW_PATH, created: false };
365
- await mkdir(path3.dirname(file), { recursive: true });
366
- await writeFile(file, CI_WORKFLOW);
367
- return { path: CI_WORKFLOW_PATH, created: true };
368
- }
369
- var CI_WORKFLOW_PATH, CI_WORKFLOW;
370
- var init_ci = __esm({
371
- "src/lib/scaffold/ci.ts"() {
372
- "use strict";
373
- init_esm_shims();
374
- CI_WORKFLOW_PATH = ".github/workflows/eds.yml";
375
- CI_WORKFLOW = `name: EDS checks
376
-
377
- # Generated by aem-eds-kit (\`eds scaffold ci\`). Runs the project's own
378
- # lint/build plus the eds audits as a gatekeeper on every PR and push to main.
379
- on:
380
- pull_request:
381
- push:
382
- branches: [main]
383
-
384
- jobs:
385
- checks:
386
- runs-on: ubuntu-latest
387
- steps:
388
- - uses: actions/checkout@v4
389
- - uses: actions/setup-node@v4
390
- with:
391
- node-version: 20
392
-
393
- # Install project dependencies when the repo has them.
394
- - name: Install dependencies
395
- run: |
396
- if [ -f package-lock.json ]; then
397
- npm ci
398
- elif [ -f package.json ]; then
399
- npm install
400
- fi
401
-
402
- # The project's own scripts, if defined (skipped otherwise).
403
- - name: Lint
404
- if: hashFiles('package.json') != ''
405
- run: npm run lint --if-present
406
- - name: Build
407
- if: hashFiles('package.json') != ''
408
- run: npm run build --if-present
409
-
410
- # aem-eds-kit audits - run via npx, no install needed. Each exits
411
- # non-zero on failures, so a bad PR fails the check.
412
- - name: Project health (doctor)
413
- run: npx --yes aem-eds-kit@latest doctor
414
- - name: Loading & LCP budget
415
- run: npx --yes aem-eds-kit@latest audit loading
416
- - name: Security & exploit scan
417
- run: npx --yes aem-eds-kit@latest audit security
418
- `;
419
- }
420
- });
421
-
422
- // src/lib/scaffold/fstab.ts
423
- import { writeFile as writeFile2 } from "fs/promises";
424
- import path4 from "path";
425
- function needsFstab(a) {
426
- return a !== "ue";
427
- }
428
- function daMountpoint(org, site) {
429
- return `https://content.da.live/${org}/${site}/`;
430
- }
431
- function fstabYaml(mountpoint) {
432
- return `mountpoints:
433
- /: ${mountpoint}
434
- `;
435
- }
436
- async function writeFstab(projectRoot, mountpoint) {
437
- await writeFile2(path4.join(projectRoot, "fstab.yaml"), fstabYaml(mountpoint));
438
- return "fstab.yaml";
439
- }
440
- function pathsJson(site) {
441
- return `${JSON.stringify(
442
- {
443
- mappings: [`/content/${site}/:/`],
444
- includes: [`/content/${site}/`]
445
- },
446
- null,
447
- 2
448
- )}
449
- `;
450
- }
451
- async function writePaths(projectRoot, site) {
452
- await writeFile2(path4.join(projectRoot, "paths.json"), pathsJson(site));
453
- return "paths.json";
454
- }
455
- var AUTHORING_NAMES;
456
- var init_fstab = __esm({
457
- "src/lib/scaffold/fstab.ts"() {
458
- "use strict";
459
- init_esm_shims();
460
- AUTHORING_NAMES = {
461
- ue: "Universal Editor (crosswalk)",
462
- da: "Document Authoring (da.live)",
463
- gdrive: "Document Authoring (Google Drive)",
464
- sharepoint: "Document Authoring (SharePoint)"
465
- };
466
- }
467
- });
468
-
469
358
  // src/lib/scaffold/blocks.ts
470
359
  function json(value) {
471
360
  return `${JSON.stringify(value, null, 2)}
@@ -479,14 +368,14 @@ var init_blocks = __esm({
479
368
  "src/lib/scaffold/blocks.ts"() {
480
369
  "use strict";
481
370
  init_esm_shims();
482
- blockDef = (title, id, model) => ({
371
+ blockDef = (title, id, model2) => ({
483
372
  title,
484
373
  id,
485
374
  plugins: {
486
375
  xwalk: {
487
376
  page: {
488
377
  resourceType: "core/franklin/components/block/v1/block",
489
- template: { name: title, model }
378
+ template: { name: title, model: model2 }
490
379
  }
491
380
  }
492
381
  }
@@ -503,14 +392,14 @@ var init_blocks = __esm({
503
392
  }
504
393
  }
505
394
  });
506
- itemDef = (title, id, model) => ({
395
+ itemDef = (title, id, model2) => ({
507
396
  title,
508
397
  id,
509
398
  plugins: {
510
399
  xwalk: {
511
400
  page: {
512
401
  resourceType: "core/franklin/components/block/v1/block/item",
513
- template: { name: title, model }
402
+ template: { name: title, model: model2 }
514
403
  }
515
404
  }
516
405
  }
@@ -872,7 +761,7 @@ function fieldReferenceFields() {
872
761
  ];
873
762
  }
874
763
  function fieldReferenceModelFile() {
875
- const model = {
764
+ const model2 = {
876
765
  definitions: [
877
766
  {
878
767
  title: "Field Reference",
@@ -890,7 +779,7 @@ function fieldReferenceModelFile() {
890
779
  models: [{ id: "field-reference", fields: fieldReferenceFields() }],
891
780
  filters: []
892
781
  };
893
- return `${JSON.stringify(model, null, 2)}
782
+ return `${JSON.stringify(model2, null, 2)}
894
783
  `;
895
784
  }
896
785
  function mergeById(existing, additions) {
@@ -913,7 +802,7 @@ function mergeDefinitions(existing, defaultContent, blockDefs) {
913
802
  }
914
803
  function mergeSectionFilter(existing, ids) {
915
804
  const filters = existing ? [...existing] : [];
916
- let section = filters.find((f) => f.id === "section");
805
+ let section = filters.find((f2) => f2.id === "section");
917
806
  if (!section) {
918
807
  section = { id: "section", components: ["text", "image", "button", "title"] };
919
808
  filters.push(section);
@@ -1044,819 +933,2089 @@ export default function decorate(block) {
1044
933
  }
1045
934
  });
1046
935
 
1047
- // src/commands/scaffold.ts
1048
- var scaffold_exports = {};
1049
- __export(scaffold_exports, {
1050
- scaffoldBlocks: () => scaffoldBlocks,
1051
- scaffoldCi: () => scaffoldCi,
1052
- scaffoldInteractive: () => scaffoldInteractive,
1053
- scaffoldUe: () => scaffoldUe
936
+ // src/lib/template/authoring.ts
937
+ import { existsSync as existsSync2 } from "fs";
938
+ import { readFile } from "fs/promises";
939
+ import path3 from "path";
940
+ import { parse as parseYaml } from "yaml";
941
+ async function detectAuthoring(projectRoot) {
942
+ const fstab = path3.join(projectRoot, "fstab.yaml");
943
+ if (existsSync2(fstab)) {
944
+ try {
945
+ const parsed = parseYaml(await readFile(fstab, "utf-8")) ?? {};
946
+ const mp = parsed.mountpoints?.["/"];
947
+ const url = typeof mp === "string" ? mp : mp?.url ?? "";
948
+ const da = url.match(/content\.da\.live\/([^/]+)\/([^/]+)/);
949
+ if (da) return { model: "da", org: da[1], site: da[2], mountpoint: url };
950
+ if (/drive\.google\.com/.test(url)) return { model: "gdrive", mountpoint: url };
951
+ if (/sharepoint\.com|-my\.sharepoint/.test(url))
952
+ return { model: "sharepoint", mountpoint: url };
953
+ return { model: "document", mountpoint: url };
954
+ } catch {
955
+ }
956
+ }
957
+ return { model: "ue" };
958
+ }
959
+ var init_authoring = __esm({
960
+ "src/lib/template/authoring.ts"() {
961
+ "use strict";
962
+ init_esm_shims();
963
+ }
964
+ });
965
+
966
+ // src/lib/template/da.ts
967
+ function daSourceUrl(org, site, path30) {
968
+ const clean = path30.replace(/^\/+/, "").replace(/\.html$/i, "");
969
+ return `https://admin.da.live/source/${org}/${site}/${clean}.html`;
970
+ }
971
+ function daEditUrl(org, site, path30) {
972
+ const clean = path30.replace(/^\/+/, "").replace(/\.html$/i, "");
973
+ return `https://da.live/edit#/${org}/${site}/${clean}`;
974
+ }
975
+ async function pushToDa(org, site, path30, html, token) {
976
+ const url = daSourceUrl(org, site, path30);
977
+ const form = new FormData();
978
+ form.append("data", new Blob([html], { type: "text/html" }), "content.html");
979
+ const headers = {};
980
+ if (token) headers.Authorization = `Bearer ${token}`;
981
+ const res = await fetch(url, { method: "POST", body: form, headers });
982
+ if (!res.ok) {
983
+ throw new Error(`DA push failed: ${res.status} ${res.statusText}`);
984
+ }
985
+ return url;
986
+ }
987
+ var init_da = __esm({
988
+ "src/lib/template/da.ts"() {
989
+ "use strict";
990
+ init_esm_shims();
991
+ }
992
+ });
993
+
994
+ // src/lib/template/page.ts
995
+ function blockDiv(name) {
996
+ return ` <div class="${name}">
997
+ <div>
998
+ <div>${name} content</div>
999
+ </div>
1000
+ </div>`;
1001
+ }
1002
+ function metadataBlock(opts) {
1003
+ const rows = [
1004
+ ` <div><div>Title</div><div>${opts.title}</div></div>`,
1005
+ ` <div><div>Description</div><div>${opts.description ?? ""}</div></div>`
1006
+ ];
1007
+ if (opts.area) rows.push(` <div><div>Template</div><div>${opts.area}</div></div>`);
1008
+ return ` <div class="metadata">
1009
+ ${rows.join("\n")}
1010
+ </div>`;
1011
+ }
1012
+ function buildPageHtml(opts) {
1013
+ const blocks = opts.blocks ?? [];
1014
+ const intro = ` <div>
1015
+ <h1>${opts.title}</h1>
1016
+ <p>${opts.description ?? "Intro copy goes here."}</p>
1017
+ </div>`;
1018
+ const blockSections = blocks.map((b) => ` <div>
1019
+ ${blockDiv(b)}
1020
+ </div>`);
1021
+ const metaSection = ` <div>
1022
+ ${metadataBlock(opts)}
1023
+ </div>`;
1024
+ return `<body>
1025
+ <header></header>
1026
+ <main>
1027
+ ${[intro, ...blockSections, metaSection].join("\n")}
1028
+ </main>
1029
+ <footer></footer>
1030
+ </body>
1031
+ `;
1032
+ }
1033
+ var init_page = __esm({
1034
+ "src/lib/template/page.ts"() {
1035
+ "use strict";
1036
+ init_esm_shims();
1037
+ }
1038
+ });
1039
+
1040
+ // src/commands/template.ts
1041
+ var template_exports = {};
1042
+ __export(template_exports, {
1043
+ templateNew: () => templateNew
1054
1044
  });
1055
1045
  import { existsSync as existsSync3 } from "fs";
1056
- import { mkdir as mkdir2, readFile, writeFile as writeFile3 } from "fs/promises";
1057
- import path5 from "path";
1058
- async function readJson(file) {
1059
- if (!existsSync3(file)) return null;
1046
+ import { mkdir, readFile as readFile2, readdir, writeFile } from "fs/promises";
1047
+ import path4 from "path";
1048
+ import chalk3 from "chalk";
1049
+ async function projectBlocks(projectRoot) {
1050
+ const dir = path4.join(projectRoot, "blocks");
1051
+ if (!existsSync3(dir)) return [];
1052
+ return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
1053
+ }
1054
+ async function readEdsrcAdmin(projectRoot) {
1055
+ const rc = path4.join(projectRoot, ".edsrc.json");
1056
+ if (!existsSync3(rc)) return {};
1060
1057
  try {
1061
- return JSON.parse(await readFile(file, "utf-8"));
1058
+ return JSON.parse(await readFile2(rc, "utf-8")).admin ?? {};
1062
1059
  } catch {
1063
- return null;
1060
+ return {};
1064
1061
  }
1065
1062
  }
1066
- async function writeJson(file, data) {
1067
- await writeFile3(file, `${JSON.stringify(data, null, 2)}
1063
+ async function registerBlocks(projectRoot, blocks) {
1064
+ if (!blocks.length) return false;
1065
+ const file = path4.join(projectRoot, "component-filters.json");
1066
+ let existing = null;
1067
+ if (existsSync3(file)) {
1068
+ try {
1069
+ existing = JSON.parse(await readFile2(file, "utf-8"));
1070
+ } catch {
1071
+ existing = null;
1072
+ }
1073
+ }
1074
+ const filters = mergeSectionFilter(existing, blocks);
1075
+ await writeFile(file, `${JSON.stringify(filters, null, 2)}
1068
1076
  `);
1077
+ return true;
1069
1078
  }
1070
- async function scaffoldUe() {
1079
+ async function templateNew(nameArg, options = {}) {
1071
1080
  const projectRoot = findProjectRoot();
1072
1081
  if (!projectRoot) {
1073
1082
  logger.error("Not inside an EDS project (no EDS project markers found).");
1074
1083
  process.exitCode = 1;
1075
1084
  return;
1076
1085
  }
1077
- logger.logoOnce(logo("Scaffold - Universal Editor"));
1078
- const created = [];
1079
- const blockDir = path5.join(projectRoot, "blocks", "field-reference");
1080
- await mkdir2(blockDir, { recursive: true });
1081
- await writeFile3(path5.join(blockDir, "field-reference.js"), FIELD_REFERENCE_JS);
1082
- await writeFile3(path5.join(blockDir, "field-reference.css"), FIELD_REFERENCE_CSS);
1083
- await writeFile3(path5.join(blockDir, "_field-reference.json"), fieldReferenceModelFile());
1084
- created.push("blocks/field-reference/ (all 17 fields + multifield)");
1085
- const defFile = path5.join(projectRoot, "component-definition.json");
1086
- const modelsFile = path5.join(projectRoot, "component-models.json");
1087
- const filtersFile = path5.join(projectRoot, "component-filters.json");
1088
- const blockDefs = [
1089
- {
1090
- title: "Field Reference",
1091
- id: "field-reference",
1092
- plugins: {
1093
- xwalk: {
1094
- page: {
1095
- resourceType: "core/franklin/components/block/v1/block",
1096
- template: { name: "Field Reference", model: "field-reference" }
1097
- }
1086
+ await logger.logoOnceAnimated("Page template");
1087
+ const { input, checkbox, confirm } = await import("@inquirer/prompts");
1088
+ const interactive = !options.yes;
1089
+ const detected = await detectAuthoring(projectRoot);
1090
+ const model2 = options.authoring ?? detected.model;
1091
+ const name = slug(nameArg);
1092
+ const title = options.title ?? (interactive ? await input({ message: "Page title", default: nameArg }) : nameArg);
1093
+ const description = options.description ?? (interactive ? await input({ message: "Description (optional)" }) : "");
1094
+ const pagePath = options.path ?? `/templates/${name}`;
1095
+ let blocks = options.blocks;
1096
+ if (!blocks && interactive) {
1097
+ const available = await projectBlocks(projectRoot);
1098
+ const choices = (available.length ? available : STANDARD_BLOCKS.map((b) => b.id)).map((b) => ({
1099
+ name: b,
1100
+ value: b
1101
+ }));
1102
+ blocks = await checkbox({ message: "Which blocks to seed into the page?", choices });
1103
+ }
1104
+ const seeded = blocks ?? [];
1105
+ const html = buildPageHtml({
1106
+ title,
1107
+ description: description || void 0,
1108
+ blocks: seeded,
1109
+ area: options.area
1110
+ });
1111
+ const localRel = path4.join("templates", `${name}.html`);
1112
+ await mkdir(path4.join(projectRoot, "templates"), { recursive: true });
1113
+ await writeFile(path4.join(projectRoot, localRel), html);
1114
+ logger.info(heading("Page template", `${pagePath} \xB7 ${model2}`));
1115
+ logger.info(accentLine(localRel, `${seeded.length} block(s) + metadata`));
1116
+ if (model2 === "da") {
1117
+ const admin = await readEdsrcAdmin(projectRoot);
1118
+ const org = options.org ?? admin.org ?? detected.org;
1119
+ const site = options.site ?? admin.site ?? detected.site;
1120
+ const wantPush = options.push ?? (interactive ? await confirm({ message: "Push to DA (da.live)?", default: true }) : true);
1121
+ if (wantPush) {
1122
+ if (!org || !site) {
1123
+ logger.warn("No DA org/site. Pass --org/--site or set admin in .edsrc.json.");
1124
+ } else {
1125
+ try {
1126
+ await pushToDa(org, site, pagePath, html, process.env.DA_TOKEN);
1127
+ logger.info(accentLine("pushed to DA", daEditUrl(org, site, pagePath)));
1128
+ } catch (err) {
1129
+ logger.error(`DA push failed: ${err.message}`);
1130
+ logger.info(chalk3.dim(" Open projects allow anonymous writes; others need DA_TOKEN."));
1098
1131
  }
1099
1132
  }
1100
1133
  }
1101
- ];
1102
- const definitions = mergeDefinitions(
1103
- await readJson(defFile),
1104
- DEFAULT_CONTENT_DEFINITIONS,
1105
- blockDefs
1106
- );
1107
- await writeJson(defFile, definitions);
1108
- created.push("component-definition.json (default content + field-reference)");
1109
- const existingModels = await readJson(modelsFile) ?? [];
1110
- const fieldRefModel = JSON.parse(fieldReferenceModelFile()).models[0];
1111
- const models = mergeById(existingModels, [...DEFAULT_CONTENT_MODELS, fieldRefModel]);
1112
- await writeJson(modelsFile, models);
1113
- created.push("component-models.json (title / image / button / field-reference)");
1114
- const filters = mergeSectionFilter(await readJson(filtersFile), ["field-reference"]);
1115
- await writeJson(filtersFile, filters);
1116
- created.push("component-filters.json (section filter)");
1117
- logger.info(heading("Created / merged"));
1118
- const names = created.map((c) => c.split(" ")[0]);
1119
- const w = columnWidth(names);
1120
- for (const c of created) {
1121
- const [name, ...rest] = c.split(" ");
1122
- logger.info(accentLine(name, rest.join(" ").replace(/^\((.*)\)$/, "$1"), w));
1123
- }
1124
- logger.info("");
1125
- logger.info(box(["Universal Editor scaffold ready"]));
1126
- }
1127
- async function scaffoldBlocks(names) {
1128
- const projectRoot = findProjectRoot();
1129
- if (!projectRoot) {
1130
- logger.error("Not inside an EDS project (no EDS project markers found).");
1131
- process.exitCode = 1;
1132
- return;
1133
- }
1134
- logger.logoOnce(logo("Scaffold \u2014 standard blocks"));
1135
- let selected;
1136
- if (names?.length) {
1137
- selected = names.map((n) => blockById(n.trim())).filter((b) => !!b);
1138
- const unknown = names.filter((n) => !blockById(n.trim()));
1139
- if (unknown.length) {
1140
- logger.warn(
1141
- `Unknown block(s): ${unknown.join(", ")}. Available: ${STANDARD_BLOCKS.map((b) => b.id).join(", ")}`
1134
+ } else if (model2 === "gdrive" || model2 === "sharepoint") {
1135
+ const where = model2 === "gdrive" ? "a Google Doc" : "a Word doc";
1136
+ logger.info("");
1137
+ logger.info(
1138
+ chalk3.dim(` Document model: import ${localRel} into ${where} in your mounted folder,`)
1139
+ );
1140
+ logger.info(
1141
+ chalk3.dim(" then preview/publish. (Share the folder + install aem-code-sync first.)")
1142
+ );
1143
+ } else if (model2 === "ue") {
1144
+ const registered = await registerBlocks(projectRoot, seeded);
1145
+ if (registered) {
1146
+ logger.info(
1147
+ accentLine("component-filters.json", "seeded blocks allowed in Universal Editor")
1142
1148
  );
1143
1149
  }
1144
- } else {
1145
- selected = STANDARD_BLOCKS;
1146
- }
1147
- const createdIds = [];
1148
- const skipped = [];
1149
- for (const b of selected) {
1150
- const dir = path5.join(projectRoot, "blocks", b.id);
1151
- if (existsSync3(dir)) {
1152
- skipped.push(b.id);
1153
- continue;
1154
- }
1155
- await mkdir2(dir, { recursive: true });
1156
- await writeFile3(path5.join(dir, `${b.id}.js`), b.js);
1157
- await writeFile3(path5.join(dir, `${b.id}.css`), b.css);
1158
- await writeFile3(path5.join(dir, `_${b.id}.json`), b.model);
1159
- createdIds.push(b.id);
1150
+ logger.info("");
1151
+ logger.info(
1152
+ chalk3.dim(" Universal Editor: create the page's editable template in AEM and author it")
1153
+ );
1154
+ logger.info(chalk3.dim(` in the Universal Editor; ${localRel} is the reference structure.`));
1160
1155
  }
1161
- const filtersFile = path5.join(projectRoot, "component-filters.json");
1162
- const filters = mergeSectionFilter(
1163
- await readJson(filtersFile),
1164
- selected.map((b) => b.id)
1165
- );
1166
- await writeJson(filtersFile, filters);
1167
- logger.info(heading("Blocks"));
1168
- const w = columnWidth([...createdIds, ...skipped, "component-filters"]);
1169
- for (const id of createdIds) logger.info(accentLine(id, "created", w));
1170
- for (const id of skipped) logger.info(statusLine("warn", id, "already exists \u2014 skipped", w));
1171
1156
  logger.info("");
1172
- logger.info(box([`${createdIds.length} block(s) scaffolded + registered`]));
1157
+ logger.info(box([`"${name}" template (${model2}) - preview with \`eds preview ${pagePath}\``]));
1173
1158
  }
1174
- async function scaffoldCi() {
1175
- const projectRoot = findProjectRoot();
1176
- if (!projectRoot) {
1177
- logger.error("Not inside an EDS project (no EDS project markers found).");
1178
- process.exitCode = 1;
1179
- return;
1180
- }
1181
- logger.logoOnce(logo("Scaffold - CI workflow"));
1182
- const res = await writeCiWorkflow(projectRoot);
1183
- logger.info(heading("GitHub Actions"));
1184
- if (res.created) {
1185
- logger.info(accentLine(res.path, "doctor + audit loading + audit security + lint/build"));
1186
- } else {
1187
- logger.info(statusLine("warn", res.path, "already exists - skipped"));
1159
+ var slug;
1160
+ var init_template = __esm({
1161
+ "src/commands/template.ts"() {
1162
+ "use strict";
1163
+ init_esm_shims();
1164
+ init_logger();
1165
+ init_project();
1166
+ init_blocks();
1167
+ init_ue();
1168
+ init_authoring();
1169
+ init_da();
1170
+ init_page();
1171
+ init_ui();
1172
+ slug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-/]+/g, "-").replace(/^-+|-+$/g, "");
1188
1173
  }
1189
- logger.info("");
1174
+ });
1175
+
1176
+ // src/lib/scaffold/ci.ts
1177
+ import { existsSync as existsSync4 } from "fs";
1178
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
1179
+ import path5 from "path";
1180
+ async function writeCiWorkflow(projectRoot) {
1181
+ const file = path5.join(projectRoot, ".github", "workflows", "eds.yml");
1182
+ if (existsSync4(file)) return { path: CI_WORKFLOW_PATH, created: false };
1183
+ await mkdir2(path5.dirname(file), { recursive: true });
1184
+ await writeFile2(file, CI_WORKFLOW);
1185
+ return { path: CI_WORKFLOW_PATH, created: true };
1186
+ }
1187
+ var CI_WORKFLOW_PATH, CI_WORKFLOW;
1188
+ var init_ci = __esm({
1189
+ "src/lib/scaffold/ci.ts"() {
1190
+ "use strict";
1191
+ init_esm_shims();
1192
+ CI_WORKFLOW_PATH = ".github/workflows/eds.yml";
1193
+ CI_WORKFLOW = `name: EDS checks
1194
+
1195
+ # Generated by aem-eds-kit (\`eds scaffold ci\`). Runs the project's own
1196
+ # lint/build plus the eds audits as a gatekeeper on every PR and push to main.
1197
+ on:
1198
+ pull_request:
1199
+ push:
1200
+ branches: [main]
1201
+
1202
+ jobs:
1203
+ checks:
1204
+ runs-on: ubuntu-latest
1205
+ steps:
1206
+ - uses: actions/checkout@v4
1207
+ - uses: actions/setup-node@v4
1208
+ with:
1209
+ node-version: 20
1210
+
1211
+ # Install project dependencies when the repo has them.
1212
+ - name: Install dependencies
1213
+ run: |
1214
+ if [ -f package-lock.json ]; then
1215
+ npm ci
1216
+ elif [ -f package.json ]; then
1217
+ npm install
1218
+ fi
1219
+
1220
+ # The project's own scripts, if defined (skipped otherwise).
1221
+ - name: Lint
1222
+ if: hashFiles('package.json') != ''
1223
+ run: npm run lint --if-present
1224
+ - name: Build
1225
+ if: hashFiles('package.json') != ''
1226
+ run: npm run build --if-present
1227
+
1228
+ # aem-eds-kit audits - run via npx, no install needed. Each exits
1229
+ # non-zero on failures, so a bad PR fails the check.
1230
+ - name: Project health (doctor)
1231
+ run: npx --yes aem-eds-kit@latest doctor
1232
+ - name: Loading & LCP budget
1233
+ run: npx --yes aem-eds-kit@latest audit loading
1234
+ - name: Security & exploit scan
1235
+ run: npx --yes aem-eds-kit@latest audit security
1236
+ `;
1237
+ }
1238
+ });
1239
+
1240
+ // src/lib/scaffold/fstab.ts
1241
+ import { writeFile as writeFile3 } from "fs/promises";
1242
+ import path6 from "path";
1243
+ function needsFstab(a) {
1244
+ return a !== "ue";
1245
+ }
1246
+ function daMountpoint(org, site) {
1247
+ return `https://content.da.live/${org}/${site}/`;
1248
+ }
1249
+ function fstabYaml(mountpoint) {
1250
+ return `mountpoints:
1251
+ /: ${mountpoint}
1252
+ `;
1253
+ }
1254
+ async function writeFstab(projectRoot, mountpoint) {
1255
+ await writeFile3(path6.join(projectRoot, "fstab.yaml"), fstabYaml(mountpoint));
1256
+ return "fstab.yaml";
1257
+ }
1258
+ function pathsJson(site) {
1259
+ return `${JSON.stringify(
1260
+ {
1261
+ mappings: [`/content/${site}/:/`],
1262
+ includes: [`/content/${site}/`]
1263
+ },
1264
+ null,
1265
+ 2
1266
+ )}
1267
+ `;
1268
+ }
1269
+ async function writePaths(projectRoot, site) {
1270
+ await writeFile3(path6.join(projectRoot, "paths.json"), pathsJson(site));
1271
+ return "paths.json";
1272
+ }
1273
+ var AUTHORING_NAMES;
1274
+ var init_fstab = __esm({
1275
+ "src/lib/scaffold/fstab.ts"() {
1276
+ "use strict";
1277
+ init_esm_shims();
1278
+ AUTHORING_NAMES = {
1279
+ ue: "Universal Editor (crosswalk)",
1280
+ da: "Document Authoring (da.live)",
1281
+ gdrive: "Document Authoring (Google Drive)",
1282
+ sharepoint: "Document Authoring (SharePoint)"
1283
+ };
1284
+ }
1285
+ });
1286
+
1287
+ // src/lib/scaffold/helpers.ts
1288
+ import { existsSync as existsSync5 } from "fs";
1289
+ import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
1290
+ import path7 from "path";
1291
+ function helperById(id) {
1292
+ return HELPERS.find((h) => h.id === id);
1293
+ }
1294
+ function resolveHelpers(ids) {
1295
+ const want = /* @__PURE__ */ new Set();
1296
+ const add = (id) => {
1297
+ if (want.has(id)) return;
1298
+ const h = helperById(id);
1299
+ if (!h) return;
1300
+ for (const dep of h.deps ?? []) add(dep);
1301
+ want.add(id);
1302
+ };
1303
+ for (const id of ids) add(id);
1304
+ return HELPERS.filter((h) => want.has(h.id)).map((h) => h.id);
1305
+ }
1306
+ function buildHelpers(ids) {
1307
+ const resolved = resolveHelpers(ids);
1308
+ const body = resolved.map((id) => helperById(id)).filter((h) => !!h).map((h) => h.code).join("\n\n");
1309
+ return `// scripts/utils.js - common EDS helpers (generated by \`eds scaffold helpers\`).
1310
+
1311
+ ${body}
1312
+ `;
1313
+ }
1314
+ async function writeHelpers(projectRoot, ids = ALL_HELPER_IDS) {
1315
+ const resolved = resolveHelpers(ids);
1316
+ const file = path7.join(projectRoot, "scripts", "utils.js");
1317
+ if (existsSync5(file)) return { path: HELPERS_PATH, created: false, helpers: resolved };
1318
+ await mkdir3(path7.dirname(file), { recursive: true });
1319
+ await writeFile4(file, buildHelpers(ids));
1320
+ return { path: HELPERS_PATH, created: true, helpers: resolved };
1321
+ }
1322
+ var HELPERS_PATH, HELPERS, ALL_HELPER_IDS;
1323
+ var init_helpers = __esm({
1324
+ "src/lib/scaffold/helpers.ts"() {
1325
+ "use strict";
1326
+ init_esm_shims();
1327
+ HELPERS_PATH = "scripts/utils.js";
1328
+ HELPERS = [
1329
+ {
1330
+ id: "getMetadata",
1331
+ label: "getMetadata (read a page meta value)",
1332
+ code: `/**
1333
+ * Reads a page metadata value by name (meta[name] or meta[property]).
1334
+ * @param {string} name The metadata name.
1335
+ * @returns {string} The value, or an empty string.
1336
+ */
1337
+ export function getMetadata(name) {
1338
+ const meta =
1339
+ document.querySelector(\`meta[name="\${name}"]\`) ||
1340
+ document.querySelector(\`meta[property="\${name}"]\`);
1341
+ return meta ? meta.content : '';
1342
+ }`
1343
+ },
1344
+ {
1345
+ id: "toCamelCase",
1346
+ label: "toCamelCase (dash/underscore/space -> camelCase)",
1347
+ code: `/**
1348
+ * Converts a string to camelCase, handling dashes, underscores and spaces.
1349
+ * @param {string} value The string to convert.
1350
+ * @returns {string} The camelCased string.
1351
+ */
1352
+ export function toCamelCase(value) {
1353
+ return String(value || '')
1354
+ .trim()
1355
+ .toLowerCase()
1356
+ .replace(/[-_\\s]+(.)/g, (_, char) => char.toUpperCase());
1357
+ }`
1358
+ },
1359
+ {
1360
+ id: "getLanguageRootPath",
1361
+ label: "getLanguageRootPath (e.g. /en, /en-us)",
1362
+ code: `/**
1363
+ * Returns the language root of a path, e.g. '/en' or '/en-us' ('' if none).
1364
+ * @param {string} path The page path.
1365
+ * @returns {string} The language root prefix.
1366
+ */
1367
+ export function getLanguageRootPath(path) {
1368
+ const match = String(path || '').match(/^\\/[a-z]{2}(?:-[a-z]{2})?(?=\\/|$)/i);
1369
+ return match ? match[0] : '';
1370
+ }`
1371
+ },
1372
+ {
1373
+ id: "getPagePath",
1374
+ label: "getPagePath (path without content root)",
1375
+ deps: ["getLanguageRootPath"],
1376
+ code: `// Content root stripped from page paths. Set this if your site is mounted under
1377
+ // a sub-path (e.g. '/content/my-site'); leave empty for a root-mounted site.
1378
+ const ROOT_PATH = '';
1379
+
1380
+ /**
1381
+ * Returns the page path without the content root; \`clean\` drops the language root.
1382
+ * @param {boolean} [clean=false] Also strip the language root.
1383
+ * @returns {string} The page path or 'NA' for the language root.
1384
+ */
1385
+ export function getPagePath({ clean = false } = {}) {
1386
+ const path = window.location.pathname.replace(ROOT_PATH, '').replace(/\\.html$/i, '');
1387
+
1388
+ if (clean) {
1389
+ const langRoot = getLanguageRootPath(path);
1390
+ const cleaned = path.startsWith(langRoot) ? path.slice(langRoot.length) : path;
1391
+ return cleaned || 'NA';
1392
+ }
1393
+
1394
+ return path || 'NA';
1395
+ }`
1396
+ },
1397
+ {
1398
+ id: "getSiteArea",
1399
+ label: "getSiteArea (blog / products / home)",
1400
+ deps: ["getPagePath"],
1401
+ code: `/**
1402
+ * Returns the top-level area of the site from the current path - e.g. 'blog' or
1403
+ * 'products', and 'home' at the site root. Uses the language-cleaned path, so you
1404
+ * can tell whether you're on the blog or somewhere else.
1405
+ * @returns {string} The site area.
1406
+ */
1407
+ export function getSiteArea() {
1408
+ const path = getPagePath({ clean: true });
1409
+ if (!path || path === 'NA') return 'home';
1410
+ return path.split('/').filter(Boolean)[0] || 'home';
1411
+ }`
1412
+ },
1413
+ {
1414
+ id: "getEnvironment",
1415
+ label: "getEnvironment (dev / stage / prod)",
1416
+ code: `/**
1417
+ * Detects the current environment from the hostname.
1418
+ * @returns {'NA'|'dev'|'stage'|'prod'} The environment name.
1419
+ */
1420
+ export function getEnvironment() {
1421
+ const { hostname } = window.location;
1422
+ if (hostname.includes('author')) return 'NA';
1423
+ if (hostname === 'localhost') return 'dev';
1424
+ const repo = hostname.match(/^[^.]+--([^.]+)--[^.]+\\.aem\\.(?:page|live)$/)?.[1] || '';
1425
+ if (repo.includes('dev')) return 'dev';
1426
+ if (repo.includes('stage')) return 'stage';
1427
+ return 'prod';
1428
+ }`
1429
+ },
1430
+ {
1431
+ id: "getContentTopic",
1432
+ label: "getContentTopic (cq-tags content-topic values)",
1433
+ deps: ["getMetadata"],
1434
+ code: `/**
1435
+ * Reads all content-topic values from the page's cq-tags metadata.
1436
+ * @returns {string[]} The topics (part after \`content-topic/\`), empty if none.
1437
+ */
1438
+ export function getContentTopic() {
1439
+ return getMetadata('cq-tags')
1440
+ .split(',')
1441
+ .map((t) => t.trim())
1442
+ .filter((t) => t.includes('content-topic/'))
1443
+ .map((t) => t.split('content-topic/')[1]);
1444
+ }`
1445
+ },
1446
+ {
1447
+ id: "isUEEdit",
1448
+ label: "isUEEdit (Universal Editor edit mode)",
1449
+ code: `/**
1450
+ * Checks whether the current page is opened in the Universal Editor Edit mode.
1451
+ * @returns {boolean} True in Universal Editor Edit mode.
1452
+ */
1453
+ export function isUEEdit() {
1454
+ return document.documentElement.classList.contains('adobe-ue-edit');
1455
+ }`
1456
+ },
1457
+ {
1458
+ id: "isUEPreview",
1459
+ label: "isUEPreview (Universal Editor preview mode)",
1460
+ code: `/**
1461
+ * Checks whether the current page is opened in the Universal Editor Preview mode.
1462
+ * @returns {boolean} True in Universal Editor Preview mode.
1463
+ */
1464
+ export function isUEPreview() {
1465
+ return document.documentElement.classList.contains('adobe-ue-preview');
1466
+ }`
1467
+ },
1468
+ {
1469
+ id: "isUE",
1470
+ label: "isUE (Universal Editor, any mode)",
1471
+ deps: ["isUEEdit", "isUEPreview"],
1472
+ code: `/**
1473
+ * Checks whether the current page is opened in the Universal Editor (any mode).
1474
+ * @returns {boolean} True in the Universal Editor.
1475
+ */
1476
+ export function isUE() {
1477
+ return isUEEdit() || isUEPreview();
1478
+ }`
1479
+ }
1480
+ ];
1481
+ ALL_HELPER_IDS = HELPERS.map((h) => h.id);
1482
+ }
1483
+ });
1484
+
1485
+ // src/commands/scaffold.ts
1486
+ var scaffold_exports = {};
1487
+ __export(scaffold_exports, {
1488
+ scaffoldBlocks: () => scaffoldBlocks,
1489
+ scaffoldCi: () => scaffoldCi,
1490
+ scaffoldHelpers: () => scaffoldHelpers,
1491
+ scaffoldInteractive: () => scaffoldInteractive,
1492
+ scaffoldUe: () => scaffoldUe
1493
+ });
1494
+ import { existsSync as existsSync6 } from "fs";
1495
+ import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile5 } from "fs/promises";
1496
+ import path8 from "path";
1497
+ async function readJson(file) {
1498
+ if (!existsSync6(file)) return null;
1499
+ try {
1500
+ return JSON.parse(await readFile3(file, "utf-8"));
1501
+ } catch {
1502
+ return null;
1503
+ }
1504
+ }
1505
+ async function writeJson(file, data) {
1506
+ await writeFile5(file, `${JSON.stringify(data, null, 2)}
1507
+ `);
1508
+ }
1509
+ async function scaffoldUe() {
1510
+ const projectRoot = findProjectRoot();
1511
+ if (!projectRoot) {
1512
+ logger.error("Not inside an EDS project (no EDS project markers found).");
1513
+ process.exitCode = 1;
1514
+ return;
1515
+ }
1516
+ logger.logoOnce(logo("Scaffold - Universal Editor"));
1517
+ const created = [];
1518
+ const blockDir = path8.join(projectRoot, "blocks", "field-reference");
1519
+ await mkdir4(blockDir, { recursive: true });
1520
+ await writeFile5(path8.join(blockDir, "field-reference.js"), FIELD_REFERENCE_JS);
1521
+ await writeFile5(path8.join(blockDir, "field-reference.css"), FIELD_REFERENCE_CSS);
1522
+ await writeFile5(path8.join(blockDir, "_field-reference.json"), fieldReferenceModelFile());
1523
+ created.push("blocks/field-reference/ (all 17 fields + multifield)");
1524
+ const defFile = path8.join(projectRoot, "component-definition.json");
1525
+ const modelsFile = path8.join(projectRoot, "component-models.json");
1526
+ const filtersFile = path8.join(projectRoot, "component-filters.json");
1527
+ const blockDefs = [
1528
+ {
1529
+ title: "Field Reference",
1530
+ id: "field-reference",
1531
+ plugins: {
1532
+ xwalk: {
1533
+ page: {
1534
+ resourceType: "core/franklin/components/block/v1/block",
1535
+ template: { name: "Field Reference", model: "field-reference" }
1536
+ }
1537
+ }
1538
+ }
1539
+ }
1540
+ ];
1541
+ const definitions = mergeDefinitions(
1542
+ await readJson(defFile),
1543
+ DEFAULT_CONTENT_DEFINITIONS,
1544
+ blockDefs
1545
+ );
1546
+ await writeJson(defFile, definitions);
1547
+ created.push("component-definition.json (default content + field-reference)");
1548
+ const existingModels = await readJson(modelsFile) ?? [];
1549
+ const fieldRefModel = JSON.parse(fieldReferenceModelFile()).models[0];
1550
+ const models = mergeById(existingModels, [...DEFAULT_CONTENT_MODELS, fieldRefModel]);
1551
+ await writeJson(modelsFile, models);
1552
+ created.push("component-models.json (title / image / button / field-reference)");
1553
+ const filters = mergeSectionFilter(await readJson(filtersFile), ["field-reference"]);
1554
+ await writeJson(filtersFile, filters);
1555
+ created.push("component-filters.json (section filter)");
1556
+ logger.info(heading("Created / merged"));
1557
+ const names = created.map((c) => c.split(" ")[0]);
1558
+ const w = columnWidth(names);
1559
+ for (const c of created) {
1560
+ const [name, ...rest] = c.split(" ");
1561
+ logger.info(accentLine(name, rest.join(" ").replace(/^\((.*)\)$/, "$1"), w));
1562
+ }
1563
+ logger.info("");
1564
+ logger.info(box(["Universal Editor scaffold ready"]));
1565
+ }
1566
+ async function scaffoldBlocks(names) {
1567
+ const projectRoot = findProjectRoot();
1568
+ if (!projectRoot) {
1569
+ logger.error("Not inside an EDS project (no EDS project markers found).");
1570
+ process.exitCode = 1;
1571
+ return;
1572
+ }
1573
+ logger.logoOnce(logo("Scaffold \u2014 standard blocks"));
1574
+ let selected;
1575
+ if (names?.length) {
1576
+ selected = names.map((n2) => blockById(n2.trim())).filter((b) => !!b);
1577
+ const unknown = names.filter((n2) => !blockById(n2.trim()));
1578
+ if (unknown.length) {
1579
+ logger.warn(
1580
+ `Unknown block(s): ${unknown.join(", ")}. Available: ${STANDARD_BLOCKS.map((b) => b.id).join(", ")}`
1581
+ );
1582
+ }
1583
+ } else {
1584
+ selected = STANDARD_BLOCKS;
1585
+ }
1586
+ const createdIds = [];
1587
+ const skipped = [];
1588
+ for (const b of selected) {
1589
+ const dir = path8.join(projectRoot, "blocks", b.id);
1590
+ if (existsSync6(dir)) {
1591
+ skipped.push(b.id);
1592
+ continue;
1593
+ }
1594
+ await mkdir4(dir, { recursive: true });
1595
+ await writeFile5(path8.join(dir, `${b.id}.js`), b.js);
1596
+ await writeFile5(path8.join(dir, `${b.id}.css`), b.css);
1597
+ await writeFile5(path8.join(dir, `_${b.id}.json`), b.model);
1598
+ createdIds.push(b.id);
1599
+ }
1600
+ const filtersFile = path8.join(projectRoot, "component-filters.json");
1601
+ const filters = mergeSectionFilter(
1602
+ await readJson(filtersFile),
1603
+ selected.map((b) => b.id)
1604
+ );
1605
+ await writeJson(filtersFile, filters);
1606
+ logger.info(heading("Blocks"));
1607
+ const w = columnWidth([...createdIds, ...skipped, "component-filters"]);
1608
+ for (const id of createdIds) logger.info(accentLine(id, "created", w));
1609
+ for (const id of skipped) logger.info(statusLine("warn", id, "already exists \u2014 skipped", w));
1610
+ logger.info("");
1611
+ logger.info(box([`${createdIds.length} block(s) scaffolded + registered`]));
1612
+ }
1613
+ async function scaffoldCi() {
1614
+ const projectRoot = findProjectRoot();
1615
+ if (!projectRoot) {
1616
+ logger.error("Not inside an EDS project (no EDS project markers found).");
1617
+ process.exitCode = 1;
1618
+ return;
1619
+ }
1620
+ logger.logoOnce(logo("Scaffold - CI workflow"));
1621
+ const res = await writeCiWorkflow(projectRoot);
1622
+ logger.info(heading("GitHub Actions"));
1623
+ if (res.created) {
1624
+ logger.info(accentLine(res.path, "doctor + audit loading + audit security + lint/build"));
1625
+ } else {
1626
+ logger.info(statusLine("warn", res.path, "already exists - skipped"));
1627
+ }
1628
+ logger.info("");
1190
1629
  logger.info(
1191
1630
  box([
1192
1631
  res.created ? "CI workflow scaffolded - commit it and it runs on every PR" : "CI workflow left untouched"
1193
1632
  ])
1194
1633
  );
1195
1634
  }
1196
- async function scaffoldInteractive() {
1197
- const { select, checkbox } = await import("@inquirer/prompts");
1198
- logger.logoOnce(logo("Scaffold"));
1199
- const what = await select({
1200
- message: "What do you want to scaffold?",
1201
- choices: [
1202
- { name: "Universal Editor config + field-reference (all 17 field types)", value: "ue" },
1203
- { name: "Standard blocks (hero, cards, columns, accordion, embed)", value: "blocks" },
1204
- { name: "GitHub Actions CI (doctor + audits + lint/build)", value: "ci" },
1205
- { name: "UE config + standard blocks", value: "both" }
1206
- ]
1635
+ async function scaffoldHelpers(ids) {
1636
+ const projectRoot = findProjectRoot();
1637
+ if (!projectRoot) {
1638
+ logger.error("Not inside an EDS project (no EDS project markers found).");
1639
+ process.exitCode = 1;
1640
+ return;
1641
+ }
1642
+ logger.logoOnce(logo("Scaffold - helpers"));
1643
+ let selected = ids?.length ? ids : void 0;
1644
+ if (!selected) {
1645
+ if (process.stdout.isTTY) {
1646
+ const { checkbox } = await import("@inquirer/prompts");
1647
+ selected = await checkbox({
1648
+ message: "Which helpers?",
1649
+ choices: HELPERS.map((h) => ({ name: h.label, value: h.id, checked: true }))
1650
+ });
1651
+ } else {
1652
+ selected = ALL_HELPER_IDS;
1653
+ }
1654
+ }
1655
+ if (!selected.length) {
1656
+ logger.info("Nothing selected.");
1657
+ return;
1658
+ }
1659
+ const res = await writeHelpers(projectRoot, selected);
1660
+ logger.info(heading("Helpers"));
1661
+ if (res.created) {
1662
+ logger.info(accentLine(res.path, res.helpers.join(", ")));
1663
+ } else {
1664
+ logger.info(statusLine("warn", res.path, "already exists - skipped"));
1665
+ }
1666
+ logger.info("");
1667
+ logger.info(
1668
+ box([
1669
+ res.created ? `${res.helpers.length} helper(s) -> scripts/utils.js` : "Helpers left untouched"
1670
+ ])
1671
+ );
1672
+ }
1673
+ async function scaffoldInteractive() {
1674
+ const { select, checkbox } = await import("@inquirer/prompts");
1675
+ logger.logoOnce(logo("Scaffold"));
1676
+ const what = await select({
1677
+ message: "What do you want to scaffold?",
1678
+ choices: [
1679
+ { name: "Universal Editor config + field-reference (all 17 field types)", value: "ue" },
1680
+ { name: "Standard blocks (hero, cards, columns, accordion, embed)", value: "blocks" },
1681
+ { name: "GitHub Actions CI (doctor + audits + lint/build)", value: "ci" },
1682
+ { name: "Helper functions (scripts/utils.js)", value: "helpers" },
1683
+ { name: "UE config + standard blocks", value: "both" }
1684
+ ]
1685
+ });
1686
+ if (what === "ue" || what === "both") await scaffoldUe();
1687
+ if (what === "blocks" || what === "both") {
1688
+ const picked = await checkbox({
1689
+ message: "Which blocks?",
1690
+ choices: STANDARD_BLOCKS.map((b) => ({ name: b.title, value: b.id, checked: true }))
1691
+ });
1692
+ await scaffoldBlocks(picked);
1693
+ }
1694
+ if (what === "ci") await scaffoldCi();
1695
+ if (what === "helpers") await scaffoldHelpers();
1696
+ }
1697
+ var init_scaffold = __esm({
1698
+ "src/commands/scaffold.ts"() {
1699
+ "use strict";
1700
+ init_esm_shims();
1701
+ init_logger();
1702
+ init_project();
1703
+ init_blocks();
1704
+ init_ci();
1705
+ init_helpers();
1706
+ init_ue();
1707
+ init_ui();
1708
+ }
1709
+ });
1710
+
1711
+ // src/commands/init.ts
1712
+ var init_exports = {};
1713
+ __export(init_exports, {
1714
+ initProject: () => initProject
1715
+ });
1716
+ import { existsSync as existsSync7 } from "fs";
1717
+ import { readFile as readFile4, writeFile as writeFile6 } from "fs/promises";
1718
+ import path9 from "path";
1719
+ import chalk4 from "chalk";
1720
+ async function setPackageName(root, name) {
1721
+ const file = path9.join(root, "package.json");
1722
+ if (!existsSync7(file)) return false;
1723
+ try {
1724
+ const pkg = JSON.parse(await readFile4(file, "utf-8"));
1725
+ pkg.name = name;
1726
+ await writeFile6(file, `${JSON.stringify(pkg, null, 2)}
1727
+ `);
1728
+ return true;
1729
+ } catch {
1730
+ return false;
1731
+ }
1732
+ }
1733
+ async function replacePlaceholders(root, name) {
1734
+ const touched = [];
1735
+ for (const rel of ["README.md", "index.html", "head.html"]) {
1736
+ const file = path9.join(root, rel);
1737
+ if (!existsSync7(file)) continue;
1738
+ try {
1739
+ const before = await readFile4(file, "utf-8");
1740
+ const after = before.replace(/aem-boilerplate/gi, name);
1741
+ if (after !== before) {
1742
+ await writeFile6(file, after);
1743
+ touched.push(rel);
1744
+ }
1745
+ } catch {
1746
+ }
1747
+ }
1748
+ return touched;
1749
+ }
1750
+ async function externalSteps(authoring) {
1751
+ const { confirm } = await import("@inquirer/prompts");
1752
+ const steps = [
1753
+ {
1754
+ title: "Install the AEM Code Sync GitHub app",
1755
+ url: "https://github.com/apps/aem-code-sync/installations/new",
1756
+ note: "Connects your repo to the Edge Delivery pipeline (preview/publish)."
1757
+ }
1758
+ ];
1759
+ if (authoring === "gdrive") {
1760
+ steps.push({
1761
+ title: "Share your content folder",
1762
+ note: "Share the Google Drive folder with the AEM service and set it as the mountpoint."
1763
+ });
1764
+ }
1765
+ if (authoring === "sharepoint") {
1766
+ steps.push({
1767
+ title: "Share your content folder",
1768
+ note: "Grant the SharePoint/OneDrive folder to the AEM service account."
1769
+ });
1770
+ }
1771
+ if (authoring === "da") {
1772
+ steps.push({
1773
+ title: "Author in DA",
1774
+ url: "https://da.live",
1775
+ note: "Create documents at da.live."
1776
+ });
1777
+ }
1778
+ if (authoring === "ue") {
1779
+ steps.push({
1780
+ title: "Open the Universal Editor",
1781
+ url: "https://experience.adobe.com",
1782
+ note: "Author in-context once the repo is connected to AEM."
1783
+ });
1784
+ }
1785
+ steps.push({
1786
+ title: "Install the AEM Sidekick extension",
1787
+ url: "https://chromewebstore.google.com/detail/aem-sidekick/igkmdomcgoebiipaifhmpfjhbjccggml",
1788
+ note: "Preview and publish from the browser."
1789
+ });
1790
+ steps.push({
1791
+ title: "Run the local dev server",
1792
+ note: "npm install, then `aem up` (from @adobe/aem-cli)."
1793
+ });
1794
+ logger.info(heading("Next steps (outside the repo)"));
1795
+ for (let i = 0; i < steps.length; i++) {
1796
+ const s = steps[i];
1797
+ logger.info(` ${brand.accent(String(i + 1))} ${chalk4.bold(s.title)}`);
1798
+ if (s.url) logger.info(` ${chalk4.cyan(s.url)}`);
1799
+ logger.info(` ${chalk4.dim(s.note)}`);
1800
+ await confirm({ message: " Done / skip?", default: true });
1801
+ }
1802
+ }
1803
+ async function initProject(options = {}) {
1804
+ const projectRoot = findProjectRoot();
1805
+ if (!projectRoot) {
1806
+ logger.error(
1807
+ "Not inside an EDS project (no EDS project markers found). Clone an EDS boilerplate first, then run this from its root."
1808
+ );
1809
+ process.exitCode = 1;
1810
+ return;
1811
+ }
1812
+ await logger.logoOnceAnimated("Project setup");
1813
+ const { input, select, confirm } = await import("@inquirer/prompts");
1814
+ const interactive = !options.yes;
1815
+ let defaultName = path9.basename(projectRoot);
1816
+ const pkgFile = path9.join(projectRoot, "package.json");
1817
+ if (existsSync7(pkgFile)) {
1818
+ try {
1819
+ defaultName = JSON.parse(await readFile4(pkgFile, "utf-8")).name || defaultName;
1820
+ } catch {
1821
+ }
1822
+ }
1823
+ const name = options.name && slug2(options.name) || slug2(
1824
+ await input({
1825
+ message: "1/4 Project name",
1826
+ default: slug2(defaultName),
1827
+ validate: (v) => !!slug2(v) || "Use letters, numbers and dashes"
1828
+ })
1829
+ );
1830
+ const authoring = options.authoring ?? await select({
1831
+ message: "2/4 Authoring model",
1832
+ choices: [
1833
+ { name: AUTHORING_NAMES.ue, value: "ue" },
1834
+ { name: AUTHORING_NAMES.da, value: "da" },
1835
+ { name: AUTHORING_NAMES.gdrive, value: "gdrive" },
1836
+ { name: AUTHORING_NAMES.sharepoint, value: "sharepoint" }
1837
+ ]
1838
+ });
1839
+ let mountpoint = options.mountpoint;
1840
+ if (needsFstab(authoring) && !mountpoint) {
1841
+ if (authoring === "da" && (options.org || interactive)) {
1842
+ const org = options.org ?? await input({ message: " DA org", validate: (v) => !!v.trim() || "Required" });
1843
+ const site = options.site ?? (interactive ? await input({ message: " DA site", default: name }) : name);
1844
+ mountpoint = daMountpoint(org.trim(), slug2(site));
1845
+ } else if (authoring !== "da" && interactive) {
1846
+ const label = authoring === "gdrive" ? "Google Drive folder URL" : "SharePoint folder URL";
1847
+ mountpoint = (await input({
1848
+ message: ` ${label}`,
1849
+ validate: (v) => /^https?:\/\//.test(v.trim()) || "Paste the folder URL"
1850
+ })).trim();
1851
+ }
1852
+ }
1853
+ const withCi = options.ci ?? (interactive ? await confirm({ message: "4/4 Add GitHub Actions CI (audits gatekeeper)?", default: true }) : true);
1854
+ logger.info(heading("Summary"));
1855
+ const w = columnWidth(["name", "authoring", "mountpoint", "ci"]);
1856
+ logger.info(accentLine("name", name, w));
1857
+ logger.info(accentLine("authoring", AUTHORING_NAMES[authoring], w));
1858
+ if (mountpoint) logger.info(accentLine("mountpoint", mountpoint, w));
1859
+ else if (needsFstab(authoring))
1860
+ logger.info(accentLine("mountpoint", "(skipped - set it in fstab.yaml later)", w));
1861
+ logger.info(accentLine("ci", withCi ? "yes" : "no", w));
1862
+ logger.info("");
1863
+ if (interactive) {
1864
+ const go = await confirm({ message: "Apply this setup?", default: true });
1865
+ if (!go) {
1866
+ logger.info("Cancelled.");
1867
+ return;
1868
+ }
1869
+ }
1870
+ const done = [];
1871
+ if (await setPackageName(projectRoot, name)) done.push("package.json (name)");
1872
+ for (const t of await replacePlaceholders(projectRoot, name)) done.push(`${t} (renamed)`);
1873
+ if (needsFstab(authoring) && mountpoint) done.push(await writeFstab(projectRoot, mountpoint));
1874
+ if (authoring === "ue") {
1875
+ await scaffoldUe();
1876
+ done.push(await writePaths(projectRoot, name));
1877
+ }
1878
+ if (withCi) {
1879
+ const ci = await writeCiWorkflow(projectRoot);
1880
+ done.push(ci.created ? ci.path : `${ci.path} (exists, skipped)`);
1881
+ }
1882
+ logger.info(heading("Configured"));
1883
+ const dw = columnWidth(done.map((d) => d.split(" ")[0]));
1884
+ for (const d of done) {
1885
+ const [n2, ...rest] = d.split(" ");
1886
+ logger.info(accentLine(n2, rest.join(" ").replace(/^\((.*)\)$/, "$1"), dw));
1887
+ }
1888
+ logger.info("");
1889
+ if (interactive) await externalSteps(authoring);
1890
+ logger.info(box([`"${name}" set up for ${AUTHORING_NAMES[authoring]}`]));
1891
+ }
1892
+ var slug2;
1893
+ var init_init = __esm({
1894
+ "src/commands/init.ts"() {
1895
+ "use strict";
1896
+ init_esm_shims();
1897
+ init_logger();
1898
+ init_project();
1899
+ init_ci();
1900
+ init_fstab();
1901
+ init_ui();
1902
+ init_scaffold();
1903
+ slug2 = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1904
+ }
1905
+ });
1906
+
1907
+ // src/lib/migrate/clientlib.ts
1908
+ import { existsSync as existsSync8 } from "fs";
1909
+ import { readFile as readFile5, readdir as readdir2 } from "fs/promises";
1910
+ import path10 from "path";
1911
+ async function listFiles(dir) {
1912
+ const out = [];
1913
+ const walk3 = async (d) => {
1914
+ for (const e of await readdir2(d, { withFileTypes: true })) {
1915
+ const p = path10.join(d, e.name);
1916
+ if (e.isDirectory()) await walk3(p);
1917
+ else out.push(p);
1918
+ }
1919
+ };
1920
+ if (existsSync8(dir)) await walk3(dir);
1921
+ return out;
1922
+ }
1923
+ async function findClientlibDir(componentDir) {
1924
+ if (!existsSync8(componentDir)) return null;
1925
+ const entries = await readdir2(componentDir, { withFileTypes: true });
1926
+ const match = entries.find((e) => e.isDirectory() && /clientlib/i.test(e.name));
1927
+ if (match) return path10.join(componentDir, match.name);
1928
+ const hasCss = entries.some((e) => e.isFile() && e.name.endsWith(".css"));
1929
+ return hasCss ? componentDir : null;
1930
+ }
1931
+ async function readClientlibCss(clientlibDir) {
1932
+ const all = await listFiles(clientlibDir);
1933
+ const preprocessed = all.filter((f2) => /\.(less|scss|sass)$/i.test(f2)).map((f2) => path10.relative(clientlibDir, f2));
1934
+ let cssFiles;
1935
+ const cssTxt = path10.join(clientlibDir, "css.txt");
1936
+ if (existsSync8(cssTxt)) {
1937
+ const lines = (await readFile5(cssTxt, "utf-8")).split("\n").map((l) => l.trim());
1938
+ let base = clientlibDir;
1939
+ const listed = [];
1940
+ for (const l of lines) {
1941
+ if (!l) continue;
1942
+ const baseMatch = l.match(/^#base\s*=\s*(.+)$/);
1943
+ if (baseMatch) base = path10.join(clientlibDir, baseMatch[1].trim());
1944
+ else if (!l.startsWith("#")) listed.push(l);
1945
+ }
1946
+ cssFiles = listed.map((rel) => path10.join(base, rel)).filter((f2) => existsSync8(f2) && f2.endsWith(".css"));
1947
+ } else {
1948
+ cssFiles = all.filter((f2) => f2.endsWith(".css"));
1949
+ }
1950
+ const parts = [];
1951
+ for (const f2 of cssFiles) parts.push(await readFile5(f2, "utf-8"));
1952
+ return {
1953
+ css: parts.join("\n\n"),
1954
+ files: cssFiles.map((f2) => path10.relative(clientlibDir, f2)),
1955
+ preprocessed
1956
+ };
1957
+ }
1958
+ var init_clientlib = __esm({
1959
+ "src/lib/migrate/clientlib.ts"() {
1960
+ "use strict";
1961
+ init_esm_shims();
1962
+ }
1963
+ });
1964
+
1965
+ // src/lib/migrate/dialog.ts
1966
+ import { XMLParser } from "fast-xml-parser";
1967
+ function tagOf(n2) {
1968
+ return Object.keys(n2).find((k) => k !== ":@") ?? "";
1969
+ }
1970
+ function childrenOf(n2) {
1971
+ const t = tagOf(n2);
1972
+ const v = n2[t];
1973
+ return Array.isArray(v) ? v : [];
1974
+ }
1975
+ function attrsOf(n2) {
1976
+ return n2[":@"] ?? {};
1977
+ }
1978
+ function stripType(v) {
1979
+ return typeof v === "string" ? v.replace(/^\{[^}]+\}/, "") : v;
1980
+ }
1981
+ function attr(n2, name) {
1982
+ return stripType(attrsOf(n2)[`@_${name}`]);
1983
+ }
1984
+ function leafComponent(rt, n2) {
1985
+ const multiple = attr(n2, "multiple") === "true" || attr(n2, "multiple") === "{Boolean}true";
1986
+ if (rt.endsWith("/form/textfield")) return "text";
1987
+ if (rt.endsWith("/form/textarea")) return "textarea";
1988
+ if (rt.includes("/richtext")) return "richtext";
1989
+ if (rt.endsWith("/form/numberfield")) return "number";
1990
+ if (rt.endsWith("/form/checkbox")) return "boolean";
1991
+ if (rt.endsWith("/form/radiogroup")) return "radio-group";
1992
+ if (rt.endsWith("/form/select")) return multiple ? "multiselect" : "select";
1993
+ if (rt.endsWith("/form/datepicker")) return "date-time";
1994
+ if (rt.endsWith("/form/pathfield") || rt.endsWith("/form/pathbrowser")) return "aem-content";
1995
+ if (rt.includes("/fileupload") || rt.endsWith("/form/fileupload")) return "reference";
1996
+ if (rt.includes("tagfield") || rt.includes("tagpicker") || rt.includes("tagbrowser"))
1997
+ return "aem-tag";
1998
+ if (rt.includes("contentfragment")) return "aem-content-fragment";
1999
+ if (rt.includes("experiencefragment")) return "aem-experience-fragment";
2000
+ return null;
2001
+ }
2002
+ function isWrapper(rt) {
2003
+ 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");
2004
+ }
2005
+ function valueTypeFor(c) {
2006
+ switch (c) {
2007
+ case "number":
2008
+ return "number";
2009
+ case "boolean":
2010
+ return "boolean";
2011
+ case "date-time":
2012
+ return "date";
2013
+ case "checkbox-group":
2014
+ return "string[]";
2015
+ case "aem-content":
2016
+ case "aem-content-fragment":
2017
+ case "aem-experience-fragment":
2018
+ return void 0;
2019
+ default:
2020
+ return "string";
2021
+ }
2022
+ }
2023
+ function cleanName(raw, fallback) {
2024
+ const n2 = (raw ?? "").replace(/^\.\//, "").trim();
2025
+ return n2 || fallback;
2026
+ }
2027
+ function readOptions(fieldNode) {
2028
+ const items = childrenOf(fieldNode).find((c) => tagOf(c) === "items");
2029
+ if (!items) return [];
2030
+ return childrenOf(items).map((opt) => {
2031
+ const value = attr(opt, "value") ?? "";
2032
+ const name = attr(opt, "text") ?? attr(opt, "jcr:title") ?? value;
2033
+ return { name, value };
1207
2034
  });
1208
- if (what === "ue" || what === "both") await scaffoldUe();
1209
- if (what === "blocks" || what === "both") {
1210
- const picked = await checkbox({
1211
- message: "Which blocks?",
1212
- choices: STANDARD_BLOCKS.map((b) => ({ name: b.title, value: b.id, checked: true }))
1213
- });
1214
- await scaffoldBlocks(picked);
2035
+ }
2036
+ function walk(nodes, out, unmapped, counter) {
2037
+ for (const node of nodes) {
2038
+ const rt = attr(node, "sling:resourceType") ?? "";
2039
+ if (rt.endsWith("/foundation/tabs")) {
2040
+ const items = childrenOf(node).find((c) => tagOf(c) === "items");
2041
+ for (const tab of items ? childrenOf(items) : []) {
2042
+ const label = attr(tab, "jcr:title");
2043
+ out.push({ component: "tab", name: cleanName(void 0, tagOf(tab)), label });
2044
+ walk(childrenOf(tab), out, unmapped, counter);
2045
+ }
2046
+ continue;
2047
+ }
2048
+ if (rt.endsWith("/form/multifield")) {
2049
+ const sub = [];
2050
+ walk(childrenOf(node), sub, unmapped, counter);
2051
+ const name = cleanName(attr(node, "name"), `items${counter.i++}`);
2052
+ out.push({
2053
+ component: "container",
2054
+ name,
2055
+ label: attr(node, "fieldLabel") ?? "Items",
2056
+ multi: true,
2057
+ collapsible: true,
2058
+ fields: sub
2059
+ });
2060
+ continue;
2061
+ }
2062
+ const comp = rt ? leafComponent(rt, node) : null;
2063
+ if (comp) {
2064
+ const name = cleanName(attr(node, "name"), `field${counter.i++}`);
2065
+ const field = { component: comp, name };
2066
+ const vt = valueTypeFor(comp);
2067
+ if (vt) field.valueType = vt;
2068
+ const label = attr(node, "fieldLabel") ?? attr(node, "text");
2069
+ if (label) field.label = label;
2070
+ if (comp === "select" || comp === "radio-group" || comp === "multiselect") {
2071
+ const options = readOptions(node);
2072
+ if (options.length) field.options = options;
2073
+ }
2074
+ const def = attr(node, "value") ?? (comp === "boolean" ? attr(node, "checked") : void 0);
2075
+ if (def !== void 0 && def !== "") field.value = comp === "boolean" ? def === "true" : def;
2076
+ out.push(field);
2077
+ continue;
2078
+ }
2079
+ if (rt && attr(node, "name") && !isWrapper(rt)) unmapped.add(rt);
2080
+ walk(childrenOf(node), out, unmapped, counter);
1215
2081
  }
1216
- if (what === "ci") await scaffoldCi();
1217
2082
  }
1218
- var init_scaffold = __esm({
1219
- "src/commands/scaffold.ts"() {
2083
+ function parseDialog(xml) {
2084
+ const parser = new XMLParser({
2085
+ ignoreAttributes: false,
2086
+ attributeNamePrefix: "@_",
2087
+ preserveOrder: true,
2088
+ removeNSPrefix: false,
2089
+ trimValues: true
2090
+ });
2091
+ const tree = parser.parse(xml);
2092
+ const root = tree.find((n2) => tagOf(n2) === "jcr:root");
2093
+ const fields = [];
2094
+ const unmapped = /* @__PURE__ */ new Set();
2095
+ if (root) walk(childrenOf(root), fields, unmapped, { i: 1 });
2096
+ return { fields, unmapped: [...unmapped] };
2097
+ }
2098
+ var init_dialog = __esm({
2099
+ "src/lib/migrate/dialog.ts"() {
1220
2100
  "use strict";
1221
2101
  init_esm_shims();
1222
- init_logger();
1223
- init_project();
1224
- init_blocks();
1225
- init_ci();
1226
- init_ue();
1227
- init_ui();
1228
2102
  }
1229
2103
  });
1230
2104
 
1231
- // src/commands/init.ts
1232
- var init_exports = {};
1233
- __export(init_exports, {
1234
- initProject: () => initProject
1235
- });
1236
- import { existsSync as existsSync4 } from "fs";
1237
- import { readFile as readFile2, writeFile as writeFile4 } from "fs/promises";
1238
- import path6 from "path";
1239
- import chalk3 from "chalk";
1240
- async function setPackageName(root, name) {
1241
- const file = path6.join(root, "package.json");
1242
- if (!existsSync4(file)) return false;
2105
+ // src/lib/migrate/component.ts
2106
+ import { existsSync as existsSync9 } from "fs";
2107
+ import { readFile as readFile6 } from "fs/promises";
2108
+ import path11 from "path";
2109
+ import { XMLParser as XMLParser2 } from "fast-xml-parser";
2110
+ function findDialogFile(componentDir) {
2111
+ const candidates = [
2112
+ path11.join(componentDir, "_cq_dialog", ".content.xml"),
2113
+ path11.join(componentDir, "_cq_dialog.xml"),
2114
+ path11.join(componentDir, "cq:dialog", ".content.xml")
2115
+ ];
2116
+ return candidates.find((c) => existsSync9(c)) ?? null;
2117
+ }
2118
+ async function readComponentTitle(componentDir) {
2119
+ const file = path11.join(componentDir, ".content.xml");
2120
+ if (!existsSync9(file)) return void 0;
1243
2121
  try {
1244
- const pkg = JSON.parse(await readFile2(file, "utf-8"));
1245
- pkg.name = name;
1246
- await writeFile4(file, `${JSON.stringify(pkg, null, 2)}
1247
- `);
1248
- return true;
2122
+ const parser = new XMLParser2({ ignoreAttributes: false, attributeNamePrefix: "@_" });
2123
+ const parsed = parser.parse(await readFile6(file, "utf-8"));
2124
+ const root = parsed["jcr:root"] ?? {};
2125
+ const title = root["@_jcr:title"];
2126
+ return typeof title === "string" ? title.replace(/^\{[^}]+\}/, "") : void 0;
1249
2127
  } catch {
1250
- return false;
2128
+ return void 0;
1251
2129
  }
1252
2130
  }
1253
- async function replacePlaceholders(root, name) {
1254
- const touched = [];
1255
- for (const rel of ["README.md", "index.html", "head.html"]) {
1256
- const file = path6.join(root, rel);
1257
- if (!existsSync4(file)) continue;
1258
- try {
1259
- const before = await readFile2(file, "utf-8");
1260
- const after = before.replace(/aem-boilerplate/gi, name);
1261
- if (after !== before) {
1262
- await writeFile4(file, after);
1263
- touched.push(rel);
2131
+ async function readDialogFields(componentDir) {
2132
+ const dialog = findDialogFile(componentDir);
2133
+ if (!dialog) return null;
2134
+ return parseDialog(await readFile6(dialog, "utf-8"));
2135
+ }
2136
+ function migratedModelFile(blockName, title, fields) {
2137
+ const model2 = {
2138
+ definitions: [
2139
+ {
2140
+ title,
2141
+ id: blockName,
2142
+ plugins: {
2143
+ xwalk: {
2144
+ page: {
2145
+ resourceType: "core/franklin/components/block/v1/block",
2146
+ template: { name: title, model: blockName }
2147
+ }
2148
+ }
2149
+ }
1264
2150
  }
1265
- } catch {
1266
- }
2151
+ ],
2152
+ models: [{ id: blockName, fields }],
2153
+ filters: []
2154
+ };
2155
+ return `${JSON.stringify(model2, null, 2)}
2156
+ `;
2157
+ }
2158
+ function migratedBlockJs(blockName, fields) {
2159
+ const hasContainer = fields.some((f2) => f2.component === "container" && f2.multi);
2160
+ const body = hasContainer ? ` // This block was migrated from a classic AEM component with a multifield.
2161
+ // Each child row is one repeated item - enhance them here.
2162
+ [...block.children].forEach((row) => {
2163
+ row.classList.add('${blockName}-item');
2164
+ });` : ` // Migrated from a classic AEM component. The authored fields arrive as
2165
+ // rows; read and enhance them here (port the old HTL logic as needed).`;
2166
+ return `// Block: ${blockName}
2167
+ // Migrated from a classic AEM component by \`eds migrate component\`.
2168
+ // TODO: port the component's HTL/JS behaviour into this decorator.
2169
+ export default function decorate(block) {
2170
+ block.classList.add('${blockName}');
2171
+ ${body}
2172
+ }
2173
+ `;
2174
+ }
2175
+ function migratedBlockCss(blockName) {
2176
+ return `/* Block: ${blockName} */
2177
+ /* Migrated from a classic AEM component. Bring the component's styles here */
2178
+ /* (from its clientlib or the rendered page) and keep them scoped to the block. */
2179
+ .${blockName} {
2180
+ }
2181
+ `;
2182
+ }
2183
+ var init_component = __esm({
2184
+ "src/lib/migrate/component.ts"() {
2185
+ "use strict";
2186
+ init_esm_shims();
2187
+ init_dialog();
1267
2188
  }
1268
- return touched;
2189
+ });
2190
+
2191
+ // src/lib/track.ts
2192
+ function trackSnippet(blockName, param, events) {
2193
+ const parts = [` // --- ${TRACK_MARKER} ---`];
2194
+ if (events.click) {
2195
+ parts.push(
2196
+ ` ${param}.addEventListener('click', (ev) => {`,
2197
+ " const el = ev.target.closest('a, button');",
2198
+ " if (!el) return;",
2199
+ ` trackEvent({ event: 'block_click', block: '${blockName}', label: el.dataset.track || el.textContent.trim() });`,
2200
+ " });"
2201
+ );
2202
+ }
2203
+ if (events.submit) {
2204
+ parts.push(
2205
+ ` ${param}.addEventListener('submit', () => {`,
2206
+ ` trackEvent({ event: 'form_submit', block: '${blockName}' });`,
2207
+ " });"
2208
+ );
2209
+ }
2210
+ return parts.join("\n");
1269
2211
  }
1270
- async function externalSteps(authoring) {
1271
- const { confirm } = await import("@inquirer/prompts");
1272
- const steps = [
1273
- {
1274
- title: "Install the AEM Code Sync GitHub app",
1275
- url: "https://github.com/apps/aem-code-sync/installations/new",
1276
- note: "Connects your repo to the Edge Delivery pipeline (preview/publish)."
2212
+ function instrumentJs(js, blockName, events) {
2213
+ if (js.includes(TRACK_MARKER)) return { js, changed: false, reason: "already instrumented" };
2214
+ if (!events.click && !events.submit) return { js, changed: false, reason: "no events selected" };
2215
+ const decorate = js.match(
2216
+ /export default (?:async )?function decorate\s*\(\s*([A-Za-z_$][\w$]*)?[^)]*\)\s*\{/
2217
+ );
2218
+ if (!decorate || decorate.index === void 0) {
2219
+ return { js, changed: false, reason: "no `export default function decorate(...)` found" };
2220
+ }
2221
+ const param = decorate[1] || "block";
2222
+ let out = js;
2223
+ if (!out.includes("scripts/analytics.js")) {
2224
+ const imports = [...out.matchAll(/^import .*$/gm)];
2225
+ if (imports.length) {
2226
+ const last = imports[imports.length - 1];
2227
+ const at2 = (last.index ?? 0) + last[0].length;
2228
+ out = `${out.slice(0, at2)}
2229
+ ${IMPORT_LINE}${out.slice(at2)}`;
2230
+ } else {
2231
+ out = `${IMPORT_LINE}
2232
+
2233
+ ${out}`;
1277
2234
  }
1278
- ];
1279
- if (authoring === "gdrive") {
1280
- steps.push({
1281
- title: "Share your content folder",
1282
- note: "Share the Google Drive folder with the AEM service and set it as the mountpoint."
1283
- });
1284
2235
  }
1285
- if (authoring === "sharepoint") {
1286
- steps.push({
1287
- title: "Share your content folder",
1288
- note: "Grant the SharePoint/OneDrive folder to the AEM service account."
1289
- });
2236
+ const d2 = out.match(
2237
+ /export default (?:async )?function decorate\s*\(\s*([A-Za-z_$][\w$]*)?[^)]*\)\s*\{/
2238
+ );
2239
+ if (!d2 || d2.index === void 0)
2240
+ return { js: out, changed: false, reason: "decorate not found" };
2241
+ const at = d2.index + d2[0].length;
2242
+ const snippet = trackSnippet(blockName, param, events);
2243
+ out = `${out.slice(0, at)}
2244
+ ${snippet}
2245
+ ${out.slice(at)}`;
2246
+ return { js: out, changed: true };
2247
+ }
2248
+ var TRACK_MARKER, IMPORT_LINE;
2249
+ var init_track = __esm({
2250
+ "src/lib/track.ts"() {
2251
+ "use strict";
2252
+ init_esm_shims();
2253
+ TRACK_MARKER = "analytics (eds track)";
2254
+ IMPORT_LINE = "import { trackEvent } from '../../scripts/analytics.js';";
2255
+ }
2256
+ });
2257
+
2258
+ // src/commands/track.ts
2259
+ var track_exports = {};
2260
+ __export(track_exports, {
2261
+ trackBlock: () => trackBlock
2262
+ });
2263
+ import { existsSync as existsSync10 } from "fs";
2264
+ import { readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
2265
+ import path12 from "path";
2266
+ import chalk5 from "chalk";
2267
+ async function trackBlock(blockName, options = {}) {
2268
+ const projectRoot = findProjectRoot();
2269
+ if (!projectRoot) {
2270
+ logger.error("Not inside an EDS project (no EDS project markers found).");
2271
+ process.exitCode = 1;
2272
+ return;
2273
+ }
2274
+ logger.logoOnce(logo("Track block"));
2275
+ const jsPath = path12.join(projectRoot, "blocks", blockName, `${blockName}.js`);
2276
+ if (!existsSync10(jsPath)) {
2277
+ logger.error(`Block "${blockName}" not found (no blocks/${blockName}/${blockName}.js).`);
2278
+ process.exitCode = 1;
2279
+ return;
1290
2280
  }
1291
- if (authoring === "da") {
1292
- steps.push({
1293
- title: "Author in DA",
1294
- url: "https://da.live",
1295
- note: "Create documents at da.live."
1296
- });
2281
+ const events = { click: options.click !== false, submit: options.submit !== false };
2282
+ const res = instrumentJs(await readFile7(jsPath, "utf-8"), blockName, events);
2283
+ if (!res.changed) {
2284
+ logger.warn(`Not instrumented: ${res.reason}.`);
2285
+ return;
1297
2286
  }
1298
- if (authoring === "ue") {
1299
- steps.push({
1300
- title: "Open the Universal Editor",
1301
- url: "https://experience.adobe.com",
1302
- note: "Author in-context once the repo is connected to AEM."
1303
- });
2287
+ await writeFile7(jsPath, res.js);
2288
+ logger.info(heading("Instrumented"));
2289
+ const picked = [events.click ? "block_click" : null, events.submit ? "form_submit" : null].filter(
2290
+ Boolean
2291
+ );
2292
+ logger.info(accentLine(`blocks/${blockName}/${blockName}.js`, picked.join(" + ")));
2293
+ if (!existsSync10(path12.join(projectRoot, "scripts", "analytics.js"))) {
2294
+ logger.info("");
2295
+ logger.warn("scripts/analytics.js is missing - run `eds integrate gtm` to create it.");
1304
2296
  }
1305
- steps.push({
1306
- title: "Install the AEM Sidekick extension",
1307
- url: "https://chromewebstore.google.com/detail/aem-sidekick/igkmdomcgoebiipaifhmpfjhbjccggml",
1308
- note: "Preview and publish from the browser."
1309
- });
1310
- steps.push({
1311
- title: "Run the local dev server",
1312
- note: "npm install, then `aem up` (from @adobe/aem-cli)."
1313
- });
1314
- logger.info(heading("Next steps (outside the repo)"));
1315
- for (let i = 0; i < steps.length; i++) {
1316
- const s = steps[i];
1317
- logger.info(` ${brand.accent(String(i + 1))} ${chalk3.bold(s.title)}`);
1318
- if (s.url) logger.info(` ${chalk3.cyan(s.url)}`);
1319
- logger.info(` ${chalk3.dim(s.note)}`);
1320
- await confirm({ message: " Done / skip?", default: true });
2297
+ logger.info("");
2298
+ logger.info(chalk5.dim(" Events push to window.dataLayer via trackEvent - GTM picks them up."));
2299
+ }
2300
+ var init_track2 = __esm({
2301
+ "src/commands/track.ts"() {
2302
+ "use strict";
2303
+ init_esm_shims();
2304
+ init_logger();
2305
+ init_project();
2306
+ init_track();
2307
+ init_ui();
1321
2308
  }
2309
+ });
2310
+
2311
+ // src/commands/migrate.ts
2312
+ var migrate_exports = {};
2313
+ __export(migrate_exports, {
2314
+ migrateComponent: () => migrateComponent
2315
+ });
2316
+ import { existsSync as existsSync11 } from "fs";
2317
+ import { mkdir as mkdir5, writeFile as writeFile8 } from "fs/promises";
2318
+ import path13 from "path";
2319
+ function clientlibBlockCss(blockName, css, files) {
2320
+ return `/* Block: ${blockName} */
2321
+ /* Migrated from the component clientlib: ${files.join(", ")}. */
2322
+ /* TODO: review selectors and scope them under .${blockName} (and port any LESS/SCSS). */
2323
+
2324
+ ${css.trim()}
2325
+ `;
1322
2326
  }
1323
- async function initProject(options = {}) {
2327
+ function previewFields(fields, depth = 0) {
2328
+ const lines = [];
2329
+ for (const f2 of fields) {
2330
+ const indent = " ".repeat(depth);
2331
+ if (f2.component === "tab") {
2332
+ lines.push(`${indent}${brand.blue("\u25B8")} ${f2.label ?? f2.name}`);
2333
+ } else {
2334
+ const opts = f2.options?.length ? ` (${f2.options.length} options)` : "";
2335
+ const multi = f2.multi ? " \xB7multi" : "";
2336
+ const label = f2.label ? `- ${f2.label}` : "";
2337
+ lines.push(`${indent}${brand.accent(f2.component)}${multi} ${f2.name}${opts} ${label}`);
2338
+ if (f2.fields?.length) lines.push(...previewFields(f2.fields, depth + 1));
2339
+ }
2340
+ }
2341
+ return lines;
2342
+ }
2343
+ function countLeaves(fields) {
2344
+ let n2 = 0;
2345
+ for (const f2 of fields) {
2346
+ if (f2.component === "tab") continue;
2347
+ n2 += 1;
2348
+ if (f2.fields) n2 += countLeaves(f2.fields);
2349
+ }
2350
+ return n2;
2351
+ }
2352
+ async function migrateComponent(dirArg, options = {}) {
1324
2353
  const projectRoot = findProjectRoot();
1325
2354
  if (!projectRoot) {
1326
- logger.error(
1327
- "Not inside an EDS project (no EDS project markers found). Clone an EDS boilerplate first, then run this from its root."
1328
- );
2355
+ logger.error("Not inside an EDS project (no EDS project markers found).");
1329
2356
  process.exitCode = 1;
1330
2357
  return;
1331
2358
  }
1332
- await logger.logoOnceAnimated("Project setup");
1333
- const { input, select, confirm } = await import("@inquirer/prompts");
2359
+ await logger.logoOnceAnimated("Migrate component");
2360
+ const { input, confirm } = await import("@inquirer/prompts");
1334
2361
  const interactive = !options.yes;
1335
- let defaultName = path6.basename(projectRoot);
1336
- const pkgFile = path6.join(projectRoot, "package.json");
1337
- if (existsSync4(pkgFile)) {
1338
- try {
1339
- defaultName = JSON.parse(await readFile2(pkgFile, "utf-8")).name || defaultName;
1340
- } catch {
1341
- }
2362
+ let componentDir = dirArg;
2363
+ if (!componentDir && interactive) {
2364
+ componentDir = await input({
2365
+ message: "Path to the classic component folder",
2366
+ validate: (v) => existsSync11(v.trim()) || "Folder not found"
2367
+ });
1342
2368
  }
1343
- const name = options.name && slug(options.name) || slug(
1344
- await input({
1345
- message: "1/4 Project name",
1346
- default: slug(defaultName),
1347
- validate: (v) => !!slug(v) || "Use letters, numbers and dashes"
1348
- })
1349
- );
1350
- const authoring = options.authoring ?? await select({
1351
- message: "2/4 Authoring model",
1352
- choices: [
1353
- { name: AUTHORING_NAMES.ue, value: "ue" },
1354
- { name: AUTHORING_NAMES.da, value: "da" },
1355
- { name: AUTHORING_NAMES.gdrive, value: "gdrive" },
1356
- { name: AUTHORING_NAMES.sharepoint, value: "sharepoint" }
1357
- ]
1358
- });
1359
- let mountpoint = options.mountpoint;
1360
- if (needsFstab(authoring) && !mountpoint) {
1361
- if (authoring === "da" && (options.org || interactive)) {
1362
- const org = options.org ?? await input({ message: " DA org", validate: (v) => !!v.trim() || "Required" });
1363
- const site = options.site ?? (interactive ? await input({ message: " DA site", default: name }) : name);
1364
- mountpoint = daMountpoint(org.trim(), slug(site));
1365
- } else if (authoring !== "da" && interactive) {
1366
- const label = authoring === "gdrive" ? "Google Drive folder URL" : "SharePoint folder URL";
1367
- mountpoint = (await input({
1368
- message: ` ${label}`,
1369
- validate: (v) => /^https?:\/\//.test(v.trim()) || "Paste the folder URL"
1370
- })).trim();
1371
- }
2369
+ if (!componentDir) {
2370
+ logger.error("Provide the path to the classic component folder.");
2371
+ process.exitCode = 1;
2372
+ return;
2373
+ }
2374
+ componentDir = path13.resolve(componentDir.trim());
2375
+ if (!findDialogFile(componentDir)) {
2376
+ logger.error(`No Touch UI dialog (_cq_dialog/.content.xml) found in ${componentDir}.`);
2377
+ process.exitCode = 1;
2378
+ return;
2379
+ }
2380
+ const parsed = await readDialogFields(componentDir);
2381
+ if (!parsed || parsed.fields.length === 0) {
2382
+ logger.warn("Dialog found but no mappable fields were detected.");
2383
+ if (parsed?.unmapped.length)
2384
+ logger.info(`Unmapped resourceTypes: ${parsed.unmapped.join(", ")}`);
2385
+ process.exitCode = 1;
2386
+ return;
2387
+ }
2388
+ const title = await readComponentTitle(componentDir) ?? path13.basename(componentDir);
2389
+ const blockName = options.name && slug3(options.name) || (interactive ? slug3(await input({ message: "Block name", default: slug3(title) })) : slug3(title));
2390
+ logger.info(heading("Mapped fields", `${countLeaves(parsed.fields)} from dialog`));
2391
+ for (const line of previewFields(parsed.fields)) logger.info(` ${line}`);
2392
+ if (parsed.unmapped.length) {
2393
+ logger.info("");
2394
+ logger.warn(`Skipped (unrecognised): ${parsed.unmapped.join(", ")}`);
1372
2395
  }
1373
- const withCi = options.ci ?? (interactive ? await confirm({ message: "4/4 Add GitHub Actions CI (audits gatekeeper)?", default: true }) : true);
1374
- logger.info(heading("Summary"));
1375
- const w = columnWidth(["name", "authoring", "mountpoint", "ci"]);
1376
- logger.info(accentLine("name", name, w));
1377
- logger.info(accentLine("authoring", AUTHORING_NAMES[authoring], w));
1378
- if (mountpoint) logger.info(accentLine("mountpoint", mountpoint, w));
1379
- else if (needsFstab(authoring))
1380
- logger.info(accentLine("mountpoint", "(skipped - set it in fstab.yaml later)", w));
1381
- logger.info(accentLine("ci", withCi ? "yes" : "no", w));
1382
2396
  logger.info("");
1383
2397
  if (interactive) {
1384
- const go = await confirm({ message: "Apply this setup?", default: true });
2398
+ const go = await confirm({ message: `Generate block "${blockName}"?`, default: true });
1385
2399
  if (!go) {
1386
2400
  logger.info("Cancelled.");
1387
2401
  return;
1388
2402
  }
1389
2403
  }
1390
- const done = [];
1391
- if (await setPackageName(projectRoot, name)) done.push("package.json (name)");
1392
- for (const t of await replacePlaceholders(projectRoot, name)) done.push(`${t} (renamed)`);
1393
- if (needsFstab(authoring) && mountpoint) done.push(await writeFstab(projectRoot, mountpoint));
1394
- if (authoring === "ue") {
1395
- await scaffoldUe();
1396
- done.push(await writePaths(projectRoot, name));
2404
+ const blockDir = path13.join(projectRoot, "blocks", blockName);
2405
+ if (existsSync11(blockDir)) {
2406
+ logger.error(
2407
+ `Block "${blockName}" already exists at blocks/${blockName}. Use --name to pick another.`
2408
+ );
2409
+ process.exitCode = 1;
2410
+ return;
1397
2411
  }
1398
- if (withCi) {
1399
- const ci = await writeCiWorkflow(projectRoot);
1400
- done.push(ci.created ? ci.path : `${ci.path} (exists, skipped)`);
2412
+ const clibDir = options.clientlib ? path13.resolve(options.clientlib.trim()) : await findClientlibDir(componentDir);
2413
+ let cssContent = migratedBlockCss(blockName);
2414
+ let cssMessage = "empty - add styles from clientlib / rendered CSS";
2415
+ let preprocessedNote = [];
2416
+ if (clibDir && existsSync11(clibDir)) {
2417
+ const lib = await readClientlibCss(clibDir);
2418
+ if (lib.css.trim()) {
2419
+ cssContent = clientlibBlockCss(blockName, lib.css, lib.files);
2420
+ cssMessage = `from clientlib (${lib.files.length} file(s)) - review & scope`;
2421
+ }
2422
+ preprocessedNote = lib.preprocessed;
1401
2423
  }
1402
- logger.info(heading("Configured"));
1403
- const dw = columnWidth(done.map((d) => d.split(" ")[0]));
1404
- for (const d of done) {
1405
- const [n, ...rest] = d.split(" ");
1406
- logger.info(accentLine(n, rest.join(" ").replace(/^\((.*)\)$/, "$1"), dw));
2424
+ await mkdir5(blockDir, { recursive: true });
2425
+ await writeFile8(
2426
+ path13.join(blockDir, `${blockName}.js`),
2427
+ migratedBlockJs(blockName, parsed.fields)
2428
+ );
2429
+ await writeFile8(path13.join(blockDir, `${blockName}.css`), cssContent);
2430
+ await writeFile8(
2431
+ path13.join(blockDir, `_${blockName}.json`),
2432
+ migratedModelFile(blockName, title, parsed.fields)
2433
+ );
2434
+ logger.info(heading("Created"));
2435
+ const files = [`${blockName}.js`, `${blockName}.css`, `_${blockName}.json`];
2436
+ const w = columnWidth(files);
2437
+ logger.info(accentLine(`${blockName}.js`, "decorator stub (port HTL logic)", w));
2438
+ logger.info(accentLine(`${blockName}.css`, cssMessage, w));
2439
+ logger.info(accentLine(`_${blockName}.json`, "Universal Editor model", w));
2440
+ if (preprocessedNote.length) {
2441
+ logger.info("");
2442
+ logger.warn(`LESS/SCSS found (needs a build, not copied): ${preprocessedNote.join(", ")}`);
1407
2443
  }
1408
2444
  logger.info("");
1409
- if (interactive) await externalSteps(authoring);
1410
- logger.info(box([`"${name}" set up for ${AUTHORING_NAMES[authoring]}`]));
2445
+ if (interactive) {
2446
+ const addTracking = await confirm({
2447
+ message: "Instrument this block for analytics (dataLayer)?",
2448
+ default: false
2449
+ });
2450
+ if (addTracking) {
2451
+ const { trackBlock: trackBlock2 } = await Promise.resolve().then(() => (init_track2(), track_exports));
2452
+ await trackBlock2(blockName, {});
2453
+ }
2454
+ }
2455
+ logger.info(
2456
+ box([`"${blockName}" migrated. Next: add CSS, then \`eds block preview ${blockName}\``])
2457
+ );
1411
2458
  }
1412
- var slug;
1413
- var init_init = __esm({
1414
- "src/commands/init.ts"() {
2459
+ var slug3;
2460
+ var init_migrate = __esm({
2461
+ "src/commands/migrate.ts"() {
1415
2462
  "use strict";
1416
2463
  init_esm_shims();
1417
2464
  init_logger();
2465
+ init_clientlib();
2466
+ init_component();
1418
2467
  init_project();
1419
- init_ci();
1420
- init_fstab();
1421
2468
  init_ui();
1422
- init_scaffold();
1423
- slug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
2469
+ slug3 = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1424
2470
  }
1425
2471
  });
1426
2472
 
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"));
2473
+ // src/lib/model/helpers.ts
2474
+ function n(prefix, base) {
2475
+ if (!prefix) return base;
2476
+ return prefix + base.charAt(0).toUpperCase() + base.slice(1);
2477
+ }
2478
+ var f;
2479
+ var init_helpers2 = __esm({
2480
+ "src/lib/model/helpers.ts"() {
2481
+ "use strict";
2482
+ init_esm_shims();
2483
+ f = {
2484
+ text: (name, label) => ({
2485
+ component: "text",
2486
+ valueType: "string",
2487
+ name,
2488
+ label
2489
+ }),
2490
+ textarea: (name, label) => ({
2491
+ component: "textarea",
2492
+ valueType: "string",
2493
+ name,
2494
+ label
2495
+ }),
2496
+ richtext: (name, label) => ({
2497
+ component: "richtext",
2498
+ valueType: "string",
2499
+ name,
2500
+ value: "",
2501
+ label
2502
+ }),
2503
+ number: (name, label) => ({
2504
+ component: "number",
2505
+ valueType: "number",
2506
+ name,
2507
+ label
2508
+ }),
2509
+ boolean: (name, label, value = false) => ({
2510
+ component: "boolean",
2511
+ valueType: "boolean",
2512
+ name,
2513
+ value,
2514
+ label
2515
+ }),
2516
+ dateTime: (name, label) => ({
2517
+ component: "date-time",
2518
+ valueType: "date",
2519
+ name,
2520
+ label
2521
+ }),
2522
+ select: (name, label, options, value) => ({
2523
+ component: "select",
2524
+ valueType: "string",
2525
+ name,
2526
+ label,
2527
+ options,
2528
+ ...value !== void 0 ? { value } : {}
2529
+ }),
2530
+ multiselect: (name, label, options) => ({
2531
+ component: "multiselect",
2532
+ valueType: "string",
2533
+ name,
2534
+ label,
2535
+ options
2536
+ }),
2537
+ radioGroup: (name, label, options, value) => ({
2538
+ component: "radio-group",
2539
+ valueType: "string",
2540
+ name,
2541
+ label,
2542
+ options,
2543
+ ...value !== void 0 ? { value } : {}
2544
+ }),
2545
+ checkboxGroup: (name, label, options) => ({
2546
+ component: "checkbox-group",
2547
+ valueType: "string[]",
2548
+ name,
2549
+ label,
2550
+ options
2551
+ }),
2552
+ reference: (name, label) => ({
2553
+ component: "reference",
2554
+ valueType: "string",
2555
+ name,
2556
+ label
2557
+ }),
2558
+ aemContent: (name, label) => ({ component: "aem-content", name, label }),
2559
+ aemContentFragment: (name, label) => ({
2560
+ component: "aem-content-fragment",
2561
+ name,
2562
+ label
2563
+ }),
2564
+ aemExperienceFragment: (name, label) => ({
2565
+ component: "aem-experience-fragment",
2566
+ name,
2567
+ label
2568
+ }),
2569
+ aemTag: (name, label) => ({
2570
+ component: "aem-tag",
2571
+ valueType: "string",
2572
+ name,
2573
+ label
2574
+ }),
2575
+ tab: (name, label) => ({ component: "tab", name, label }),
2576
+ container: (name, label, fields, collapsible = true) => ({
2577
+ component: "container",
2578
+ name,
2579
+ label,
2580
+ multi: true,
2581
+ collapsible,
2582
+ fields
2583
+ })
2584
+ };
1469
2585
  }
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
- };
2586
+ });
2587
+
2588
+ // src/lib/model/partials.ts
2589
+ function partialById(id) {
2590
+ return PARTIALS.find((p) => p.id === id);
1477
2591
  }
1478
- var init_clientlib = __esm({
1479
- "src/lib/migrate/clientlib.ts"() {
2592
+ var HEADING_LEVELS, LINK_TARGETS, BUTTON_STYLES, SECTION_STYLES, heading2, eyebrow, image, richtext, link, cta, ctaGroup, classes, embed, teaser, card, PARTIALS;
2593
+ var init_partials = __esm({
2594
+ "src/lib/model/partials.ts"() {
1480
2595
  "use strict";
1481
2596
  init_esm_shims();
2597
+ init_helpers2();
2598
+ HEADING_LEVELS = [
2599
+ { name: "H1", value: "h1" },
2600
+ { name: "H2", value: "h2" },
2601
+ { name: "H3", value: "h3" },
2602
+ { name: "H4", value: "h4" },
2603
+ { name: "H5", value: "h5" },
2604
+ { name: "H6", value: "h6" }
2605
+ ];
2606
+ LINK_TARGETS = [
2607
+ { name: "Same tab", value: "_self" },
2608
+ { name: "New tab", value: "_blank" }
2609
+ ];
2610
+ BUTTON_STYLES = [
2611
+ { name: "Default", value: "" },
2612
+ { name: "Primary", value: "primary" },
2613
+ { name: "Secondary", value: "secondary" }
2614
+ ];
2615
+ SECTION_STYLES = [
2616
+ { name: "Highlight", value: "highlight" },
2617
+ { name: "Dark", value: "dark" },
2618
+ { name: "Centered", value: "centered" },
2619
+ { name: "Full width", value: "full-width" }
2620
+ ];
2621
+ heading2 = (p = "") => [
2622
+ f.text(n(p, "title"), "Title"),
2623
+ f.select(n(p, "titleType"), "Heading level", HEADING_LEVELS, "h2")
2624
+ ];
2625
+ eyebrow = (p = "") => [f.text(n(p, "eyebrow"), "Eyebrow")];
2626
+ image = (p = "") => [
2627
+ f.reference(n(p, "image"), "Image"),
2628
+ f.text(n(p, "imageAlt"), "Alt text")
2629
+ ];
2630
+ richtext = (p = "") => [f.richtext(n(p, "text"), "Text")];
2631
+ link = (p = "") => [
2632
+ f.text(n(p, "linkText"), "Link text"),
2633
+ f.aemContent(n(p, "link"), "Link"),
2634
+ f.select(n(p, "linkTarget"), "Open in", LINK_TARGETS, "_self")
2635
+ ];
2636
+ cta = (p = "") => [
2637
+ ...link(p),
2638
+ f.select(n(p, "ctaStyle"), "Style", BUTTON_STYLES, "")
2639
+ ];
2640
+ ctaGroup = (p = "") => [
2641
+ f.container(n(p, "ctas") || "ctas", "Buttons", cta())
2642
+ ];
2643
+ classes = (p = "") => [
2644
+ f.multiselect(n(p, "classes"), "Styles", SECTION_STYLES)
2645
+ ];
2646
+ embed = (p = "") => [f.aemContent(n(p, "url"), "URL (video / embed)")];
2647
+ teaser = (p = "") => [
2648
+ ...image(p),
2649
+ ...eyebrow(p),
2650
+ ...heading2(p),
2651
+ ...richtext(p),
2652
+ ...cta(p)
2653
+ ];
2654
+ card = (p = "") => [
2655
+ ...image(p),
2656
+ f.text(n(p, "title"), "Title"),
2657
+ f.richtext(n(p, "description"), "Description"),
2658
+ ...link(p)
2659
+ ];
2660
+ PARTIALS = [
2661
+ { id: "heading", label: "Heading (title + level)", build: heading2 },
2662
+ { id: "eyebrow", label: "Eyebrow (kicker line)", build: eyebrow },
2663
+ { id: "image", label: "Image (reference + alt)", build: image },
2664
+ { id: "richtext", label: "Rich text body", build: richtext },
2665
+ { id: "link", label: "Link (text + url + target)", build: link },
2666
+ { id: "cta", label: "CTA (link + style)", build: cta },
2667
+ { id: "cta-group", label: "CTA group (repeatable buttons)", build: ctaGroup },
2668
+ { id: "classes", label: "Section styles (CSS modifiers)", build: classes },
2669
+ { id: "embed", label: "Embed / video (URL)", build: embed },
2670
+ { id: "teaser", label: "Teaser (image + eyebrow + title + text + CTA)", build: teaser },
2671
+ { id: "card", label: "Card (image + title + description + link)", build: card }
2672
+ ];
1482
2673
  }
1483
2674
  });
1484
2675
 
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";
2676
+ // src/commands/model.ts
2677
+ var model_exports = {};
2678
+ __export(model_exports, {
2679
+ modelAdd: () => modelAdd
2680
+ });
2681
+ import { existsSync as existsSync12 } from "fs";
2682
+ import { readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
2683
+ import path14 from "path";
2684
+ async function modelAdd(blockArg, partialArgs, options = {}) {
2685
+ const projectRoot = findProjectRoot();
2686
+ if (!projectRoot) {
2687
+ logger.error("Not inside an EDS project (no EDS project markers found).");
2688
+ process.exitCode = 1;
2689
+ return;
1541
2690
  }
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;
2691
+ logger.logoOnce(logo("Model - add partials"));
2692
+ const { select, checkbox } = await import("@inquirer/prompts");
2693
+ const interactive = !options.yes;
2694
+ let blockName = blockArg;
2695
+ if (!blockName && interactive) {
2696
+ const { readdir: readdir8 } = await import("fs/promises");
2697
+ const blocksDir = path14.join(projectRoot, "blocks");
2698
+ const dirs = existsSync12(blocksDir) ? (await readdir8(blocksDir, { withFileTypes: true })).filter(
2699
+ (e) => e.isDirectory() && existsSync12(path14.join(blocksDir, e.name, `_${e.name}.json`))
2700
+ ).map((e) => e.name) : [];
2701
+ if (dirs.length === 0) {
2702
+ logger.error("No block with a _<name>.json model found. Create one with `eds block create`.");
2703
+ process.exitCode = 1;
2704
+ return;
1581
2705
  }
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;
2706
+ blockName = await select({
2707
+ message: "Which block's model?",
2708
+ choices: dirs.map((d) => ({ name: d, value: d }))
2709
+ });
2710
+ }
2711
+ if (!blockName) {
2712
+ logger.error("Provide a block name.");
2713
+ process.exitCode = 1;
2714
+ return;
2715
+ }
2716
+ const modelFile = path14.join(projectRoot, "blocks", blockName, `_${blockName}.json`);
2717
+ if (!existsSync12(modelFile)) {
2718
+ logger.error(`No model at blocks/${blockName}/_${blockName}.json. Create the block first.`);
2719
+ process.exitCode = 1;
2720
+ return;
2721
+ }
2722
+ let ids = partialArgs;
2723
+ if (ids.length === 0 && interactive) {
2724
+ ids = await checkbox({
2725
+ message: "Which partials to add?",
2726
+ choices: PARTIALS.map((p) => ({ name: p.label, value: p.id }))
2727
+ });
2728
+ }
2729
+ const partials = ids.map((id) => partialById(id.trim())).filter((p) => !!p);
2730
+ const unknown = ids.filter((id) => !partialById(id.trim()));
2731
+ if (unknown.length) {
2732
+ logger.warn(
2733
+ `Unknown partial(s): ${unknown.join(", ")}. Available: ${PARTIALS.map((p) => p.id).join(", ")}`
2734
+ );
2735
+ }
2736
+ if (partials.length === 0) {
2737
+ logger.info("Nothing to add.");
2738
+ return;
2739
+ }
2740
+ let model2;
2741
+ try {
2742
+ model2 = JSON.parse(await readFile8(modelFile, "utf-8"));
2743
+ } catch {
2744
+ logger.error(`Could not parse ${modelFile}.`);
2745
+ process.exitCode = 1;
2746
+ return;
2747
+ }
2748
+ const models = Array.isArray(model2.models) ? model2.models : [];
2749
+ const target = models.find((m) => m.id === blockName) ?? models[0];
2750
+ if (!target) {
2751
+ logger.error("Model has no `models[]` entry to add fields to.");
2752
+ process.exitCode = 1;
2753
+ return;
2754
+ }
2755
+ target.fields = Array.isArray(target.fields) ? target.fields : [];
2756
+ const existing = new Set(target.fields.map((x) => x.name));
2757
+ const added = [];
2758
+ const skipped = [];
2759
+ for (const p of partials) {
2760
+ for (const field of p.build()) {
2761
+ if (existing.has(field.name)) {
2762
+ skipped.push(field.name);
2763
+ continue;
1593
2764
  }
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;
2765
+ existing.add(field.name);
2766
+ target.fields.push(field);
2767
+ added.push(field.name);
1598
2768
  }
1599
- if (rt && attr(node, "name") && !isWrapper(rt)) unmapped.add(rt);
1600
- walk(childrenOf(node), out, unmapped, counter);
1601
2769
  }
2770
+ await writeFile9(modelFile, `${JSON.stringify(model2, null, 2)}
2771
+ `);
2772
+ logger.info(heading("Model updated", `blocks/${blockName}/_${blockName}.json`));
2773
+ logger.info(accentLine("partials", partials.map((p) => p.id).join(", ")));
2774
+ logger.info(accentLine("fields added", added.length ? added.join(", ") : "(none)"));
2775
+ if (skipped.length) logger.info(statusLine("warn", "skipped (exists)", skipped.join(", ")));
2776
+ logger.info("");
2777
+ logger.info(box([`${added.length} field(s) added to "${blockName}"`]));
1602
2778
  }
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"() {
2779
+ var init_model = __esm({
2780
+ "src/commands/model.ts"() {
1620
2781
  "use strict";
1621
2782
  init_esm_shims();
2783
+ init_logger();
2784
+ init_partials();
2785
+ init_project();
2786
+ init_ui();
1622
2787
  }
1623
2788
  });
1624
2789
 
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;
2790
+ // src/lib/schema/jsonld.ts
2791
+ function schemaTypeById(id) {
2792
+ return SCHEMA_TYPES.find((t) => t.id.toLowerCase() === id.toLowerCase());
2793
+ }
2794
+ function injectJsonLd(js, type) {
2795
+ if (js.includes(SCHEMA_MARKER)) return { js, changed: false, reason: "already has JSON-LD" };
2796
+ const decorate = js.match(
2797
+ /export default (?:async )?function decorate\s*\(\s*([A-Za-z_$][\w$]*)?[^)]*\)\s*\{/
2798
+ );
2799
+ if (!decorate || decorate.index === void 0) {
2800
+ return { js, changed: false, reason: "no `export default function decorate(...)` found" };
2801
+ }
2802
+ const param = decorate[1] || "block";
2803
+ const at = decorate.index + decorate[0].length;
2804
+ const snippet = `
2805
+ // --- ${SCHEMA_MARKER}: ${type.id}) ---
2806
+ {
2807
+ const root = ${param};
2808
+ const ld = ${type.object};
2809
+ const ldScript = document.createElement('script');
2810
+ ldScript.type = 'application/ld+json';
2811
+ ldScript.textContent = JSON.stringify(ld);
2812
+ document.head.append(ldScript);
1649
2813
  }
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
2814
  `;
2815
+ return { js: `${js.slice(0, at)}${snippet}${js.slice(at)}`, changed: true };
1702
2816
  }
1703
- var init_component = __esm({
1704
- "src/lib/migrate/component.ts"() {
2817
+ var SCHEMA_MARKER, SCHEMA_TYPES;
2818
+ var init_jsonld = __esm({
2819
+ "src/lib/schema/jsonld.ts"() {
1705
2820
  "use strict";
1706
2821
  init_esm_shims();
1707
- init_dialog();
2822
+ SCHEMA_MARKER = "json-ld (eds schema";
2823
+ SCHEMA_TYPES = [
2824
+ {
2825
+ id: "Article",
2826
+ label: "Article / BlogPosting",
2827
+ object: `{
2828
+ '@context': 'https://schema.org',
2829
+ '@type': 'Article',
2830
+ headline: root.querySelector('h1, h2, h3')?.textContent?.trim() || document.title,
2831
+ image: [...root.querySelectorAll('img')].map((img) => img.src),
2832
+ description: root.querySelector('p')?.textContent?.trim(),
2833
+ url: window.location.href,
2834
+ // TODO: author, datePublished, dateModified
2835
+ }`
2836
+ },
2837
+ {
2838
+ id: "FAQPage",
2839
+ label: "FAQ page",
2840
+ object: `{
2841
+ '@context': 'https://schema.org',
2842
+ '@type': 'FAQPage',
2843
+ mainEntity: [...root.children].map((row) => {
2844
+ const [q, a] = row.children;
2845
+ return {
2846
+ '@type': 'Question',
2847
+ name: q?.textContent?.trim(),
2848
+ acceptedAnswer: { '@type': 'Answer', text: a?.innerHTML?.trim() },
2849
+ };
2850
+ }),
2851
+ }`
2852
+ },
2853
+ {
2854
+ id: "BreadcrumbList",
2855
+ label: "Breadcrumb list",
2856
+ object: `{
2857
+ '@context': 'https://schema.org',
2858
+ '@type': 'BreadcrumbList',
2859
+ itemListElement: [...root.querySelectorAll('a')].map((a, i) => ({
2860
+ '@type': 'ListItem',
2861
+ position: i + 1,
2862
+ name: a.textContent?.trim(),
2863
+ item: a.href,
2864
+ })),
2865
+ }`
2866
+ },
2867
+ {
2868
+ id: "Product",
2869
+ label: "Product",
2870
+ object: `{
2871
+ '@context': 'https://schema.org',
2872
+ '@type': 'Product',
2873
+ name: root.querySelector('h1, h2, h3')?.textContent?.trim(),
2874
+ image: [...root.querySelectorAll('img')].map((img) => img.src),
2875
+ description: root.querySelector('p')?.textContent?.trim(),
2876
+ // TODO: brand, sku, offers { '@type': 'Offer', price, priceCurrency, availability }
2877
+ }`
2878
+ },
2879
+ {
2880
+ id: "Organization",
2881
+ label: "Organization",
2882
+ object: `{
2883
+ '@context': 'https://schema.org',
2884
+ '@type': 'Organization',
2885
+ name: document.title,
2886
+ url: window.location.origin,
2887
+ logo: root.querySelector('img')?.src,
2888
+ // TODO: sameAs: [social profile URLs], contactPoint
2889
+ }`
2890
+ },
2891
+ {
2892
+ id: "LocalBusiness",
2893
+ label: "Local business",
2894
+ object: `{
2895
+ '@context': 'https://schema.org',
2896
+ '@type': 'LocalBusiness',
2897
+ name: root.querySelector('h1, h2, h3')?.textContent?.trim() || document.title,
2898
+ image: root.querySelector('img')?.src,
2899
+ url: window.location.origin,
2900
+ // TODO: address (PostalAddress), telephone, openingHours, geo
2901
+ }`
2902
+ },
2903
+ {
2904
+ id: "Event",
2905
+ label: "Event",
2906
+ object: `{
2907
+ '@context': 'https://schema.org',
2908
+ '@type': 'Event',
2909
+ name: root.querySelector('h1, h2, h3')?.textContent?.trim(),
2910
+ image: [...root.querySelectorAll('img')].map((img) => img.src),
2911
+ url: window.location.href,
2912
+ // TODO: startDate, endDate, location (Place), offers
2913
+ }`
2914
+ },
2915
+ {
2916
+ id: "VideoObject",
2917
+ label: "Video",
2918
+ object: `{
2919
+ '@context': 'https://schema.org',
2920
+ '@type': 'VideoObject',
2921
+ name: root.querySelector('h1, h2, h3')?.textContent?.trim() || document.title,
2922
+ thumbnailUrl: root.querySelector('img')?.src,
2923
+ contentUrl: root.querySelector('a[href], video source[src]')?.href,
2924
+ // TODO: description, uploadDate, duration (ISO 8601)
2925
+ }`
2926
+ },
2927
+ {
2928
+ id: "Recipe",
2929
+ label: "Recipe",
2930
+ object: `{
2931
+ '@context': 'https://schema.org',
2932
+ '@type': 'Recipe',
2933
+ name: root.querySelector('h1, h2, h3')?.textContent?.trim(),
2934
+ image: [...root.querySelectorAll('img')].map((img) => img.src),
2935
+ recipeIngredient: [...root.querySelectorAll('li')].map((li) => li.textContent?.trim()),
2936
+ // TODO: recipeInstructions, prepTime, cookTime, nutrition
2937
+ }`
2938
+ },
2939
+ {
2940
+ id: "HowTo",
2941
+ label: "How-to",
2942
+ object: `{
2943
+ '@context': 'https://schema.org',
2944
+ '@type': 'HowTo',
2945
+ name: root.querySelector('h1, h2, h3')?.textContent?.trim(),
2946
+ step: [...root.querySelectorAll('li')].map((li, i) => ({
2947
+ '@type': 'HowToStep',
2948
+ position: i + 1,
2949
+ text: li.textContent?.trim(),
2950
+ })),
2951
+ }`
2952
+ }
2953
+ ];
1708
2954
  }
1709
2955
  });
1710
2956
 
1711
- // src/commands/migrate.ts
1712
- var migrate_exports = {};
1713
- __export(migrate_exports, {
1714
- migrateComponent: () => migrateComponent
2957
+ // src/commands/schema.ts
2958
+ var schema_exports = {};
2959
+ __export(schema_exports, {
2960
+ schemaBlock: () => schemaBlock
1715
2961
  });
1716
- import { existsSync as existsSync7 } from "fs";
1717
- import { mkdir as mkdir3, writeFile as writeFile5 } from "fs/promises";
1718
- import path9 from "path";
1719
- function clientlibBlockCss(blockName, css, files) {
1720
- return `/* Block: ${blockName} */
1721
- /* Migrated from the component clientlib: ${files.join(", ")}. */
1722
- /* TODO: review selectors and scope them under .${blockName} (and port any LESS/SCSS). */
1723
-
1724
- ${css.trim()}
1725
- `;
1726
- }
1727
- function previewFields(fields, depth = 0) {
1728
- const lines = [];
1729
- for (const f of fields) {
1730
- const indent = " ".repeat(depth);
1731
- if (f.component === "tab") {
1732
- lines.push(`${indent}${brand.blue("\u25B8")} ${f.label ?? f.name}`);
1733
- } else {
1734
- const opts = f.options?.length ? ` (${f.options.length} options)` : "";
1735
- const multi = f.multi ? " \xB7multi" : "";
1736
- const label = f.label ? `- ${f.label}` : "";
1737
- lines.push(`${indent}${brand.accent(f.component)}${multi} ${f.name}${opts} ${label}`);
1738
- if (f.fields?.length) lines.push(...previewFields(f.fields, depth + 1));
1739
- }
1740
- }
1741
- return lines;
1742
- }
1743
- function countLeaves(fields) {
1744
- let n = 0;
1745
- for (const f of fields) {
1746
- if (f.component === "tab") continue;
1747
- n += 1;
1748
- if (f.fields) n += countLeaves(f.fields);
1749
- }
1750
- return n;
1751
- }
1752
- async function migrateComponent(dirArg, options = {}) {
2962
+ import { existsSync as existsSync13 } from "fs";
2963
+ import { readFile as readFile9, writeFile as writeFile10 } from "fs/promises";
2964
+ import path15 from "path";
2965
+ import chalk6 from "chalk";
2966
+ async function schemaBlock(blockName, options = {}) {
1753
2967
  const projectRoot = findProjectRoot();
1754
2968
  if (!projectRoot) {
1755
2969
  logger.error("Not inside an EDS project (no EDS project markers found).");
1756
2970
  process.exitCode = 1;
1757
2971
  return;
1758
2972
  }
1759
- await logger.logoOnceAnimated("Migrate component");
1760
- const { input, confirm } = await import("@inquirer/prompts");
1761
- const interactive = !options.yes;
1762
- let componentDir = dirArg;
1763
- if (!componentDir && interactive) {
1764
- componentDir = await input({
1765
- message: "Path to the classic component folder",
1766
- validate: (v) => existsSync7(v.trim()) || "Folder not found"
1767
- });
1768
- }
1769
- if (!componentDir) {
1770
- logger.error("Provide the path to the classic component folder.");
1771
- process.exitCode = 1;
1772
- return;
1773
- }
1774
- componentDir = path9.resolve(componentDir.trim());
1775
- if (!findDialogFile(componentDir)) {
1776
- logger.error(`No Touch UI dialog (_cq_dialog/.content.xml) found in ${componentDir}.`);
1777
- process.exitCode = 1;
1778
- return;
1779
- }
1780
- const parsed = await readDialogFields(componentDir);
1781
- if (!parsed || parsed.fields.length === 0) {
1782
- logger.warn("Dialog found but no mappable fields were detected.");
1783
- if (parsed?.unmapped.length)
1784
- logger.info(`Unmapped resourceTypes: ${parsed.unmapped.join(", ")}`);
2973
+ logger.logoOnce(logo("Schema - JSON-LD"));
2974
+ const jsPath = path15.join(projectRoot, "blocks", blockName, `${blockName}.js`);
2975
+ if (!existsSync13(jsPath)) {
2976
+ logger.error(`Block "${blockName}" not found (no blocks/${blockName}/${blockName}.js).`);
1785
2977
  process.exitCode = 1;
1786
2978
  return;
1787
2979
  }
1788
- const title = await readComponentTitle(componentDir) ?? path9.basename(componentDir);
1789
- const blockName = options.name && slug2(options.name) || (interactive ? slug2(await input({ message: "Block name", default: slug2(title) })) : slug2(title));
1790
- logger.info(heading("Mapped fields", `${countLeaves(parsed.fields)} from dialog`));
1791
- for (const line of previewFields(parsed.fields)) logger.info(` ${line}`);
1792
- if (parsed.unmapped.length) {
1793
- logger.info("");
1794
- logger.warn(`Skipped (unrecognised): ${parsed.unmapped.join(", ")}`);
1795
- }
1796
- logger.info("");
1797
- if (interactive) {
1798
- const go = await confirm({ message: `Generate block "${blockName}"?`, default: true });
1799
- if (!go) {
1800
- logger.info("Cancelled.");
1801
- return;
1802
- }
2980
+ let typeId = options.type;
2981
+ if (!typeId && !options.yes) {
2982
+ const { select } = await import("@inquirer/prompts");
2983
+ typeId = await select({
2984
+ message: "Which schema.org type?",
2985
+ choices: SCHEMA_TYPES.map((t) => ({ name: `${t.id} - ${t.label}`, value: t.id }))
2986
+ });
1803
2987
  }
1804
- const blockDir = path9.join(projectRoot, "blocks", blockName);
1805
- if (existsSync7(blockDir)) {
2988
+ const type = typeId ? schemaTypeById(typeId) : void 0;
2989
+ if (!type) {
1806
2990
  logger.error(
1807
- `Block "${blockName}" already exists at blocks/${blockName}. Use --name to pick another.`
2991
+ `Unknown schema type "${typeId}". Options: ${SCHEMA_TYPES.map((t) => t.id).join(", ")}`
1808
2992
  );
1809
2993
  process.exitCode = 1;
1810
2994
  return;
1811
2995
  }
1812
- const clibDir = options.clientlib ? path9.resolve(options.clientlib.trim()) : await findClientlibDir(componentDir);
1813
- let cssContent = migratedBlockCss(blockName);
1814
- let cssMessage = "empty - add styles from clientlib / rendered CSS";
1815
- let preprocessedNote = [];
1816
- if (clibDir && existsSync7(clibDir)) {
1817
- const lib = await readClientlibCss(clibDir);
1818
- if (lib.css.trim()) {
1819
- cssContent = clientlibBlockCss(blockName, lib.css, lib.files);
1820
- cssMessage = `from clientlib (${lib.files.length} file(s)) - review & scope`;
1821
- }
1822
- preprocessedNote = lib.preprocessed;
1823
- }
1824
- await mkdir3(blockDir, { recursive: true });
1825
- await writeFile5(
1826
- path9.join(blockDir, `${blockName}.js`),
1827
- migratedBlockJs(blockName, parsed.fields)
1828
- );
1829
- await writeFile5(path9.join(blockDir, `${blockName}.css`), cssContent);
1830
- await writeFile5(
1831
- path9.join(blockDir, `_${blockName}.json`),
1832
- migratedModelFile(blockName, title, parsed.fields)
1833
- );
1834
- logger.info(heading("Created"));
1835
- const files = [`${blockName}.js`, `${blockName}.css`, `_${blockName}.json`];
1836
- const w = columnWidth(files);
1837
- logger.info(accentLine(`${blockName}.js`, "decorator stub (port HTL logic)", w));
1838
- logger.info(accentLine(`${blockName}.css`, cssMessage, w));
1839
- logger.info(accentLine(`_${blockName}.json`, "Universal Editor model", w));
1840
- if (preprocessedNote.length) {
1841
- logger.info("");
1842
- logger.warn(`LESS/SCSS found (needs a build, not copied): ${preprocessedNote.join(", ")}`);
2996
+ const res = injectJsonLd(await readFile9(jsPath, "utf-8"), type);
2997
+ if (!res.changed) {
2998
+ logger.warn(`Not added: ${res.reason}.`);
2999
+ return;
1843
3000
  }
3001
+ await writeFile10(jsPath, res.js);
3002
+ logger.info(heading("JSON-LD added"));
3003
+ logger.info(accentLine(`blocks/${blockName}/${blockName}.js`, `${type.id} (${type.label})`));
1844
3004
  logger.info("");
1845
3005
  logger.info(
1846
- box([`"${blockName}" migrated. Next: add CSS, then \`eds block preview ${blockName}\``])
3006
+ chalk6.dim(
3007
+ " Review the generated object - some fields are heuristic or TODO. Validate at search.google.com/test/rich-results."
3008
+ )
1847
3009
  );
1848
3010
  }
1849
- var slug2;
1850
- var init_migrate = __esm({
1851
- "src/commands/migrate.ts"() {
3011
+ var init_schema = __esm({
3012
+ "src/commands/schema.ts"() {
1852
3013
  "use strict";
1853
3014
  init_esm_shims();
1854
3015
  init_logger();
1855
- init_clientlib();
1856
- init_component();
1857
3016
  init_project();
3017
+ init_jsonld();
1858
3018
  init_ui();
1859
- slug2 = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1860
3019
  }
1861
3020
  });
1862
3021
 
@@ -1866,16 +3025,16 @@ __export(create_exports, {
1866
3025
  createBlock: () => createBlock,
1867
3026
  ueModelFor: () => ueModelFor
1868
3027
  });
1869
- import { existsSync as existsSync8 } from "fs";
1870
- import { mkdir as mkdir4, writeFile as writeFile6 } from "fs/promises";
1871
- import path10 from "path";
1872
- import chalk4 from "chalk";
3028
+ import { existsSync as existsSync14 } from "fs";
3029
+ import { mkdir as mkdir6, writeFile as writeFile11 } from "fs/promises";
3030
+ import path16 from "path";
3031
+ import chalk7 from "chalk";
1873
3032
  function titleCase(name) {
1874
3033
  return name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1875
3034
  }
1876
3035
  function ueModelFor(name) {
1877
3036
  const title = titleCase(name);
1878
- const model = {
3037
+ const model2 = {
1879
3038
  definitions: [
1880
3039
  {
1881
3040
  title,
@@ -1928,7 +3087,7 @@ function ueModelFor(name) {
1928
3087
  ],
1929
3088
  filters: []
1930
3089
  };
1931
- return `${JSON.stringify(model, null, 2)}
3090
+ return `${JSON.stringify(model2, null, 2)}
1932
3091
  `;
1933
3092
  }
1934
3093
  async function createBlock(name, options = {}) {
@@ -1947,13 +3106,13 @@ async function createBlock(name, options = {}) {
1947
3106
  process.exitCode = 1;
1948
3107
  return;
1949
3108
  }
1950
- const blocksDir = path10.join(projectRoot, "blocks", name);
1951
- if (existsSync8(blocksDir)) {
3109
+ const blocksDir = path16.join(projectRoot, "blocks", name);
3110
+ if (existsSync14(blocksDir)) {
1952
3111
  logger.error(`Block "${name}" already exists at ${blocksDir}`);
1953
3112
  process.exitCode = 1;
1954
3113
  return;
1955
3114
  }
1956
- await mkdir4(blocksDir, { recursive: true });
3115
+ await mkdir6(blocksDir, { recursive: true });
1957
3116
  const jsContent = `export default function decorate(block) {
1958
3117
  // TODO: implement ${name} block
1959
3118
  }
@@ -1962,14 +3121,14 @@ async function createBlock(name, options = {}) {
1962
3121
  /* TODO: style ${name} block */
1963
3122
  }
1964
3123
  `;
1965
- await writeFile6(path10.join(blocksDir, `${name}.js`), jsContent);
1966
- await writeFile6(path10.join(blocksDir, `${name}.css`), cssContent);
3124
+ await writeFile11(path16.join(blocksDir, `${name}.js`), jsContent);
3125
+ await writeFile11(path16.join(blocksDir, `${name}.css`), cssContent);
1967
3126
  const withUe = options.ueModel !== false;
1968
3127
  if (withUe) {
1969
- await writeFile6(path10.join(blocksDir, `_${name}.json`), ueModelFor(name));
3128
+ await writeFile11(path16.join(blocksDir, `_${name}.json`), ueModelFor(name));
1970
3129
  }
1971
3130
  logger.success(
1972
- `Block "${name}" created at ${chalk4.cyan(path10.relative(projectRoot, blocksDir))}${withUe ? chalk4.dim(" (+ Universal Editor model)") : ""}`
3131
+ `Block "${name}" created at ${chalk7.cyan(path16.relative(projectRoot, blocksDir))}${withUe ? chalk7.dim(" (+ Universal Editor model)") : ""}`
1973
3132
  );
1974
3133
  }
1975
3134
  var init_create = __esm({
@@ -1983,10 +3142,10 @@ var init_create = __esm({
1983
3142
  });
1984
3143
 
1985
3144
  // src/lib/agents/detect.ts
1986
- import { existsSync as existsSync9 } from "fs";
1987
- import { readFile as readFile5 } from "fs/promises";
3145
+ import { existsSync as existsSync15 } from "fs";
3146
+ import { readFile as readFile10 } from "fs/promises";
1988
3147
  import { homedir } from "os";
1989
- import path11 from "path";
3148
+ import path17 from "path";
1990
3149
  async function whichBinary(name) {
1991
3150
  const { execaCommand } = await import("execa");
1992
3151
  try {
@@ -2014,8 +3173,8 @@ function findFigmaServerKey(node) {
2014
3173
  async function findFigmaMcpServer(configPaths) {
2015
3174
  for (const configPath of configPaths) {
2016
3175
  try {
2017
- if (!existsSync9(configPath)) continue;
2018
- const content = await readFile5(configPath, "utf-8");
3176
+ if (!existsSync15(configPath)) continue;
3177
+ const content = await readFile10(configPath, "utf-8");
2019
3178
  try {
2020
3179
  const key = findFigmaServerKey(JSON.parse(content));
2021
3180
  if (key) return key;
@@ -2037,7 +3196,7 @@ async function detectAgent(preferred) {
2037
3196
  return {
2038
3197
  type: def.type,
2039
3198
  path: binPath,
2040
- configDir: existsSync9(def.configDir) ? def.configDir : void 0,
3199
+ configDir: existsSync15(def.configDir) ? def.configDir : void 0,
2041
3200
  hasFigmaMcp: !!figmaMcpServerName,
2042
3201
  figmaMcpServerName
2043
3202
  };
@@ -2052,7 +3211,7 @@ async function detectAgent(preferred) {
2052
3211
  return {
2053
3212
  type: def.type,
2054
3213
  path: binPath,
2055
- configDir: existsSync9(def.configDir) ? def.configDir : void 0,
3214
+ configDir: existsSync15(def.configDir) ? def.configDir : void 0,
2056
3215
  hasFigmaMcp: !!figmaMcpServerName,
2057
3216
  figmaMcpServerName
2058
3217
  };
@@ -2069,7 +3228,7 @@ async function detectAllAgents() {
2069
3228
  agents.push({
2070
3229
  type: def.type,
2071
3230
  path: binPath,
2072
- configDir: existsSync9(def.configDir) ? def.configDir : void 0,
3231
+ configDir: existsSync15(def.configDir) ? def.configDir : void 0,
2073
3232
  hasFigmaMcp: !!figmaMcpServerName,
2074
3233
  figmaMcpServerName
2075
3234
  });
@@ -2087,22 +3246,22 @@ var init_detect = __esm({
2087
3246
  {
2088
3247
  type: "claude",
2089
3248
  binary: "claude",
2090
- configDir: path11.join(homedir(), ".claude"),
3249
+ configDir: path17.join(homedir(), ".claude"),
2091
3250
  mcpConfigPaths: [
2092
- path11.join(homedir(), ".claude", "plugins.json"),
2093
- path11.join(homedir(), ".claude.json")
3251
+ path17.join(homedir(), ".claude", "plugins.json"),
3252
+ path17.join(homedir(), ".claude.json")
2094
3253
  ]
2095
3254
  },
2096
3255
  {
2097
3256
  type: "cursor",
2098
3257
  binary: "cursor",
2099
- configDir: path11.join(homedir(), ".cursor"),
2100
- mcpConfigPaths: [path11.join(homedir(), ".cursor", "mcp.json")]
3258
+ configDir: path17.join(homedir(), ".cursor"),
3259
+ mcpConfigPaths: [path17.join(homedir(), ".cursor", "mcp.json")]
2101
3260
  },
2102
3261
  {
2103
3262
  type: "codex",
2104
3263
  binary: "codex",
2105
- configDir: path11.join(homedir(), ".codex"),
3264
+ configDir: path17.join(homedir(), ".codex"),
2106
3265
  mcpConfigPaths: []
2107
3266
  }
2108
3267
  ];
@@ -2110,27 +3269,27 @@ var init_detect = __esm({
2110
3269
  });
2111
3270
 
2112
3271
  // src/lib/agents/invoke.ts
2113
- import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
3272
+ import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile12 } from "fs/promises";
2114
3273
  import { tmpdir } from "os";
2115
- import path12 from "path";
3274
+ import path18 from "path";
2116
3275
  import readline from "readline";
2117
- import chalk5 from "chalk";
3276
+ import chalk8 from "chalk";
2118
3277
  async function savePromptToFile(prompt, projectRoot) {
2119
- const base = projectRoot ? path12.join(projectRoot, "tmp") : tmpdir();
2120
- const dir = path12.join(base, "eds-cli");
2121
- await mkdir5(dir, { recursive: true });
3278
+ const base = projectRoot ? path18.join(projectRoot, "tmp") : tmpdir();
3279
+ const dir = path18.join(base, "eds-cli");
3280
+ await mkdir7(dir, { recursive: true });
2122
3281
  const filename = `from-design-${Date.now()}.md`;
2123
- const filePath = path12.join(dir, filename);
2124
- await writeFile7(filePath, prompt, "utf-8");
3282
+ const filePath = path18.join(dir, filename);
3283
+ await writeFile12(filePath, prompt, "utf-8");
2125
3284
  return filePath;
2126
3285
  }
2127
3286
  async function invokeAgent(agent, promptFile, cwd) {
2128
3287
  const { execa } = await import("execa");
2129
- const prompt = await readFile6(promptFile, "utf-8");
3288
+ const prompt = await readFile11(promptFile, "utf-8");
2130
3289
  try {
2131
3290
  switch (agent.type) {
2132
3291
  case "claude": {
2133
- logger.info(`Running Claude Code ${chalk5.dim("- live log:")}`);
3292
+ logger.info(`Running Claude Code ${chalk8.dim("- live log:")}`);
2134
3293
  const figmaServers = [
2135
3294
  ...agent.figmaMcpServerName ? [agent.figmaMcpServerName] : [],
2136
3295
  "figma-sse",
@@ -2193,10 +3352,10 @@ async function renderClaudeStream(sub, cwd) {
2193
3352
  const ora4 = (await import("ora")).default;
2194
3353
  const start = Date.now();
2195
3354
  const elapsed = () => `${Math.round((Date.now() - start) / 1e3)}s`;
2196
- const spinner = ora4({ text: `thinking ${chalk5.dim(elapsed())}`, color: "yellow" }).start();
3355
+ const spinner = ora4({ text: `thinking ${chalk8.dim(elapsed())}`, color: "yellow" }).start();
2197
3356
  let activity = "thinking";
2198
3357
  const timer = setInterval(() => {
2199
- spinner.text = `${activity} ${chalk5.dim(elapsed())}`;
3358
+ spinner.text = `${activity} ${chalk8.dim(elapsed())}`;
2200
3359
  }, 300);
2201
3360
  const pending = /* @__PURE__ */ new Map();
2202
3361
  const emit = (line) => {
@@ -2222,7 +3381,7 @@ async function renderClaudeStream(sub, cwd) {
2222
3381
  for (const block2 of content) {
2223
3382
  if (block2.type === "text" && "text" in block2) {
2224
3383
  const first = block2.text.trim().split("\n")[0].slice(0, 100);
2225
- if (first) emit(chalk5.dim(first));
3384
+ if (first) emit(chalk8.dim(first));
2226
3385
  } else if (block2.type === "tool_use" && "name" in block2) {
2227
3386
  const id = "id" in block2 && block2.id ? block2.id : `${Math.random()}`;
2228
3387
  pending.set(id, {
@@ -2238,8 +3397,8 @@ async function renderClaudeStream(sub, cwd) {
2238
3397
  const p = pending.get(block2.tool_use_id);
2239
3398
  if (p) {
2240
3399
  const dur = `${((Date.now() - p.start) / 1e3).toFixed(1)}s`;
2241
- const failed = "is_error" in block2 && block2.is_error ? ` ${chalk5.red("failed")}` : "";
2242
- emit(`${p.label} ${chalk5.dim(dur)}${failed}`);
3400
+ const failed = "is_error" in block2 && block2.is_error ? ` ${chalk8.red("failed")}` : "";
3401
+ emit(`${p.label} ${chalk8.dim(dur)}${failed}`);
2243
3402
  pending.delete(block2.tool_use_id);
2244
3403
  }
2245
3404
  }
@@ -2251,9 +3410,9 @@ async function renderClaudeStream(sub, cwd) {
2251
3410
  }
2252
3411
  }
2253
3412
  }
2254
- for (const p of pending.values()) emit(`${p.label} ${chalk5.dim("\u2026")}`);
3413
+ for (const p of pending.values()) emit(`${p.label} ${chalk8.dim("\u2026")}`);
2255
3414
  clearInterval(timer);
2256
- if (result) spinner.succeed(`${brand.accent("agent finished")} ${chalk5.dim(result)}`);
3415
+ if (result) spinner.succeed(`${brand.accent("agent finished")} ${chalk8.dim(result)}`);
2257
3416
  else spinner.stop();
2258
3417
  } catch (err) {
2259
3418
  clearInterval(timer);
@@ -2266,22 +3425,22 @@ function toolLabel(name, input, cwd) {
2266
3425
  const rel = (key) => {
2267
3426
  const p = input?.[key];
2268
3427
  if (typeof p !== "string") return "";
2269
- return p.startsWith(cwd) ? path12.relative(cwd, p) : path12.basename(p);
3428
+ return p.startsWith(cwd) ? path18.relative(cwd, p) : path18.basename(p);
2270
3429
  };
2271
3430
  if (name.includes("figma")) {
2272
3431
  const short = name.split("__").pop() ?? name;
2273
- return `${icon.figma} ${chalk5.bold(short)} ${chalk5.dim("Figma MCP")}`;
3432
+ return `${icon.figma} ${chalk8.bold(short)} ${chalk8.dim("Figma MCP")}`;
2274
3433
  }
2275
3434
  switch (name) {
2276
3435
  case "Write":
2277
3436
  case "Edit":
2278
3437
  case "NotebookEdit":
2279
- return `${brand.accent("\u270E")} ${chalk5.cyan(rel("file_path"))}`;
3438
+ return `${brand.accent("\u270E")} ${chalk8.cyan(rel("file_path"))}`;
2280
3439
  case "Read":
2281
- return chalk5.dim(`\xB7 read ${rel("file_path")}`);
3440
+ return chalk8.dim(`\xB7 read ${rel("file_path")}`);
2282
3441
  case "Bash": {
2283
3442
  const cmd = typeof input?.command === "string" ? input.command.split("\n")[0].slice(0, 70) : "";
2284
- return chalk5.dim(`$ ${cmd}`);
3443
+ return chalk8.dim(`$ ${cmd}`);
2285
3444
  }
2286
3445
  default:
2287
3446
  return `${brand.blue("\u2192")} ${name}`;
@@ -2297,13 +3456,13 @@ var init_invoke = __esm({
2297
3456
  });
2298
3457
 
2299
3458
  // src/lib/config.ts
2300
- import { existsSync as existsSync10 } from "fs";
2301
- import { readFile as readFile7 } from "fs/promises";
3459
+ import { existsSync as existsSync16 } from "fs";
3460
+ import { readFile as readFile12 } from "fs/promises";
2302
3461
  import { homedir as homedir2 } from "os";
2303
- import path13 from "path";
3462
+ import path19 from "path";
2304
3463
  async function loadJsonFile(filePath) {
2305
3464
  try {
2306
- const content = await readFile7(filePath, "utf-8");
3465
+ const content = await readFile12(filePath, "utf-8");
2307
3466
  return JSON.parse(content);
2308
3467
  } catch {
2309
3468
  return void 0;
@@ -2311,7 +3470,7 @@ async function loadJsonFile(filePath) {
2311
3470
  }
2312
3471
  async function loadConfig(projectRoot) {
2313
3472
  const globalRaw = await loadJsonFile(GLOBAL_CONFIG_PATH);
2314
- const projectRaw = projectRoot ? await loadJsonFile(path13.join(projectRoot, PROJECT_CONFIG_NAME)) : void 0;
3473
+ const projectRaw = projectRoot ? await loadJsonFile(path19.join(projectRoot, PROJECT_CONFIG_NAME)) : void 0;
2315
3474
  const merged = {
2316
3475
  ...typeof globalRaw === "object" && globalRaw !== null ? globalRaw : {},
2317
3476
  ...typeof projectRaw === "object" && projectRaw !== null ? projectRaw : {}
@@ -2330,7 +3489,7 @@ function globalConfigPath() {
2330
3489
  return GLOBAL_CONFIG_PATH;
2331
3490
  }
2332
3491
  function globalConfigExists() {
2333
- return existsSync10(GLOBAL_CONFIG_PATH);
3492
+ return existsSync16(GLOBAL_CONFIG_PATH);
2334
3493
  }
2335
3494
  var GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_PATH, PROJECT_CONFIG_NAME;
2336
3495
  var init_config = __esm({
@@ -2339,8 +3498,8 @@ var init_config = __esm({
2339
3498
  init_esm_shims();
2340
3499
  init_logger();
2341
3500
  init_schemas();
2342
- GLOBAL_CONFIG_DIR = path13.join(homedir2(), ".eds");
2343
- GLOBAL_CONFIG_PATH = path13.join(GLOBAL_CONFIG_DIR, "config.json");
3501
+ GLOBAL_CONFIG_DIR = path19.join(homedir2(), ".eds");
3502
+ GLOBAL_CONFIG_PATH = path19.join(GLOBAL_CONFIG_DIR, "config.json");
2344
3503
  PROJECT_CONFIG_NAME = ".edsrc.json";
2345
3504
  }
2346
3505
  });
@@ -2390,9 +3549,9 @@ var init_node_id = __esm({
2390
3549
  });
2391
3550
 
2392
3551
  // src/lib/figma/prompt-builder.ts
2393
- import { existsSync as existsSync11 } from "fs";
2394
- import { readFile as readFile8 } from "fs/promises";
2395
- import path14 from "path";
3552
+ import { existsSync as existsSync17 } from "fs";
3553
+ import { readFile as readFile13 } from "fs/promises";
3554
+ import path20 from "path";
2396
3555
  function contentSourceGuidance(source, cfHint, blockName) {
2397
3556
  if (!source || source === "document" || source === "ue") return "";
2398
3557
  const q = cfHint ? ` (model / persisted query: \`${cfHint}\`)` : "";
@@ -2521,6 +3680,9 @@ Rules:
2521
3680
  - **Self-check before finishing:** every field \`name\` in the model must be read by \`decorate()\` (or applied via \`classes\`). List each field and where the JS/CSS consumes it. Unconsumed fields = broken authoring.
2522
3681
 
2523
3682
  If the project uses a **single aggregated model file** (\`component-definition.json\` + \`component-models.json\` + \`component-filters.json\`) instead of per-block \`_${ctx.blockName}.json\`, detect that convention and add the block's definition/model/filter to those files instead (and register the block in the \`section\` filter). Prefer the per-block \`_${ctx.blockName}.json\` when neither exists. Full field reference and a repeatable-cards round-trip example: \`docs/universal-editor-fields.md\`.
3683
+
3684
+ **Reusable field groups (partials):** when the design shows one of these common shapes, reuse the standard group instead of inventing fields \u2014 same groups \`eds model add\` uses, so the model stays consistent across the project:
3685
+ ${PARTIALS.map((p) => `- \`${p.id}\` \u2014 ${p.label}`).join("\n")}
2524
3686
  ` : "";
2525
3687
  return `# Pixel-Perfect EDS Block from ${providerName}
2526
3688
 
@@ -2604,10 +3766,10 @@ ${tokensHint}
2604
3766
  `;
2605
3767
  }
2606
3768
  async function getTokensHint(projectRoot) {
2607
- const stylesPath = path14.join(projectRoot, "styles", "styles.css");
2608
- if (!existsSync11(stylesPath)) return "";
3769
+ const stylesPath = path20.join(projectRoot, "styles", "styles.css");
3770
+ if (!existsSync17(stylesPath)) return "";
2609
3771
  try {
2610
- const css = await readFile8(stylesPath, "utf-8");
3772
+ const css = await readFile13(stylesPath, "utf-8");
2611
3773
  const rootMatch = css.match(/:root\s*\{([^}]+)\}/);
2612
3774
  if (!rootMatch) return "";
2613
3775
  const props = rootMatch[1].split("\n").filter((line) => line.trim().startsWith("--")).map((line) => line.trim()).join("\n ");
@@ -2632,12 +3794,14 @@ var init_prompt_builder = __esm({
2632
3794
  "src/lib/figma/prompt-builder.ts"() {
2633
3795
  "use strict";
2634
3796
  init_esm_shims();
3797
+ init_partials();
2635
3798
  PROMPT_VERSION = "0.5.0";
2636
3799
  PROVIDER_NAMES = {
2637
3800
  figma: "Figma",
2638
3801
  stitch: "Google Stitch",
2639
3802
  canva: "Canva",
2640
- sketch: "Sketch"
3803
+ sketch: "Sketch",
3804
+ framer: "Framer"
2641
3805
  };
2642
3806
  }
2643
3807
  });
@@ -2747,11 +3911,11 @@ var init_harness = __esm({
2747
3911
  });
2748
3912
 
2749
3913
  // src/lib/preview/server.ts
2750
- import { createReadStream, existsSync as existsSync12, statSync } from "fs";
3914
+ import { createReadStream, existsSync as existsSync18, statSync } from "fs";
2751
3915
  import http from "http";
2752
- import path15 from "path";
3916
+ import path21 from "path";
2753
3917
  function startServer(root, routes, port, host = "127.0.0.1") {
2754
- const resolvedRoot = path15.resolve(root);
3918
+ const resolvedRoot = path21.resolve(root);
2755
3919
  const clients = /* @__PURE__ */ new Set();
2756
3920
  const server = http.createServer((req, res) => {
2757
3921
  const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
@@ -2777,19 +3941,19 @@ function startServer(root, routes, port, host = "127.0.0.1") {
2777
3941
  res.end(route.body);
2778
3942
  return;
2779
3943
  }
2780
- const filePath = path15.join(resolvedRoot, urlPath);
3944
+ const filePath = path21.join(resolvedRoot, urlPath);
2781
3945
  if (!filePath.startsWith(resolvedRoot)) {
2782
3946
  res.writeHead(403);
2783
3947
  res.end("Forbidden");
2784
3948
  return;
2785
3949
  }
2786
- if (!existsSync12(filePath) || !statSync(filePath).isFile()) {
3950
+ if (!existsSync18(filePath) || !statSync(filePath).isFile()) {
2787
3951
  res.writeHead(404, { "content-type": "text/plain" });
2788
3952
  res.end(`Not found: ${urlPath}`);
2789
3953
  return;
2790
3954
  }
2791
3955
  res.writeHead(200, {
2792
- "content-type": MIME[path15.extname(filePath).toLowerCase()] ?? "application/octet-stream"
3956
+ "content-type": MIME[path21.extname(filePath).toLowerCase()] ?? "application/octet-stream"
2793
3957
  });
2794
3958
  createReadStream(filePath).pipe(res);
2795
3959
  });
@@ -2853,10 +4017,10 @@ var preview_exports = {};
2853
4017
  __export(preview_exports, {
2854
4018
  previewBlock: () => previewBlock
2855
4019
  });
2856
- import { existsSync as existsSync13, watch } from "fs";
2857
- import { readFile as readFile9 } from "fs/promises";
2858
- import path16 from "path";
2859
- import chalk6 from "chalk";
4020
+ import { existsSync as existsSync19, watch } from "fs";
4021
+ import { readFile as readFile14 } from "fs/promises";
4022
+ import path22 from "path";
4023
+ import chalk9 from "chalk";
2860
4024
  function labelFor(width) {
2861
4025
  if (width <= 480) return `${width} mobile`;
2862
4026
  if (width <= 1024) return `${width} tablet`;
@@ -2908,30 +4072,30 @@ async function previewBlock(name, options = {}) {
2908
4072
  process.exitCode = 1;
2909
4073
  return;
2910
4074
  }
2911
- const blockDir = path16.join(projectRoot, "blocks", name);
2912
- if (!existsSync13(blockDir)) {
4075
+ const blockDir = path22.join(projectRoot, "blocks", name);
4076
+ if (!existsSync19(blockDir)) {
2913
4077
  logger.error(`Block "${name}" not found at blocks/${name}/.`);
2914
4078
  process.exitCode = 1;
2915
4079
  return;
2916
4080
  }
2917
- if (!existsSync13(path16.join(blockDir, `${name}.css`)) && !existsSync13(path16.join(blockDir, `${name}.js`))) {
4081
+ if (!existsSync19(path22.join(blockDir, `${name}.css`)) && !existsSync19(path22.join(blockDir, `${name}.js`))) {
2918
4082
  logger.error(`Block "${name}" has no ${name}.css / ${name}.js to preview.`);
2919
4083
  process.exitCode = 1;
2920
4084
  return;
2921
4085
  }
2922
4086
  let meta = null;
2923
- const metaPath = path16.join(blockDir, ".eds-meta.json");
2924
- if (existsSync13(metaPath)) {
4087
+ const metaPath = path22.join(blockDir, ".eds-meta.json");
4088
+ if (existsSync19(metaPath)) {
2925
4089
  try {
2926
- meta = JSON.parse(await readFile9(metaPath, "utf-8"));
4090
+ meta = JSON.parse(await readFile14(metaPath, "utf-8"));
2927
4091
  } catch {
2928
4092
  }
2929
4093
  }
2930
- const samplePath = path16.join(blockDir, `_${name}.preview.html`);
4094
+ const samplePath = path22.join(blockDir, `_${name}.preview.html`);
2931
4095
  let sampleContent;
2932
4096
  let placeholder = false;
2933
- if (existsSync13(samplePath)) {
2934
- sampleContent = await readFile9(samplePath, "utf-8");
4097
+ if (existsSync19(samplePath)) {
4098
+ sampleContent = await readFile14(samplePath, "utf-8");
2935
4099
  } else {
2936
4100
  sampleContent = defaultSample(name);
2937
4101
  placeholder = true;
@@ -2981,9 +4145,9 @@ async function previewBlock(name, options = {}) {
2981
4145
  reloadTimer = setTimeout(() => server.reload(), 150);
2982
4146
  });
2983
4147
  logger.info("");
2984
- logger.info(` ${chalk6.bold("\u279C")} ${chalk6.cyan(server.url)}`);
4148
+ logger.info(` ${chalk9.bold("\u279C")} ${chalk9.cyan(server.url)}`);
2985
4149
  logger.info(statusLine("pass", "live reload", "edits to the block refresh the browser"));
2986
- logger.info(chalk6.dim(" Press Ctrl+C to stop.\n"));
4150
+ logger.info(chalk9.dim(" Press Ctrl+C to stop.\n"));
2987
4151
  if (options.open !== false) await openInBrowser(server.url);
2988
4152
  const stop = async () => {
2989
4153
  watcher.close();
@@ -3012,10 +4176,10 @@ var from_design_exports = {};
3012
4176
  __export(from_design_exports, {
3013
4177
  blockFromDesign: () => blockFromDesign
3014
4178
  });
3015
- import { existsSync as existsSync14 } from "fs";
3016
- import { readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
3017
- import path17 from "path";
3018
- import chalk7 from "chalk";
4179
+ import { existsSync as existsSync20 } from "fs";
4180
+ import { readFile as readFile15, writeFile as writeFile13 } from "fs/promises";
4181
+ import path23 from "path";
4182
+ import chalk10 from "chalk";
3019
4183
  import ora from "ora";
3020
4184
  async function previewRunning(port = 8777) {
3021
4185
  try {
@@ -3038,7 +4202,8 @@ async function runWizard(figmaUrl) {
3038
4202
  { name: "Figma", value: "figma" },
3039
4203
  { name: "Google Stitch", value: "stitch" },
3040
4204
  { name: "Canva", value: "canva" },
3041
- { name: "Sketch", value: "sketch" }
4205
+ { name: "Sketch", value: "sketch" },
4206
+ { name: "Framer", value: "framer" }
3042
4207
  ],
3043
4208
  default: "figma"
3044
4209
  });
@@ -3164,7 +4329,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3164
4329
  try {
3165
4330
  figma2 = parseFigmaUrl(designUrl);
3166
4331
  spinner.succeed(
3167
- `Parsed Figma URL ${chalk7.dim(figma2.nodeId ? `node ${figma2.nodeId}` : `file ${figma2.fileKey}`)}`
4332
+ `Parsed Figma URL ${chalk10.dim(figma2.nodeId ? `node ${figma2.nodeId}` : `file ${figma2.fileKey}`)}`
3168
4333
  );
3169
4334
  } catch (err) {
3170
4335
  spinner.fail(err.message);
@@ -3172,7 +4337,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3172
4337
  return;
3173
4338
  }
3174
4339
  } else {
3175
- logger.info(` ${icon.pass} ${providerName} design ${chalk7.dim(designRef)}`);
4340
+ logger.info(` ${icon.pass} ${providerName} design ${chalk10.dim(designRef)}`);
3176
4341
  }
3177
4342
  const blockName = options.name || (figma2 ? inferBlockName(figma2.fileName) : "");
3178
4343
  if (!kebabCaseRegex.test(blockName)) {
@@ -3195,20 +4360,20 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3195
4360
  screenshot: options.screenshot !== false
3196
4361
  });
3197
4362
  const promptFile = await savePromptToFile(prompt, projectRoot);
3198
- const hasTokens = existsSync14(path17.join(projectRoot, "styles", "styles.css"));
4363
+ const hasTokens = existsSync20(path23.join(projectRoot, "styles", "styles.css"));
3199
4364
  promptSpinner.succeed(
3200
- hasTokens ? `Loaded design tokens ${chalk7.dim("styles/styles.css")}` : `Built prompt ${chalk7.dim("no project tokens found")}`
4365
+ hasTokens ? `Loaded design tokens ${chalk10.dim("styles/styles.css")}` : `Built prompt ${chalk10.dim("no project tokens found")}`
3201
4366
  );
3202
- logger.info(chalk7.dim(` prompt \u2192 ${promptFile}`));
4367
+ logger.info(chalk10.dim(` prompt \u2192 ${promptFile}`));
3203
4368
  if (options.dryRun) {
3204
4369
  logger.info("");
3205
- logger.info(chalk7.yellow("Dry run mode \u2014 prompt generated but agent not invoked."));
3206
- logger.info(`Prompt file: ${chalk7.cyan(promptFile)}`);
3207
- logger.info(`Block name: ${chalk7.cyan(blockName)}`);
4370
+ logger.info(chalk10.yellow("Dry run mode \u2014 prompt generated but agent not invoked."));
4371
+ logger.info(`Prompt file: ${chalk10.cyan(promptFile)}`);
4372
+ logger.info(`Block name: ${chalk10.cyan(blockName)}`);
3208
4373
  logger.info(`Prompt version: ${PROMPT_VERSION}`);
3209
4374
  logger.info("");
3210
4375
  logger.info("Preview:");
3211
- logger.info(chalk7.dim("\u2500".repeat(60)));
4376
+ logger.info(chalk10.dim("\u2500".repeat(60)));
3212
4377
  console.log(prompt);
3213
4378
  return;
3214
4379
  }
@@ -3216,22 +4381,22 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3216
4381
  const preferredAgent = options.agent || config.agent;
3217
4382
  if (options.agent === "none") {
3218
4383
  logger.info("Agent skipped (--agent none). Use the prompt file manually:");
3219
- logger.info(` ${chalk7.cyan(promptFile)}`);
4384
+ logger.info(` ${chalk10.cyan(promptFile)}`);
3220
4385
  return;
3221
4386
  }
3222
4387
  const agentSpinner = ora("Detecting AI agent...").start();
3223
4388
  const agent = await detectAgent(preferredAgent);
3224
4389
  if (!agent) {
3225
4390
  agentSpinner.fail("No AI agent found (claude, cursor, or codex).");
3226
- logger.info(`Install Claude Code: ${chalk7.cyan("npm install -g @anthropic-ai/claude-code")}`);
3227
- logger.info(`Or use the prompt file manually: ${chalk7.cyan(promptFile)}`);
4391
+ logger.info(`Install Claude Code: ${chalk10.cyan("npm install -g @anthropic-ai/claude-code")}`);
4392
+ logger.info(`Or use the prompt file manually: ${chalk10.cyan(promptFile)}`);
3228
4393
  process.exitCode = 1;
3229
4394
  return;
3230
4395
  }
3231
- agentSpinner.succeed(`Agent ready ${chalk7.cyan(agent.type)} ${chalk7.dim(agent.path)}`);
4396
+ agentSpinner.succeed(`Agent ready ${chalk10.cyan(agent.type)} ${chalk10.dim(agent.path)}`);
3232
4397
  if (provider === "figma" && !agent.hasFigmaMcp) {
3233
4398
  logger.warn(`Figma MCP may not be configured for ${agent.type}.`);
3234
- logger.info(`Run ${chalk7.cyan("eds figma setup")} to configure it.`);
4399
+ logger.info(`Run ${chalk10.cyan("eds figma setup")} to configure it.`);
3235
4400
  }
3236
4401
  if (!options.yes) {
3237
4402
  const { confirm } = await import("@inquirer/prompts");
@@ -3240,24 +4405,24 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3240
4405
  default: true
3241
4406
  });
3242
4407
  if (!proceed) {
3243
- logger.info(`Prompt saved to: ${chalk7.cyan(promptFile)}`);
4408
+ logger.info(`Prompt saved to: ${chalk10.cyan(promptFile)}`);
3244
4409
  return;
3245
4410
  }
3246
4411
  }
3247
4412
  logger.info(
3248
- ` ${chalk7.dim("\u2192")} Handing off to ${chalk7.cyan(agent.type)} ${chalk7.dim(`via ${providerName} MCP`)}`
4413
+ ` ${chalk10.dim("\u2192")} Handing off to ${chalk10.cyan(agent.type)} ${chalk10.dim(`via ${providerName} MCP`)}`
3249
4414
  );
3250
4415
  if (provider === "figma") {
3251
- logger.info(chalk7.dim(" get_design_context \xB7 get_variable_defs \xB7 get_screenshot"));
4416
+ logger.info(chalk10.dim(" get_design_context \xB7 get_variable_defs \xB7 get_screenshot"));
3252
4417
  }
3253
4418
  const success = await invokeAgent(agent, promptFile, projectRoot);
3254
4419
  if (!success) {
3255
4420
  process.exitCode = 1;
3256
4421
  return;
3257
4422
  }
3258
- const blockDir = path17.join(projectRoot, "blocks", blockName);
4423
+ const blockDir = path23.join(projectRoot, "blocks", blockName);
3259
4424
  const ok = await verifyGeneratedBlock(blockDir, blockName, options.ueModel !== false);
3260
- if (existsSync14(blockDir)) {
4425
+ if (existsSync20(blockDir)) {
3261
4426
  const meta = {
3262
4427
  provider,
3263
4428
  designRef,
@@ -3267,7 +4432,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3267
4432
  promptVersion: PROMPT_VERSION,
3268
4433
  agentUsed: agent.type
3269
4434
  };
3270
- await writeFile8(path17.join(blockDir, ".eds-meta.json"), `${JSON.stringify(meta, null, 2)}
4435
+ await writeFile13(path23.join(blockDir, ".eds-meta.json"), `${JSON.stringify(meta, null, 2)}
3271
4436
  `);
3272
4437
  }
3273
4438
  if (!ok) {
@@ -3278,10 +4443,21 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3278
4443
  return;
3279
4444
  }
3280
4445
  logger.success(`Block "${blockName}" generation complete.`);
4446
+ if (!options.yes) {
4447
+ const { confirm } = await import("@inquirer/prompts");
4448
+ const addTracking = await confirm({
4449
+ message: "Instrument this block for analytics (dataLayer)?",
4450
+ default: false
4451
+ });
4452
+ if (addTracking) {
4453
+ const { trackBlock: trackBlock2 } = await Promise.resolve().then(() => (init_track2(), track_exports));
4454
+ await trackBlock2(blockName, {});
4455
+ }
4456
+ }
3281
4457
  if (options.serve !== false) {
3282
4458
  if (await previewRunning()) {
3283
4459
  logger.info(
3284
- chalk7.dim(
4460
+ chalk10.dim(
3285
4461
  " A preview server is already running at http://127.0.0.1:8777 \u2014 your change will refresh there."
3286
4462
  )
3287
4463
  );
@@ -3291,15 +4467,15 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3291
4467
  await previewBlock2(blockName, {});
3292
4468
  }
3293
4469
  } else {
3294
- logger.info(` ${chalk7.dim("\u2192")} Next: ${chalk7.cyan(`eds block preview ${blockName}`)}`);
4470
+ logger.info(` ${chalk10.dim("\u2192")} Next: ${chalk10.cyan(`eds block preview ${blockName}`)}`);
3295
4471
  }
3296
4472
  }
3297
4473
  async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3298
4474
  const checks = [];
3299
- const jsPath = path17.join(blockDir, `${blockName}.js`);
3300
- const cssPath = path17.join(blockDir, `${blockName}.css`);
3301
- const hasJs = existsSync14(jsPath);
3302
- const hasCss = existsSync14(cssPath);
4475
+ const jsPath = path23.join(blockDir, `${blockName}.js`);
4476
+ const cssPath = path23.join(blockDir, `${blockName}.css`);
4477
+ const hasJs = existsSync20(jsPath);
4478
+ const hasCss = existsSync20(cssPath);
3303
4479
  checks.push({
3304
4480
  status: hasJs ? "pass" : "fail",
3305
4481
  name: `${blockName}.js`,
@@ -3311,7 +4487,7 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3311
4487
  message: hasCss ? "created" : "missing \u2014 agent did not write the CSS file"
3312
4488
  });
3313
4489
  if (hasJs) {
3314
- const js = await readFile10(jsPath, "utf-8");
4490
+ const js = await readFile15(jsPath, "utf-8");
3315
4491
  checks.push({
3316
4492
  status: /export\s+default/.test(js) ? "pass" : "fail",
3317
4493
  name: "decorate export",
@@ -3326,7 +4502,7 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3326
4502
  }
3327
4503
  }
3328
4504
  if (hasCss) {
3329
- const css = await readFile10(cssPath, "utf-8");
4505
+ const css = await readFile15(cssPath, "utf-8");
3330
4506
  checks.push({
3331
4507
  status: css.includes(`.${blockName}`) ? "pass" : "warn",
3332
4508
  name: "scoped CSS",
@@ -3334,12 +4510,12 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3334
4510
  });
3335
4511
  }
3336
4512
  if (withUeModel) {
3337
- const hasPerBlock = existsSync14(path17.join(blockDir, `_${blockName}.json`));
3338
- const modelsFile = path17.join(blockDir, "..", "..", "component-models.json");
4513
+ const hasPerBlock = existsSync20(path23.join(blockDir, `_${blockName}.json`));
4514
+ const modelsFile = path23.join(blockDir, "..", "..", "component-models.json");
3339
4515
  let hasAggregated = false;
3340
- if (!hasPerBlock && existsSync14(modelsFile)) {
4516
+ if (!hasPerBlock && existsSync20(modelsFile)) {
3341
4517
  try {
3342
- hasAggregated = (await readFile10(modelsFile, "utf-8")).includes(`"${blockName}"`);
4518
+ hasAggregated = (await readFile15(modelsFile, "utf-8")).includes(`"${blockName}"`);
3343
4519
  } catch {
3344
4520
  }
3345
4521
  }
@@ -3351,8 +4527,8 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3351
4527
  });
3352
4528
  if (hasPerBlock && hasJs) {
3353
4529
  try {
3354
- const modelRaw = await readFile10(path17.join(blockDir, `_${blockName}.json`), "utf-8");
3355
- const js = await readFile10(jsPath, "utf-8");
4530
+ const modelRaw = await readFile15(path23.join(blockDir, `_${blockName}.json`), "utf-8");
4531
+ const js = await readFile15(jsPath, "utf-8");
3356
4532
  const isRepeatable = /"resourceType"\s*:\s*"[^"]*\/item"/.test(modelRaw) || /"filter"\s*:/.test(modelRaw) || /"multi"\s*:\s*true/.test(modelRaw);
3357
4533
  const iterates = /\.children/.test(js) || /\bfor\s*\(/.test(js) || /\.forEach\s*\(/.test(js);
3358
4534
  if (isRepeatable) {
@@ -3395,10 +4571,10 @@ var list_exports = {};
3395
4571
  __export(list_exports, {
3396
4572
  listBlocks: () => listBlocks
3397
4573
  });
3398
- import { existsSync as existsSync15 } from "fs";
3399
- import { readFile as readFile11, readdir as readdir2 } from "fs/promises";
3400
- import path18 from "path";
3401
- import chalk8 from "chalk";
4574
+ import { existsSync as existsSync21 } from "fs";
4575
+ import { readFile as readFile16, readdir as readdir3 } from "fs/promises";
4576
+ import path24 from "path";
4577
+ import chalk11 from "chalk";
3402
4578
  async function listBlocks(options = {}) {
3403
4579
  const projectRoot = findProjectRoot();
3404
4580
  if (!projectRoot) {
@@ -3406,27 +4582,27 @@ async function listBlocks(options = {}) {
3406
4582
  process.exitCode = 1;
3407
4583
  return;
3408
4584
  }
3409
- const blocksDir = path18.join(projectRoot, "blocks");
3410
- if (!existsSync15(blocksDir)) {
4585
+ const blocksDir = path24.join(projectRoot, "blocks");
4586
+ if (!existsSync21(blocksDir)) {
3411
4587
  logger.info("No blocks/ directory found.");
3412
4588
  return;
3413
4589
  }
3414
- const entries = await readdir2(blocksDir, { withFileTypes: true });
4590
+ const entries = await readdir3(blocksDir, { withFileTypes: true });
3415
4591
  const blocks = [];
3416
4592
  for (const entry of entries) {
3417
4593
  if (!entry.isDirectory()) continue;
3418
4594
  const name = entry.name;
3419
- const blockDir = path18.join(blocksDir, name);
4595
+ const blockDir = path24.join(blocksDir, name);
3420
4596
  const info = {
3421
4597
  name,
3422
- hasJs: existsSync15(path18.join(blockDir, `${name}.js`)),
3423
- hasCss: existsSync15(path18.join(blockDir, `${name}.css`)),
3424
- hasUeModel: existsSync15(path18.join(blockDir, `_${name}.json`))
4598
+ hasJs: existsSync21(path24.join(blockDir, `${name}.js`)),
4599
+ hasCss: existsSync21(path24.join(blockDir, `${name}.css`)),
4600
+ hasUeModel: existsSync21(path24.join(blockDir, `_${name}.json`))
3425
4601
  };
3426
- const metaFile = path18.join(blockDir, ".eds-meta.json");
3427
- if (existsSync15(metaFile)) {
4602
+ const metaFile = path24.join(blockDir, ".eds-meta.json");
4603
+ if (existsSync21(metaFile)) {
3428
4604
  try {
3429
- const meta = JSON.parse(await readFile11(metaFile, "utf-8"));
4605
+ const meta = JSON.parse(await readFile16(metaFile, "utf-8"));
3430
4606
  info.figmaSource = {
3431
4607
  fileKey: meta.figmaFileKey,
3432
4608
  nodeId: meta.figmaNodeId,
@@ -3449,17 +4625,17 @@ async function listBlocks(options = {}) {
3449
4625
  const nameWidth = columnWidth(blocks.map((b) => b.name));
3450
4626
  for (const b of blocks) {
3451
4627
  const badges = [
3452
- b.hasJs ? brand.ok("js") : chalk8.dim.strikethrough("js"),
3453
- b.hasCss ? brand.ok("css") : chalk8.dim.strikethrough("css")
4628
+ b.hasJs ? brand.ok("js") : chalk11.dim.strikethrough("js"),
4629
+ b.hasCss ? brand.ok("css") : chalk11.dim.strikethrough("css")
3454
4630
  ];
3455
4631
  if (b.hasUeModel) badges.push(brand.blue("ue-model"));
3456
4632
  if (b.figmaSource) badges.push(`${icon.figma} ${brand.dotPurple("figma")}`);
3457
- const name = padEndVisible(chalk8.bold(b.name), nameWidth);
3458
- logger.info(` ${name} ${chalk8.dim("[")}${badges.join(" ")}${chalk8.dim("]")}`);
4633
+ const name = padEndVisible(chalk11.bold(b.name), nameWidth);
4634
+ logger.info(` ${name} ${chalk11.dim("[")}${badges.join(" ")}${chalk11.dim("]")}`);
3459
4635
  if (b.figmaSource) {
3460
4636
  const synced = new Date(b.figmaSource.lastSyncedAt).toLocaleDateString();
3461
4637
  logger.info(
3462
- ` ${" ".repeat(nameWidth)} ${icon.arrow} ${chalk8.dim(
4638
+ ` ${" ".repeat(nameWidth)} ${icon.arrow} ${chalk11.dim(
3463
4639
  `figma:${b.figmaSource.fileKey} \xB7 synced ${synced}`
3464
4640
  )}`
3465
4641
  );
@@ -3499,22 +4675,85 @@ var init_integrations = __esm({
3499
4675
  id: "gtm",
3500
4676
  name: "Google Tag Manager",
3501
4677
  fields: [{ name: "id", message: "GTM container ID", placeholder: "GTM-XXXXXXX" }],
3502
- code: (v) => `// Google Tag Manager (${v.id}) \u2014 delayed phase, after LCP.
3503
- (function (w, d, s, l, i) {
3504
- w[l] = w[l] || [];
3505
- w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
3506
- const f = d.getElementsByTagName(s)[0];
3507
- const j = d.createElement(s);
3508
- j.async = true;
3509
- j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i;
3510
- f.parentNode.insertBefore(j, f);
3511
- })(window, document, 'script', 'dataLayer', '${v.id}');`
4678
+ // delayed.js imports the loader + page-meta, pushes page metadata, then GTM.
4679
+ code: () => `// Google Tag Manager - loaded from ./analytics.js (delayed phase, after LCP).
4680
+ import { loadGTM } from './analytics.js';
4681
+ import pageMetaPush from './page-meta.js';
4682
+
4683
+ const pageMetaReady = pageMetaPush();
4684
+ pageMetaReady.then(loadGTM);`,
4685
+ module: (v) => [
4686
+ {
4687
+ file: "scripts/analytics.js",
4688
+ content: `// scripts/analytics.js - Google Tag Manager (generated by \`eds integrate gtm\`).
4689
+ // The container ID comes from the \`gtm-id\` metadata, falling back to the one
4690
+ // configured here. Import { trackEvent } from './analytics.js' to push events.
4691
+
4692
+ const FALLBACK_ID = ${JSON.stringify(v.id || "")};
4693
+
4694
+ function getMetadata(name) {
4695
+ const meta = document.querySelector(\`meta[name="\${name}"]\`);
4696
+ return meta ? meta.content : '';
4697
+ }
4698
+
4699
+ function isDev() {
4700
+ const h = window.location.hostname;
4701
+ return h === 'localhost' || h === '127.0.0.1' || h.endsWith('.aem.page') || h.endsWith('.hlx.page');
4702
+ }
4703
+
4704
+ // dataLayer must exist before anything pushes to it (trackEvent may run before loadGTM).
4705
+ window.dataLayer = window.dataLayer || [];
4706
+
4707
+ export function trackEvent(event) {
4708
+ if (isDev()) {
4709
+ // eslint-disable-next-line no-console
4710
+ console.log('[gtm]', JSON.stringify(event, null, 2));
4711
+ }
4712
+ window.dataLayer.push(event);
4713
+ }
4714
+
4715
+ let loaded = false;
4716
+
4717
+ export function loadGTM() {
4718
+ if (loaded) return;
4719
+ const raw = getMetadata('gtm-id') || FALLBACK_ID;
4720
+ if (!raw) return;
4721
+ loaded = true;
4722
+
4723
+ const id = raw.startsWith('GTM-') ? raw : \`GTM-\${raw}\`;
4724
+ window.dataLayer.push({ 'gtm.start': Date.now(), event: 'gtm.js' });
4725
+
4726
+ const script = document.createElement('script');
4727
+ script.async = true;
4728
+ script.src = \`https://www.googletagmanager.com/gtm.js?id=\${id}\`;
4729
+ document.head.append(script);
4730
+ }
4731
+ `
4732
+ },
4733
+ {
4734
+ file: "scripts/page-meta.js",
4735
+ content: `// scripts/page-meta.js - pushes page-level metadata to the dataLayer before
4736
+ // GTM initialises. Extend \`pageMeta\` with the fields your container expects.
4737
+ import { trackEvent } from './analytics.js';
4738
+
4739
+ export default async function pageMetaPush() {
4740
+ const pageMeta = {
4741
+ event: 'page_meta',
4742
+ };
4743
+
4744
+ trackEvent(pageMeta);
4745
+
4746
+ return pageMeta;
4747
+ }
4748
+ `
4749
+ }
4750
+ ]
3512
4751
  },
3513
4752
  {
3514
4753
  id: "ga4",
3515
4754
  name: "Google Analytics 4",
3516
4755
  fields: [{ name: "id", message: "Measurement ID", placeholder: "G-XXXXXXXXXX" }],
3517
- code: (v) => `// Google Analytics 4 (${v.id}) \u2014 delayed phase.
4756
+ code: (v) => `// Google Analytics 4 (${v.id}) - delayed phase.
3518
4757
  const gaScript = document.createElement('script');
3519
4758
  gaScript.async = true;
3520
4759
  gaScript.src = 'https://www.googletagmanager.com/gtag/js?id=${v.id}';
@@ -3531,7 +4770,7 @@ gtag('config', '${v.id}');`
3531
4770
  { name: "label", message: "Button label", placeholder: "Chat with us" },
3532
4771
  { name: "src", message: "Chat widget script URL" }
3533
4772
  ],
3534
- code: (v) => `// Chat widget \u2014 facade pattern: load the heavy script only on first interaction.
4773
+ code: (v) => `// Chat widget - facade pattern: load the heavy script only on first interaction.
3535
4774
  const chatBtn = document.createElement('button');
3536
4775
  chatBtn.type = 'button';
3537
4776
  chatBtn.className = 'chat-facade';
@@ -3549,7 +4788,7 @@ document.body.appendChild(chatBtn);`
3549
4788
  id: "cookiebot",
3550
4789
  name: "Cookie consent (Cookiebot)",
3551
4790
  fields: [{ name: "id", message: "Cookiebot domain group ID (CBID)" }],
3552
- code: (v) => `// Cookiebot consent \u2014 load early in delayed; configure GTM consent mode to respect it.
4791
+ code: (v) => `// Cookiebot consent - load early in delayed; configure GTM consent mode to respect it.
3553
4792
  const cb = document.createElement('script');
3554
4793
  cb.id = 'Cookiebot';
3555
4794
  cb.src = 'https://consent.cookiebot.com/uc.js';
@@ -3561,7 +4800,7 @@ document.head.appendChild(cb);`
3561
4800
  id: "custom",
3562
4801
  name: "Custom third-party script",
3563
4802
  fields: [{ name: "url", message: "Script URL" }],
3564
- code: (v) => `// Custom third-party script \u2014 delayed phase.
4803
+ code: (v) => `// Custom third-party script - delayed phase.
3565
4804
  const s = document.createElement('script');
3566
4805
  s.async = true;
3567
4806
  s.src = ${JSON.stringify(v.url || "")};
@@ -3576,10 +4815,10 @@ var integrate_exports = {};
3576
4815
  __export(integrate_exports, {
3577
4816
  integrate: () => integrate
3578
4817
  });
3579
- import { existsSync as existsSync16 } from "fs";
3580
- import { mkdir as mkdir6, readFile as readFile12, writeFile as writeFile9 } from "fs/promises";
3581
- import path19 from "path";
3582
- import chalk9 from "chalk";
4818
+ import { existsSync as existsSync22 } from "fs";
4819
+ import { mkdir as mkdir8, readFile as readFile17, writeFile as writeFile14 } from "fs/promises";
4820
+ import path25 from "path";
4821
+ import chalk12 from "chalk";
3583
4822
  async function integrate(type) {
3584
4823
  const projectRoot = findProjectRoot();
3585
4824
  if (!projectRoot) {
@@ -3587,7 +4826,7 @@ async function integrate(type) {
3587
4826
  process.exitCode = 1;
3588
4827
  return;
3589
4828
  }
3590
- logger.logoOnce(logo("Integrate \u2014 third-party service"));
4829
+ logger.logoOnce(logo("Integrate - third-party service"));
3591
4830
  const { select, input } = await import("@inquirer/prompts");
3592
4831
  const chosenId = type || await select({
3593
4832
  message: "Which service do you want to add?",
@@ -3602,30 +4841,43 @@ async function integrate(type) {
3602
4841
  return;
3603
4842
  }
3604
4843
  const values = {};
3605
- for (const f of integration.fields) {
3606
- const answer = await input({ message: ` ${f.message}`, default: f.placeholder });
3607
- values[f.name] = answer.trim();
3608
- }
3609
- const block2 = wrapBlock(integration.name, integration.code(values));
3610
- const delayedPath = path19.join(projectRoot, "scripts", "delayed.js");
3611
- await mkdir6(path19.dirname(delayedPath), { recursive: true });
3612
- const existing = existsSync16(delayedPath) ? await readFile12(delayedPath, "utf-8") : DELAYED_HEADER;
3613
- await writeFile9(delayedPath, `${existing.replace(/\s*$/, "")}
4844
+ for (const f2 of integration.fields) {
4845
+ const answer = await input({ message: ` ${f2.message}`, default: f2.placeholder });
4846
+ values[f2.name] = answer.trim();
4847
+ }
4848
+ const modulePaths = [];
4849
+ if (integration.module) {
4850
+ for (const mod of integration.module(values)) {
4851
+ const abs = path25.join(projectRoot, mod.file);
4852
+ await mkdir8(path25.dirname(abs), { recursive: true });
4853
+ await writeFile14(abs, mod.content);
4854
+ modulePaths.push(mod.file);
4855
+ }
4856
+ }
4857
+ const block2 = integration.module ? `
4858
+ // ${integration.name} (eds integrate)
4859
+ ${integration.code(values)}
4860
+ ` : wrapBlock(integration.name, integration.code(values));
4861
+ const delayedPath = path25.join(projectRoot, "scripts", "delayed.js");
4862
+ await mkdir8(path25.dirname(delayedPath), { recursive: true });
4863
+ const existing = existsSync22(delayedPath) ? await readFile17(delayedPath, "utf-8") : DELAYED_HEADER;
4864
+ await writeFile14(delayedPath, `${existing.replace(/\s*$/, "")}
3614
4865
  ${block2}`);
3615
4866
  logger.info(heading("Added"));
3616
- logger.info(accentLine(integration.name, "\u2192 scripts/delayed.js"));
4867
+ for (const p of modulePaths) logger.info(accentLine(integration.name, `wrote ${p}`));
4868
+ logger.info(accentLine(integration.name, "wired into scripts/delayed.js"));
3617
4869
  logger.info("");
3618
4870
  logger.info(
3619
- chalk9.dim(" Loads after LCP (delayed phase) \u2014 zero impact on your Core Web Vitals.")
4871
+ chalk12.dim(" Loads after LCP (delayed phase) - zero impact on your Core Web Vitals.")
3620
4872
  );
3621
4873
  if (integration.id === "gtm" || integration.id === "ga4") {
3622
4874
  logger.info(
3623
- chalk9.dim(" Tip: gate analytics behind user consent (add the Cookiebot integration).")
4875
+ chalk12.dim(" Tip: gate analytics behind user consent (add the Cookiebot integration).")
3624
4876
  );
3625
4877
  }
3626
4878
  if (integration.id === "chat") {
3627
4879
  logger.info(
3628
- chalk9.dim(" The widget loads only when the visitor clicks the button (facade pattern).")
4880
+ chalk12.dim(" The widget loads only when the visitor clicks the button (facade pattern).")
3629
4881
  );
3630
4882
  }
3631
4883
  logger.info("");
@@ -3640,7 +4892,7 @@ var init_integrate = __esm({
3640
4892
  init_logger();
3641
4893
  init_project();
3642
4894
  init_ui();
3643
- DELAYED_HEADER = `// delayed.js \u2014 third-party / non-critical scripts.
4895
+ DELAYED_HEADER = `// delayed.js - third-party / non-critical scripts.
3644
4896
  // Loaded in the EDS delayed phase (after LCP), so nothing here blocks the page.
3645
4897
  `;
3646
4898
  }
@@ -3651,9 +4903,9 @@ var setup_exports = {};
3651
4903
  __export(setup_exports, {
3652
4904
  figmaSetup: () => figmaSetup
3653
4905
  });
3654
- import { existsSync as existsSync17 } from "fs";
3655
- import { mkdir as mkdir7, writeFile as writeFile10 } from "fs/promises";
3656
- import chalk10 from "chalk";
4906
+ import { existsSync as existsSync23 } from "fs";
4907
+ import { mkdir as mkdir9, writeFile as writeFile15 } from "fs/promises";
4908
+ import chalk13 from "chalk";
3657
4909
  async function figmaSetup() {
3658
4910
  const { select, confirm } = await import("@inquirer/prompts");
3659
4911
  logger.logoOnce(logo());
@@ -3663,15 +4915,15 @@ async function figmaSetup() {
3663
4915
  let selectedAgent;
3664
4916
  if (agents.length === 0) {
3665
4917
  logger.warn("No AI agents found (claude, cursor, codex).");
3666
- logger.info(`Install Claude Code: ${chalk10.cyan("npm install -g @anthropic-ai/claude-code")}`);
4918
+ logger.info(`Install Claude Code: ${chalk13.cyan("npm install -g @anthropic-ai/claude-code")}`);
3667
4919
  selectedAgent = "none";
3668
4920
  } else {
3669
- logger.success(`Found: ${agents.map((a) => chalk10.cyan(a.type)).join(", ")}`);
4921
+ logger.success(`Found: ${agents.map((a) => chalk13.cyan(a.type)).join(", ")}`);
3670
4922
  selectedAgent = await select({
3671
4923
  message: "Which agent do you want to use with eds-cli?",
3672
4924
  choices: [
3673
4925
  ...agents.map((a) => ({
3674
- name: `${a.type}${a.hasFigmaMcp ? chalk10.green(" (Figma MCP detected)") : ""}`,
4926
+ name: `${a.type}${a.hasFigmaMcp ? chalk13.green(" (Figma MCP detected)") : ""}`,
3675
4927
  value: a.type
3676
4928
  })),
3677
4929
  { name: "None (manual mode)", value: "none" }
@@ -3687,17 +4939,17 @@ async function figmaSetup() {
3687
4939
  switch (selectedAgent) {
3688
4940
  case "claude":
3689
4941
  logger.info(
3690
- ` ${chalk10.cyan("claude mcp add --transport sse figma-sse https://mcp.figma.com/sse")}`
4942
+ ` ${chalk13.cyan("claude mcp add --transport sse figma-sse https://mcp.figma.com/sse")}`
3691
4943
  );
3692
4944
  logger.info(" or for Figma Desktop app:");
3693
4945
  logger.info(
3694
- ` ${chalk10.cyan("claude mcp add --transport http figma-desktop http://127.0.0.1:3845/mcp")}`
4946
+ ` ${chalk13.cyan("claude mcp add --transport http figma-desktop http://127.0.0.1:3845/mcp")}`
3695
4947
  );
3696
4948
  break;
3697
4949
  case "cursor":
3698
- logger.info(` Add to ${chalk10.cyan("~/.cursor/mcp.json")}:`);
4950
+ logger.info(` Add to ${chalk13.cyan("~/.cursor/mcp.json")}:`);
3699
4951
  logger.info(
3700
- chalk10.dim(
4952
+ chalk13.dim(
3701
4953
  JSON.stringify(
3702
4954
  {
3703
4955
  mcpServers: {
@@ -3722,7 +4974,7 @@ async function figmaSetup() {
3722
4974
  }
3723
4975
  }
3724
4976
  logger.info(
3725
- chalk10.dim(
4977
+ chalk13.dim(
3726
4978
  "\nNote: Figma MCP requires a Figma seat (Dev or Full on paid plans for desktop server)."
3727
4979
  )
3728
4980
  );
@@ -3732,22 +4984,22 @@ async function figmaSetup() {
3732
4984
  });
3733
4985
  if (shouldSave) {
3734
4986
  const configDir = globalConfigDir();
3735
- if (!existsSync17(configDir)) {
3736
- await mkdir7(configDir, { recursive: true });
4987
+ if (!existsSync23(configDir)) {
4988
+ await mkdir9(configDir, { recursive: true });
3737
4989
  }
3738
4990
  const config = {};
3739
4991
  if (selectedAgent !== "none") {
3740
4992
  config.agent = selectedAgent;
3741
4993
  }
3742
- await writeFile10(globalConfigPath(), `${JSON.stringify(config, null, 2)}
4994
+ await writeFile15(globalConfigPath(), `${JSON.stringify(config, null, 2)}
3743
4995
  `, {
3744
4996
  mode: 384
3745
4997
  });
3746
- logger.success(`Config saved to ${chalk10.cyan(globalConfigPath())}`);
4998
+ logger.success(`Config saved to ${chalk13.cyan(globalConfigPath())}`);
3747
4999
  }
3748
- logger.info(chalk10.bold("\nSetup complete!"));
5000
+ logger.info(chalk13.bold("\nSetup complete!"));
3749
5001
  if (selectedAgent !== "none") {
3750
- logger.info(`Try: ${chalk10.cyan('eds block from-design "<figma-url>" --dry-run')}`);
5002
+ logger.info(`Try: ${chalk13.cyan('eds block from-design "<figma-url>" --dry-run')}`);
3751
5003
  }
3752
5004
  }
3753
5005
  var init_setup = __esm({
@@ -3766,10 +5018,10 @@ var doctor_exports = {};
3766
5018
  __export(doctor_exports, {
3767
5019
  doctor: () => doctor
3768
5020
  });
3769
- import { existsSync as existsSync18 } from "fs";
3770
- import { readFile as readFile13, readdir as readdir3 } from "fs/promises";
3771
- import path20 from "path";
3772
- import { parse as parseYaml } from "yaml";
5021
+ import { existsSync as existsSync24 } from "fs";
5022
+ import { readFile as readFile18, readdir as readdir4 } from "fs/promises";
5023
+ import path26 from "path";
5024
+ import { parse as parseYaml2 } from "yaml";
3773
5025
  async function doctor(options = {}) {
3774
5026
  const projectRoot = findProjectRoot();
3775
5027
  if (!projectRoot) {
@@ -3790,8 +5042,8 @@ async function doctor(options = {}) {
3790
5042
  });
3791
5043
  if (project.hasFstab) {
3792
5044
  try {
3793
- const content = await readFile13(path20.join(projectRoot, "fstab.yaml"), "utf-8");
3794
- parseYaml(content);
5045
+ const content = await readFile18(path26.join(projectRoot, "fstab.yaml"), "utf-8");
5046
+ parseYaml2(content);
3795
5047
  results.push({ name: "fstab.yaml valid YAML", status: "pass", message: "Valid" });
3796
5048
  } catch {
3797
5049
  results.push({
@@ -3812,28 +5064,28 @@ async function doctor(options = {}) {
3812
5064
  message: project.hasBlocks ? "Found" : "Missing - no blocks yet"
3813
5065
  });
3814
5066
  if (project.hasBlocks) {
3815
- const blocksDir = path20.join(projectRoot, "blocks");
3816
- const entries = await readdir3(blocksDir, { withFileTypes: true });
5067
+ const blocksDir = path26.join(projectRoot, "blocks");
5068
+ const entries = await readdir4(blocksDir, { withFileTypes: true });
3817
5069
  let aggregatedModels = "";
3818
- const aggregatedPath = path20.join(projectRoot, "component-models.json");
3819
- if (existsSync18(aggregatedPath)) {
5070
+ const aggregatedPath = path26.join(projectRoot, "component-models.json");
5071
+ if (existsSync24(aggregatedPath)) {
3820
5072
  try {
3821
- aggregatedModels = await readFile13(aggregatedPath, "utf-8");
5073
+ aggregatedModels = await readFile18(aggregatedPath, "utf-8");
3822
5074
  } catch {
3823
5075
  }
3824
5076
  }
3825
5077
  for (const entry of entries) {
3826
5078
  if (!entry.isDirectory()) continue;
3827
5079
  const blockName = entry.name;
3828
- const jsFile = path20.join(blocksDir, blockName, `${blockName}.js`);
3829
- const hasJs = existsSync18(jsFile);
5080
+ const jsFile = path26.join(blocksDir, blockName, `${blockName}.js`);
5081
+ const hasJs = existsSync24(jsFile);
3830
5082
  results.push({
3831
5083
  name: `blocks/${blockName} has JS`,
3832
5084
  status: hasJs ? "pass" : "warn",
3833
5085
  message: hasJs ? "Found" : `Missing ${blockName}.js`
3834
5086
  });
3835
5087
  if (hasJs) {
3836
- const jsContent = await readFile13(jsFile, "utf-8");
5088
+ const jsContent = await readFile18(jsFile, "utf-8");
3837
5089
  if (jsContent.includes("console.log")) {
3838
5090
  results.push({
3839
5091
  name: `blocks/${blockName} no console.log`,
@@ -3842,17 +5094,17 @@ async function doctor(options = {}) {
3842
5094
  });
3843
5095
  }
3844
5096
  }
3845
- const hasPerBlockModel = existsSync18(path20.join(blocksDir, blockName, `_${blockName}.json`));
5097
+ const hasPerBlockModel = existsSync24(path26.join(blocksDir, blockName, `_${blockName}.json`));
3846
5098
  const hasAggregatedModel = aggregatedModels.includes(`"${blockName}"`);
3847
5099
  results.push({
3848
5100
  name: `blocks/${blockName} UE model`,
3849
5101
  status: hasPerBlockModel || hasAggregatedModel ? "pass" : "warn",
3850
5102
  message: hasPerBlockModel || hasAggregatedModel ? hasPerBlockModel ? `_${blockName}.json` : "in component-models.json" : "no Universal Editor model - authors can't edit this block in UE"
3851
5103
  });
3852
- const metaFile = path20.join(blocksDir, blockName, ".eds-meta.json");
3853
- if (existsSync18(metaFile)) {
5104
+ const metaFile = path26.join(blocksDir, blockName, ".eds-meta.json");
5105
+ if (existsSync24(metaFile)) {
3854
5106
  try {
3855
- const meta = JSON.parse(await readFile13(metaFile, "utf-8"));
5107
+ const meta = JSON.parse(await readFile18(metaFile, "utf-8"));
3856
5108
  const syncDate = new Date(meta.lastSyncedAt);
3857
5109
  const daysSinceSync = Math.floor(
3858
5110
  (Date.now() - syncDate.getTime()) / (1e3 * 60 * 60 * 24)
@@ -3872,11 +5124,11 @@ async function doctor(options = {}) {
3872
5124
  }
3873
5125
  }
3874
5126
  }
3875
- const queryFile = path20.join(projectRoot, "helix-query.yaml");
3876
- if (existsSync18(queryFile)) {
5127
+ const queryFile = path26.join(projectRoot, "helix-query.yaml");
5128
+ if (existsSync24(queryFile)) {
3877
5129
  try {
3878
- const content = await readFile13(queryFile, "utf-8");
3879
- parseYaml(content);
5130
+ const content = await readFile18(queryFile, "utf-8");
5131
+ parseYaml2(content);
3880
5132
  results.push({ name: "helix-query.yaml valid", status: "pass", message: "Valid YAML" });
3881
5133
  } catch {
3882
5134
  results.push({
@@ -3932,10 +5184,10 @@ var audit_loading_exports = {};
3932
5184
  __export(audit_loading_exports, {
3933
5185
  auditLoading: () => auditLoading
3934
5186
  });
3935
- import { existsSync as existsSync19 } from "fs";
3936
- import { readFile as readFile14, readdir as readdir4, stat as stat3 } from "fs/promises";
3937
- import path21 from "path";
3938
- import chalk11 from "chalk";
5187
+ import { existsSync as existsSync25 } from "fs";
5188
+ import { readFile as readFile19, readdir as readdir5, stat as stat3 } from "fs/promises";
5189
+ import path27 from "path";
5190
+ import chalk14 from "chalk";
3939
5191
  async function auditLoading(options = {}) {
3940
5192
  const projectRoot = findProjectRoot();
3941
5193
  if (!projectRoot) {
@@ -3968,8 +5220,8 @@ async function auditLoading(options = {}) {
3968
5220
  printResults(results);
3969
5221
  }
3970
5222
  async function checkHeadHtml(root, results) {
3971
- const filePath = path21.join(root, "head.html");
3972
- if (!existsSync19(filePath)) {
5223
+ const filePath = path27.join(root, "head.html");
5224
+ if (!existsSync25(filePath)) {
3973
5225
  results.push({
3974
5226
  rule: "head.html exists",
3975
5227
  status: "warn",
@@ -3977,7 +5229,7 @@ async function checkHeadHtml(root, results) {
3977
5229
  });
3978
5230
  return;
3979
5231
  }
3980
- const content = await readFile14(filePath, "utf-8");
5232
+ const content = await readFile19(filePath, "utf-8");
3981
5233
  const lines = content.split("\n");
3982
5234
  for (let i = 0; i < lines.length; i++) {
3983
5235
  const line = lines[i];
@@ -4049,8 +5301,8 @@ async function checkHeadHtml(root, results) {
4049
5301
  }
4050
5302
  }
4051
5303
  async function checkScriptsJs(root, results) {
4052
- const filePath = path21.join(root, "scripts", "scripts.js");
4053
- if (!existsSync19(filePath)) {
5304
+ const filePath = path27.join(root, "scripts", "scripts.js");
5305
+ if (!existsSync25(filePath)) {
4054
5306
  results.push({
4055
5307
  rule: "scripts/scripts.js exists",
4056
5308
  status: "warn",
@@ -4058,7 +5310,7 @@ async function checkScriptsJs(root, results) {
4058
5310
  });
4059
5311
  return;
4060
5312
  }
4061
- const content = await readFile14(filePath, "utf-8");
5313
+ const content = await readFile19(filePath, "utf-8");
4062
5314
  const lines = content.split("\n");
4063
5315
  const hasLoadEager = /function\s+loadEager|loadEager\s*=/i.test(content);
4064
5316
  const hasLoadLazy = /function\s+loadLazy|loadLazy\s*=/i.test(content);
@@ -4136,8 +5388,8 @@ async function checkScriptsJs(root, results) {
4136
5388
  }
4137
5389
  }
4138
5390
  async function checkStylesCss(root, results) {
4139
- const filePath = path21.join(root, "styles", "styles.css");
4140
- if (!existsSync19(filePath)) return;
5391
+ const filePath = path27.join(root, "styles", "styles.css");
5392
+ if (!existsSync25(filePath)) return;
4141
5393
  const stats = await stat3(filePath);
4142
5394
  const sizeKb = stats.size / 1024;
4143
5395
  results.push({
@@ -4146,7 +5398,7 @@ async function checkStylesCss(root, results) {
4146
5398
  message: `${sizeKb.toFixed(1)} KB${sizeKb > 40 ? " \u2014 large CSS competes with 100KB LCP budget" : ""}`,
4147
5399
  file: "styles/styles.css"
4148
5400
  });
4149
- const content = await readFile14(filePath, "utf-8");
5401
+ const content = await readFile19(filePath, "utf-8");
4150
5402
  if (/@import\s/.test(content)) {
4151
5403
  results.push({
4152
5404
  rule: "No @import in styles.css",
@@ -4157,8 +5409,8 @@ async function checkStylesCss(root, results) {
4157
5409
  }
4158
5410
  }
4159
5411
  async function checkDelayedJs(root, results) {
4160
- const filePath = path21.join(root, "scripts", "delayed.js");
4161
- if (!existsSync19(filePath)) {
5412
+ const filePath = path27.join(root, "scripts", "delayed.js");
5413
+ if (!existsSync25(filePath)) {
4162
5414
  results.push({
4163
5415
  rule: "scripts/delayed.js exists",
4164
5416
  status: "info",
@@ -4166,7 +5418,7 @@ async function checkDelayedJs(root, results) {
4166
5418
  });
4167
5419
  return;
4168
5420
  }
4169
- const content = await readFile14(filePath, "utf-8");
5421
+ const content = await readFile19(filePath, "utf-8");
4170
5422
  const stripped = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").trim();
4171
5423
  if (!stripped) {
4172
5424
  results.push({
@@ -4178,15 +5430,15 @@ async function checkDelayedJs(root, results) {
4178
5430
  }
4179
5431
  }
4180
5432
  async function checkBlocks(root, results) {
4181
- const blocksDir = path21.join(root, "blocks");
4182
- if (!existsSync19(blocksDir)) return;
4183
- const entries = await readdir4(blocksDir, { withFileTypes: true });
5433
+ const blocksDir = path27.join(root, "blocks");
5434
+ if (!existsSync25(blocksDir)) return;
5435
+ const entries = await readdir5(blocksDir, { withFileTypes: true });
4184
5436
  for (const entry of entries) {
4185
5437
  if (!entry.isDirectory()) continue;
4186
5438
  const blockName = entry.name;
4187
- const jsFile = path21.join(blocksDir, blockName, `${blockName}.js`);
4188
- if (!existsSync19(jsFile)) continue;
4189
- const content = await readFile14(jsFile, "utf-8");
5439
+ const jsFile = path27.join(blocksDir, blockName, `${blockName}.js`);
5440
+ if (!existsSync25(jsFile)) continue;
5441
+ const content = await readFile19(jsFile, "utf-8");
4190
5442
  const lines = content.split("\n");
4191
5443
  const heavyLibs = [
4192
5444
  "jquery",
@@ -4236,9 +5488,9 @@ async function checkBlocks(root, results) {
4236
5488
  for (const entry of entries) {
4237
5489
  if (!entry.isDirectory()) continue;
4238
5490
  const blockName = entry.name;
4239
- const cssFile = path21.join(blocksDir, blockName, `${blockName}.css`);
4240
- if (!existsSync19(cssFile)) continue;
4241
- const content = await readFile14(cssFile, "utf-8");
5491
+ const cssFile = path27.join(blocksDir, blockName, `${blockName}.css`);
5492
+ if (!existsSync25(cssFile)) continue;
5493
+ const content = await readFile19(cssFile, "utf-8");
4242
5494
  const selectorPattern = /^([a-z][a-z0-9-]*)\s*\{/gm;
4243
5495
  for (const selectorMatch of content.matchAll(selectorPattern)) {
4244
5496
  const selector = selectorMatch[1];
@@ -4254,8 +5506,8 @@ async function checkBlocks(root, results) {
4254
5506
  }
4255
5507
  }
4256
5508
  async function checkAemJs(root, results) {
4257
- const filePath = path21.join(root, "scripts", "aem.js");
4258
- if (!existsSync19(filePath)) {
5509
+ const filePath = path27.join(root, "scripts", "aem.js");
5510
+ if (!existsSync25(filePath)) {
4259
5511
  results.push({
4260
5512
  rule: "scripts/aem.js exists",
4261
5513
  status: "warn",
@@ -4293,12 +5545,12 @@ function printResults(results) {
4293
5545
  grouped.set(key, bucket);
4294
5546
  }
4295
5547
  for (const [file, fileResults] of grouped) {
4296
- logger.info(` ${icon.bullet} ${chalk11.underline(file)}`);
5548
+ logger.info(` ${icon.bullet} ${chalk14.underline(file)}`);
4297
5549
  for (const r of fileResults) {
4298
- const loc = r.line ? chalk11.dim(`:${r.line}`) : "";
5550
+ const loc = r.line ? chalk14.dim(`:${r.line}`) : "";
4299
5551
  logger.info(` ${statusIcon(r.status)} ${r.rule}${loc}`);
4300
5552
  if (r.status !== "pass") {
4301
- logger.info(chalk11.dim(` ${r.message}`));
5553
+ logger.info(chalk14.dim(` ${r.message}`));
4302
5554
  }
4303
5555
  }
4304
5556
  logger.info("");
@@ -4327,14 +5579,14 @@ var audit_security_exports = {};
4327
5579
  __export(audit_security_exports, {
4328
5580
  auditSecurity: () => auditSecurity
4329
5581
  });
4330
- import { existsSync as existsSync20 } from "fs";
4331
- import { readFile as readFile15, readdir as readdir5 } from "fs/promises";
4332
- import path22 from "path";
4333
- import chalk12 from "chalk";
5582
+ import { existsSync as existsSync26 } from "fs";
5583
+ import { readFile as readFile20, readdir as readdir6 } from "fs/promises";
5584
+ import path28 from "path";
5585
+ import chalk15 from "chalk";
4334
5586
  async function* walk2(dir) {
4335
- const entries = await readdir5(dir, { withFileTypes: true });
5587
+ const entries = await readdir6(dir, { withFileTypes: true });
4336
5588
  for (const entry of entries) {
4337
- const full = path22.join(dir, entry.name);
5589
+ const full = path28.join(dir, entry.name);
4338
5590
  if (entry.isDirectory()) {
4339
5591
  if (IGNORE_DIRS.has(entry.name)) continue;
4340
5592
  yield* walk2(full);
@@ -4368,11 +5620,11 @@ function scanContent(content, relFile, ext) {
4368
5620
  return findings;
4369
5621
  }
4370
5622
  async function auditDependencies(root) {
4371
- if (!existsSync20(path22.join(root, "package.json"))) {
5623
+ if (!existsSync26(path28.join(root, "package.json"))) {
4372
5624
  return { status: "skipped", note: "no package.json" };
4373
5625
  }
4374
- const hasLock = existsSync20(path22.join(root, "package-lock.json")) || existsSync20(path22.join(root, "npm-shrinkwrap.json"));
4375
- if (!hasLock && !existsSync20(path22.join(root, "node_modules"))) {
5626
+ const hasLock = existsSync26(path28.join(root, "package-lock.json")) || existsSync26(path28.join(root, "npm-shrinkwrap.json"));
5627
+ if (!hasLock && !existsSync26(path28.join(root, "node_modules"))) {
4376
5628
  return { status: "skipped", note: "no package-lock.json / node_modules \u2014 run npm install" };
4377
5629
  }
4378
5630
  try {
@@ -4404,15 +5656,15 @@ async function auditSecurity(options = {}) {
4404
5656
  }
4405
5657
  const findings = [];
4406
5658
  for await (const file of walk2(projectRoot)) {
4407
- const ext = path22.extname(file).slice(1).toLowerCase();
5659
+ const ext = path28.extname(file).slice(1).toLowerCase();
4408
5660
  if (!SCAN_EXT.has(ext)) continue;
4409
- const rel = path22.relative(projectRoot, file);
5661
+ const rel = path28.relative(projectRoot, file);
4410
5662
  if (rel.endsWith("aem.js") || rel.endsWith("lib-franklin.js")) continue;
4411
- const content = await readFile15(file, "utf-8");
5663
+ const content = await readFile20(file, "utf-8");
4412
5664
  findings.push(...scanContent(content, rel, ext));
4413
5665
  }
4414
5666
  const deps = await auditDependencies(projectRoot);
4415
- const bySeverity = (s) => findings.filter((f) => f.severity === s);
5667
+ const bySeverity = (s) => findings.filter((f2) => f2.severity === s);
4416
5668
  const codeCounts = {
4417
5669
  critical: bySeverity("critical").length,
4418
5670
  high: bySeverity("high").length,
@@ -4454,11 +5706,11 @@ async function auditSecurity(options = {}) {
4454
5706
  const sorted = [...findings].sort(
4455
5707
  (a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity)
4456
5708
  );
4457
- for (const f of sorted) {
4458
- const meta = SEVERITY_META[f.severity];
4459
- const loc = chalk12.dim(`${f.file}:${f.line}`);
4460
- logger.info(` ${statusIcon(meta.status)} ${chalk12.bold(meta.label)} ${f.message} ${loc}`);
4461
- logger.info(chalk12.dim(` ${f.snippet}`));
5709
+ for (const f2 of sorted) {
5710
+ const meta = SEVERITY_META[f2.severity];
5711
+ const loc = chalk15.dim(`${f2.file}:${f2.line}`);
5712
+ logger.info(` ${statusIcon(meta.status)} ${chalk15.bold(meta.label)} ${f2.message} ${loc}`);
5713
+ logger.info(chalk15.dim(` ${f2.snippet}`));
4462
5714
  }
4463
5715
  }
4464
5716
  const summaryLine = summary({
@@ -4665,7 +5917,7 @@ __export(preview_exports2, {
4665
5917
  preview: () => preview,
4666
5918
  resolveAdminConfig: () => resolveAdminConfig
4667
5919
  });
4668
- import chalk13 from "chalk";
5920
+ import chalk16 from "chalk";
4669
5921
  import ora2 from "ora";
4670
5922
  async function preview(paths, options) {
4671
5923
  const projectRoot = findProjectRoot();
@@ -4678,7 +5930,7 @@ async function preview(paths, options) {
4678
5930
  let hasErrors = false;
4679
5931
  for (const r of results) {
4680
5932
  if (r.ok) {
4681
- logger.success(`${r.path} \u2192 ${chalk13.cyan(r.previewUrl || "preview updated")}`);
5933
+ logger.success(`${r.path} \u2192 ${chalk16.cyan(r.previewUrl || "preview updated")}`);
4682
5934
  } else {
4683
5935
  logger.error(`${r.path} \u2014 ${r.message}`);
4684
5936
  hasErrors = true;
@@ -4692,7 +5944,7 @@ function resolveAdminConfig(config, options) {
4692
5944
  const ref = options.ref || config.admin?.ref || "main";
4693
5945
  if (!org || !site) {
4694
5946
  logger.error("Missing --org and --site. Set them via flags or in .edsrc.json:");
4695
- logger.info(chalk13.dim(' { "admin": { "org": "my-org", "site": "my-site" } }'));
5947
+ logger.info(chalk16.dim(' { "admin": { "org": "my-org", "site": "my-site" } }'));
4696
5948
  process.exitCode = 1;
4697
5949
  return void 0;
4698
5950
  }
@@ -4714,7 +5966,7 @@ var publish_exports = {};
4714
5966
  __export(publish_exports, {
4715
5967
  publish: () => publish
4716
5968
  });
4717
- import chalk14 from "chalk";
5969
+ import chalk17 from "chalk";
4718
5970
  import ora3 from "ora";
4719
5971
  async function publish(paths, options) {
4720
5972
  const projectRoot = findProjectRoot();
@@ -4727,7 +5979,7 @@ async function publish(paths, options) {
4727
5979
  let hasErrors = false;
4728
5980
  for (const r of results) {
4729
5981
  if (r.ok) {
4730
- logger.success(`${r.path} \u2192 ${chalk14.cyan(r.liveUrl || "published")}`);
5982
+ logger.success(`${r.path} \u2192 ${chalk17.cyan(r.liveUrl || "published")}`);
4731
5983
  } else {
4732
5984
  logger.error(`${r.path} \u2014 ${r.message}`);
4733
5985
  hasErrors = true;
@@ -4826,13 +6078,13 @@ var menu_exports = {};
4826
6078
  __export(menu_exports, {
4827
6079
  mainMenu: () => mainMenu
4828
6080
  });
4829
- import { existsSync as existsSync21 } from "fs";
4830
- import { readdir as readdir6 } from "fs/promises";
4831
- import path23 from "path";
6081
+ import { existsSync as existsSync27 } from "fs";
6082
+ import { readdir as readdir7 } from "fs/promises";
6083
+ import path29 from "path";
4832
6084
  async function listBlockNames(projectRoot) {
4833
- const dir = path23.join(projectRoot, "blocks");
4834
- if (!existsSync21(dir)) return [];
4835
- const entries = await readdir6(dir, { withFileTypes: true });
6085
+ const dir = path29.join(projectRoot, "blocks");
6086
+ if (!existsSync27(dir)) return [];
6087
+ const entries = await readdir7(dir, { withFileTypes: true });
4836
6088
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
4837
6089
  }
4838
6090
  async function mainMenu() {
@@ -4842,11 +6094,18 @@ async function mainMenu() {
4842
6094
  message: "Choose an action",
4843
6095
  choices: [
4844
6096
  { name: "Set up this project (authoring model, CI, naming)", value: "init" },
4845
- { name: "Generate a block from a design (Figma/Stitch/Canva/Sketch)", value: "from-design" },
6097
+ { name: "Generate a page template (initial content)", value: "template" },
6098
+ {
6099
+ name: "Generate a block from a design (Figma/Stitch/Canva/Sketch/Framer)",
6100
+ value: "from-design"
6101
+ },
4846
6102
  { name: "Migrate a classic AEM component to EDS", value: "migrate" },
4847
6103
  { name: "Scaffold Universal Editor components", value: "scaffold" },
4848
6104
  { name: "Create an empty block", value: "create" },
4849
6105
  { name: "Add a third-party integration (GTM, chat, consent\u2026)", value: "integrate" },
6106
+ { name: "Add fields to a block model (partials)", value: "model" },
6107
+ { name: "Instrument a block for analytics (dataLayer)", value: "track" },
6108
+ { name: "Add JSON-LD structured data to a block", value: "schema" },
4850
6109
  { name: "Preview a block in the browser", value: "preview-block" },
4851
6110
  { name: "Preview / publish pages (Admin API)", value: "admin" },
4852
6111
  { name: "Audit the project", value: "audit" },
@@ -4859,6 +6118,15 @@ async function mainMenu() {
4859
6118
  await initProject2({});
4860
6119
  return;
4861
6120
  }
6121
+ case "template": {
6122
+ const name = await input({
6123
+ message: "Template name",
6124
+ validate: (v) => !!v.trim() || "Required"
6125
+ });
6126
+ const { templateNew: templateNew2 } = await Promise.resolve().then(() => (init_template(), template_exports));
6127
+ await templateNew2(name.trim(), {});
6128
+ return;
6129
+ }
4862
6130
  case "from-design": {
4863
6131
  const { blockFromDesign: blockFromDesign2 } = await Promise.resolve().then(() => (init_from_design(), from_design_exports));
4864
6132
  await blockFromDesign2(void 0, {});
@@ -4907,6 +6175,49 @@ async function mainMenu() {
4907
6175
  await previewBlock2(name, {});
4908
6176
  return;
4909
6177
  }
6178
+ case "model": {
6179
+ const { modelAdd: modelAdd2 } = await Promise.resolve().then(() => (init_model(), model_exports));
6180
+ await modelAdd2(void 0, [], {});
6181
+ return;
6182
+ }
6183
+ case "track": {
6184
+ const projectRoot = findProjectRoot();
6185
+ if (!projectRoot) {
6186
+ logger.error("Not inside an EDS project (no EDS project markers found).");
6187
+ return;
6188
+ }
6189
+ const blocks = await listBlockNames(projectRoot);
6190
+ if (blocks.length === 0) {
6191
+ logger.warn("No blocks found in blocks/.");
6192
+ return;
6193
+ }
6194
+ const name = await select({
6195
+ message: "Which block to instrument?",
6196
+ choices: blocks.map((b) => ({ name: b, value: b }))
6197
+ });
6198
+ const { trackBlock: trackBlock2 } = await Promise.resolve().then(() => (init_track2(), track_exports));
6199
+ await trackBlock2(name, {});
6200
+ return;
6201
+ }
6202
+ case "schema": {
6203
+ const projectRoot = findProjectRoot();
6204
+ if (!projectRoot) {
6205
+ logger.error("Not inside an EDS project (no EDS project markers found).");
6206
+ return;
6207
+ }
6208
+ const blocks = await listBlockNames(projectRoot);
6209
+ if (blocks.length === 0) {
6210
+ logger.warn("No blocks found in blocks/.");
6211
+ return;
6212
+ }
6213
+ const name = await select({
6214
+ message: "Which block?",
6215
+ choices: blocks.map((b) => ({ name: b, value: b }))
6216
+ });
6217
+ const { schemaBlock: schemaBlock2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
6218
+ await schemaBlock2(name, {});
6219
+ return;
6220
+ }
4910
6221
  case "admin": {
4911
6222
  const { adminWizard: adminWizard2 } = await Promise.resolve().then(() => (init_admin(), admin_exports));
4912
6223
  await adminWizard2();
@@ -4952,13 +6263,13 @@ var init_menu = __esm({
4952
6263
 
4953
6264
  // src/bin/eds.ts
4954
6265
  init_esm_shims();
4955
- import chalk15 from "chalk";
6266
+ import chalk18 from "chalk";
4956
6267
  import { Command } from "commander";
4957
6268
 
4958
6269
  // package.json
4959
6270
  var package_default = {
4960
6271
  name: "aem-eds-kit",
4961
- version: "0.4.9",
6272
+ version: "0.4.16",
4962
6273
  description: "CLI for AEM Edge Delivery Services with Figma MCP integration",
4963
6274
  type: "module",
4964
6275
  engines: {
@@ -5034,9 +6345,18 @@ program.name("eds").description("CLI for AEM Edge Delivery Services with Figma i
5034
6345
  const opts = thisCommand.opts();
5035
6346
  if (opts.verbose) logger.setLevel("debug");
5036
6347
  if (opts.quiet) logger.setQuiet(true);
5037
- if (opts.color === false) chalk15.level = 0;
6348
+ if (opts.color === false) chalk18.level = 0;
5038
6349
  });
5039
6350
  program.addHelpText("beforeAll", () => logo());
6351
+ var template = program.command("template").description("Generate page templates / initial content");
6352
+ template.command("new <name>").description("Generate EDS page initial content (sections + blocks + metadata), DA push optional").option("--path <path>", "Content path (default: /templates/<name>)").option("--title <title>", "Page title").option("--description <text>", "Page description").option(
6353
+ "--blocks <list>",
6354
+ "Comma-separated block ids to seed",
6355
+ (v) => v.split(",").map((b) => b.trim())
6356
+ ).option("--area <area>", "Site area / template label (stored in metadata)").option("--authoring <model>", "Force authoring model: da | gdrive | sharepoint | ue").option("--push", "Push the page to DA (da.live)").option("--org <org>", "DA org (else from .edsrc.json)").option("--site <site>", "DA site (else from .edsrc.json)").option("--yes", "Skip prompts").action(async (name, options) => {
6357
+ const { templateNew: templateNew2 } = await Promise.resolve().then(() => (init_template(), template_exports));
6358
+ await templateNew2(name, options);
6359
+ });
5040
6360
  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) => {
5041
6361
  const { initProject: initProject2 } = await Promise.resolve().then(() => (init_init(), init_exports));
5042
6362
  await initProject2(options);
@@ -5046,6 +6366,21 @@ migrate.command("component [dir]").description("Migrate a classic AEM component'
5046
6366
  const { migrateComponent: migrateComponent2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
5047
6367
  await migrateComponent2(dir, options);
5048
6368
  });
6369
+ var model = program.command("model").description("Compose Universal Editor models");
6370
+ model.command("add <block> [partials...]").description("Add reusable field groups (partials) to a block's UE model").option("--yes", "Skip prompts").action(async (blockName, partials, options) => {
6371
+ const { modelAdd: modelAdd2 } = await Promise.resolve().then(() => (init_model(), model_exports));
6372
+ await modelAdd2(blockName, partials, options);
6373
+ });
6374
+ var schema = program.command("schema").description("Structured data (JSON-LD) for blocks");
6375
+ schema.command("block <name>").description("Inject a JSON-LD (schema.org) builder into a block's decorate()").option("--type <type>", "schema.org type (Article, FAQPage, Product, ...)").option("--yes", "Skip prompts").action(async (name, options) => {
6376
+ const { schemaBlock: schemaBlock2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
6377
+ await schemaBlock2(name, options);
6378
+ });
6379
+ var track = program.command("track").description("Instrument blocks for analytics");
6380
+ 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) => {
6381
+ const { trackBlock: trackBlock2 } = await Promise.resolve().then(() => (init_track2(), track_exports));
6382
+ await trackBlock2(name, options);
6383
+ });
5049
6384
  var block = program.command("block").description("Manage EDS blocks");
5050
6385
  block.command("create <name>").description("Scaffold a new EDS block (with Universal Editor model by default)").option("--no-ue-model", "Skip the Universal Editor model (generated by default)").action(async (name, options) => {
5051
6386
  const { createBlock: createBlock2 } = await Promise.resolve().then(() => (init_create(), create_exports));
@@ -5081,6 +6416,10 @@ scaffold.command("ci").description("Scaffold a GitHub Actions workflow (doctor +
5081
6416
  const { scaffoldCi: scaffoldCi2 } = await Promise.resolve().then(() => (init_scaffold(), scaffold_exports));
5082
6417
  await scaffoldCi2();
5083
6418
  });
6419
+ scaffold.command("helpers [names...]").description("Scaffold scripts/utils.js with chosen EDS helpers (interactive picker)").action(async (names) => {
6420
+ const { scaffoldHelpers: scaffoldHelpers2 } = await Promise.resolve().then(() => (init_scaffold(), scaffold_exports));
6421
+ await scaffoldHelpers2(names);
6422
+ });
5084
6423
  program.command("integrate [type]").description("Add a third-party service (GTM, GA4, chat, consent) to scripts/delayed.js").action(async (type) => {
5085
6424
  const { integrate: integrate2 } = await Promise.resolve().then(() => (init_integrate(), integrate_exports));
5086
6425
  await integrate2(type);