aem-eds-kit 0.4.12 → 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,152 +933,740 @@ 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("");
1629
+ logger.info(
1630
+ box([
1631
+ res.created ? "CI workflow scaffolded - commit it and it runs on every PR" : "CI workflow left untouched"
1632
+ ])
1633
+ );
1634
+ }
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("");
1190
1667
  logger.info(
1191
1668
  box([
1192
- res.created ? "CI workflow scaffolded - commit it and it runs on every PR" : "CI workflow left untouched"
1669
+ res.created ? `${res.helpers.length} helper(s) -> scripts/utils.js` : "Helpers left untouched"
1193
1670
  ])
1194
1671
  );
1195
1672
  }
@@ -1202,6 +1679,7 @@ async function scaffoldInteractive() {
1202
1679
  { name: "Universal Editor config + field-reference (all 17 field types)", value: "ue" },
1203
1680
  { name: "Standard blocks (hero, cards, columns, accordion, embed)", value: "blocks" },
1204
1681
  { name: "GitHub Actions CI (doctor + audits + lint/build)", value: "ci" },
1682
+ { name: "Helper functions (scripts/utils.js)", value: "helpers" },
1205
1683
  { name: "UE config + standard blocks", value: "both" }
1206
1684
  ]
1207
1685
  });
@@ -1214,6 +1692,7 @@ async function scaffoldInteractive() {
1214
1692
  await scaffoldBlocks(picked);
1215
1693
  }
1216
1694
  if (what === "ci") await scaffoldCi();
1695
+ if (what === "helpers") await scaffoldHelpers();
1217
1696
  }
1218
1697
  var init_scaffold = __esm({
1219
1698
  "src/commands/scaffold.ts"() {
@@ -1223,6 +1702,7 @@ var init_scaffold = __esm({
1223
1702
  init_project();
1224
1703
  init_blocks();
1225
1704
  init_ci();
1705
+ init_helpers();
1226
1706
  init_ue();
1227
1707
  init_ui();
1228
1708
  }
@@ -1233,17 +1713,17 @@ var init_exports = {};
1233
1713
  __export(init_exports, {
1234
1714
  initProject: () => initProject
1235
1715
  });
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";
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";
1240
1720
  async function setPackageName(root, name) {
1241
- const file = path6.join(root, "package.json");
1242
- if (!existsSync4(file)) return false;
1721
+ const file = path9.join(root, "package.json");
1722
+ if (!existsSync7(file)) return false;
1243
1723
  try {
1244
- const pkg = JSON.parse(await readFile2(file, "utf-8"));
1724
+ const pkg = JSON.parse(await readFile4(file, "utf-8"));
1245
1725
  pkg.name = name;
1246
- await writeFile4(file, `${JSON.stringify(pkg, null, 2)}
1726
+ await writeFile6(file, `${JSON.stringify(pkg, null, 2)}
1247
1727
  `);
1248
1728
  return true;
1249
1729
  } catch {
@@ -1253,13 +1733,13 @@ async function setPackageName(root, name) {
1253
1733
  async function replacePlaceholders(root, name) {
1254
1734
  const touched = [];
1255
1735
  for (const rel of ["README.md", "index.html", "head.html"]) {
1256
- const file = path6.join(root, rel);
1257
- if (!existsSync4(file)) continue;
1736
+ const file = path9.join(root, rel);
1737
+ if (!existsSync7(file)) continue;
1258
1738
  try {
1259
- const before = await readFile2(file, "utf-8");
1739
+ const before = await readFile4(file, "utf-8");
1260
1740
  const after = before.replace(/aem-boilerplate/gi, name);
1261
1741
  if (after !== before) {
1262
- await writeFile4(file, after);
1742
+ await writeFile6(file, after);
1263
1743
  touched.push(rel);
1264
1744
  }
1265
1745
  } catch {
@@ -1314,9 +1794,9 @@ async function externalSteps(authoring) {
1314
1794
  logger.info(heading("Next steps (outside the repo)"));
1315
1795
  for (let i = 0; i < steps.length; i++) {
1316
1796
  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)}`);
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)}`);
1320
1800
  await confirm({ message: " Done / skip?", default: true });
1321
1801
  }
1322
1802
  }
@@ -1332,19 +1812,19 @@ async function initProject(options = {}) {
1332
1812
  await logger.logoOnceAnimated("Project setup");
1333
1813
  const { input, select, confirm } = await import("@inquirer/prompts");
1334
1814
  const interactive = !options.yes;
1335
- let defaultName = path6.basename(projectRoot);
1336
- const pkgFile = path6.join(projectRoot, "package.json");
1337
- if (existsSync4(pkgFile)) {
1815
+ let defaultName = path9.basename(projectRoot);
1816
+ const pkgFile = path9.join(projectRoot, "package.json");
1817
+ if (existsSync7(pkgFile)) {
1338
1818
  try {
1339
- defaultName = JSON.parse(await readFile2(pkgFile, "utf-8")).name || defaultName;
1819
+ defaultName = JSON.parse(await readFile4(pkgFile, "utf-8")).name || defaultName;
1340
1820
  } catch {
1341
1821
  }
1342
1822
  }
1343
- const name = options.name && slug(options.name) || slug(
1823
+ const name = options.name && slug2(options.name) || slug2(
1344
1824
  await input({
1345
1825
  message: "1/4 Project name",
1346
- default: slug(defaultName),
1347
- validate: (v) => !!slug(v) || "Use letters, numbers and dashes"
1826
+ default: slug2(defaultName),
1827
+ validate: (v) => !!slug2(v) || "Use letters, numbers and dashes"
1348
1828
  })
1349
1829
  );
1350
1830
  const authoring = options.authoring ?? await select({
@@ -1361,7 +1841,7 @@ async function initProject(options = {}) {
1361
1841
  if (authoring === "da" && (options.org || interactive)) {
1362
1842
  const org = options.org ?? await input({ message: " DA org", validate: (v) => !!v.trim() || "Required" });
1363
1843
  const site = options.site ?? (interactive ? await input({ message: " DA site", default: name }) : name);
1364
- mountpoint = daMountpoint(org.trim(), slug(site));
1844
+ mountpoint = daMountpoint(org.trim(), slug2(site));
1365
1845
  } else if (authoring !== "da" && interactive) {
1366
1846
  const label = authoring === "gdrive" ? "Google Drive folder URL" : "SharePoint folder URL";
1367
1847
  mountpoint = (await input({
@@ -1402,14 +1882,14 @@ async function initProject(options = {}) {
1402
1882
  logger.info(heading("Configured"));
1403
1883
  const dw = columnWidth(done.map((d) => d.split(" ")[0]));
1404
1884
  for (const d of done) {
1405
- const [n, ...rest] = d.split(" ");
1406
- logger.info(accentLine(n, rest.join(" ").replace(/^\((.*)\)$/, "$1"), dw));
1885
+ const [n2, ...rest] = d.split(" ");
1886
+ logger.info(accentLine(n2, rest.join(" ").replace(/^\((.*)\)$/, "$1"), dw));
1407
1887
  }
1408
1888
  logger.info("");
1409
1889
  if (interactive) await externalSteps(authoring);
1410
1890
  logger.info(box([`"${name}" set up for ${AUTHORING_NAMES[authoring]}`]));
1411
1891
  }
1412
- var slug;
1892
+ var slug2;
1413
1893
  var init_init = __esm({
1414
1894
  "src/commands/init.ts"() {
1415
1895
  "use strict";
@@ -1420,58 +1900,58 @@ var init_init = __esm({
1420
1900
  init_fstab();
1421
1901
  init_ui();
1422
1902
  init_scaffold();
1423
- slug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1903
+ slug2 = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1424
1904
  }
1425
1905
  });
1426
1906
 
1427
1907
  // 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";
1908
+ import { existsSync as existsSync8 } from "fs";
1909
+ import { readFile as readFile5, readdir as readdir2 } from "fs/promises";
1910
+ import path10 from "path";
1431
1911
  async function listFiles(dir) {
1432
1912
  const out = [];
1433
1913
  const walk3 = async (d) => {
1434
- for (const e of await readdir(d, { withFileTypes: true })) {
1435
- const p = path7.join(d, e.name);
1914
+ for (const e of await readdir2(d, { withFileTypes: true })) {
1915
+ const p = path10.join(d, e.name);
1436
1916
  if (e.isDirectory()) await walk3(p);
1437
1917
  else out.push(p);
1438
1918
  }
1439
1919
  };
1440
- if (existsSync5(dir)) await walk3(dir);
1920
+ if (existsSync8(dir)) await walk3(dir);
1441
1921
  return out;
1442
1922
  }
1443
1923
  async function findClientlibDir(componentDir) {
1444
- if (!existsSync5(componentDir)) return null;
1445
- const entries = await readdir(componentDir, { withFileTypes: true });
1924
+ if (!existsSync8(componentDir)) return null;
1925
+ const entries = await readdir2(componentDir, { withFileTypes: true });
1446
1926
  const match = entries.find((e) => e.isDirectory() && /clientlib/i.test(e.name));
1447
- if (match) return path7.join(componentDir, match.name);
1927
+ if (match) return path10.join(componentDir, match.name);
1448
1928
  const hasCss = entries.some((e) => e.isFile() && e.name.endsWith(".css"));
1449
1929
  return hasCss ? componentDir : null;
1450
1930
  }
1451
1931
  async function readClientlibCss(clientlibDir) {
1452
1932
  const all = await listFiles(clientlibDir);
1453
- const preprocessed = all.filter((f) => /\.(less|scss|sass)$/i.test(f)).map((f) => path7.relative(clientlibDir, f));
1933
+ const preprocessed = all.filter((f2) => /\.(less|scss|sass)$/i.test(f2)).map((f2) => path10.relative(clientlibDir, f2));
1454
1934
  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());
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());
1458
1938
  let base = clientlibDir;
1459
1939
  const listed = [];
1460
1940
  for (const l of lines) {
1461
1941
  if (!l) continue;
1462
1942
  const baseMatch = l.match(/^#base\s*=\s*(.+)$/);
1463
- if (baseMatch) base = path7.join(clientlibDir, baseMatch[1].trim());
1943
+ if (baseMatch) base = path10.join(clientlibDir, baseMatch[1].trim());
1464
1944
  else if (!l.startsWith("#")) listed.push(l);
1465
1945
  }
1466
- cssFiles = listed.map((rel) => path7.join(base, rel)).filter((f) => existsSync5(f) && f.endsWith(".css"));
1946
+ cssFiles = listed.map((rel) => path10.join(base, rel)).filter((f2) => existsSync8(f2) && f2.endsWith(".css"));
1467
1947
  } else {
1468
- cssFiles = all.filter((f) => f.endsWith(".css"));
1948
+ cssFiles = all.filter((f2) => f2.endsWith(".css"));
1469
1949
  }
1470
1950
  const parts = [];
1471
- for (const f of cssFiles) parts.push(await readFile3(f, "utf-8"));
1951
+ for (const f2 of cssFiles) parts.push(await readFile5(f2, "utf-8"));
1472
1952
  return {
1473
1953
  css: parts.join("\n\n"),
1474
- files: cssFiles.map((f) => path7.relative(clientlibDir, f)),
1954
+ files: cssFiles.map((f2) => path10.relative(clientlibDir, f2)),
1475
1955
  preprocessed
1476
1956
  };
1477
1957
  }
@@ -1484,25 +1964,25 @@ var init_clientlib = __esm({
1484
1964
 
1485
1965
  // src/lib/migrate/dialog.ts
1486
1966
  import { XMLParser } from "fast-xml-parser";
1487
- function tagOf(n) {
1488
- return Object.keys(n).find((k) => k !== ":@") ?? "";
1967
+ function tagOf(n2) {
1968
+ return Object.keys(n2).find((k) => k !== ":@") ?? "";
1489
1969
  }
1490
- function childrenOf(n) {
1491
- const t = tagOf(n);
1492
- const v = n[t];
1970
+ function childrenOf(n2) {
1971
+ const t = tagOf(n2);
1972
+ const v = n2[t];
1493
1973
  return Array.isArray(v) ? v : [];
1494
1974
  }
1495
- function attrsOf(n) {
1496
- return n[":@"] ?? {};
1975
+ function attrsOf(n2) {
1976
+ return n2[":@"] ?? {};
1497
1977
  }
1498
1978
  function stripType(v) {
1499
1979
  return typeof v === "string" ? v.replace(/^\{[^}]+\}/, "") : v;
1500
1980
  }
1501
- function attr(n, name) {
1502
- return stripType(attrsOf(n)[`@_${name}`]);
1981
+ function attr(n2, name) {
1982
+ return stripType(attrsOf(n2)[`@_${name}`]);
1503
1983
  }
1504
- function leafComponent(rt, n) {
1505
- const multiple = attr(n, "multiple") === "true" || attr(n, "multiple") === "{Boolean}true";
1984
+ function leafComponent(rt, n2) {
1985
+ const multiple = attr(n2, "multiple") === "true" || attr(n2, "multiple") === "{Boolean}true";
1506
1986
  if (rt.endsWith("/form/textfield")) return "text";
1507
1987
  if (rt.endsWith("/form/textarea")) return "textarea";
1508
1988
  if (rt.includes("/richtext")) return "richtext";
@@ -1541,8 +2021,8 @@ function valueTypeFor(c) {
1541
2021
  }
1542
2022
  }
1543
2023
  function cleanName(raw, fallback) {
1544
- const n = (raw ?? "").replace(/^\.\//, "").trim();
1545
- return n || fallback;
2024
+ const n2 = (raw ?? "").replace(/^\.\//, "").trim();
2025
+ return n2 || fallback;
1546
2026
  }
1547
2027
  function readOptions(fieldNode) {
1548
2028
  const items = childrenOf(fieldNode).find((c) => tagOf(c) === "items");
@@ -1609,7 +2089,7 @@ function parseDialog(xml) {
1609
2089
  trimValues: true
1610
2090
  });
1611
2091
  const tree = parser.parse(xml);
1612
- const root = tree.find((n) => tagOf(n) === "jcr:root");
2092
+ const root = tree.find((n2) => tagOf(n2) === "jcr:root");
1613
2093
  const fields = [];
1614
2094
  const unmapped = /* @__PURE__ */ new Set();
1615
2095
  if (root) walk(childrenOf(root), fields, unmapped, { i: 1 });
@@ -1623,24 +2103,24 @@ var init_dialog = __esm({
1623
2103
  });
1624
2104
 
1625
2105
  // 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";
2106
+ import { existsSync as existsSync9 } from "fs";
2107
+ import { readFile as readFile6 } from "fs/promises";
2108
+ import path11 from "path";
1629
2109
  import { XMLParser as XMLParser2 } from "fast-xml-parser";
1630
2110
  function findDialogFile(componentDir) {
1631
2111
  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")
2112
+ path11.join(componentDir, "_cq_dialog", ".content.xml"),
2113
+ path11.join(componentDir, "_cq_dialog.xml"),
2114
+ path11.join(componentDir, "cq:dialog", ".content.xml")
1635
2115
  ];
1636
- return candidates.find((c) => existsSync6(c)) ?? null;
2116
+ return candidates.find((c) => existsSync9(c)) ?? null;
1637
2117
  }
1638
2118
  async function readComponentTitle(componentDir) {
1639
- const file = path8.join(componentDir, ".content.xml");
1640
- if (!existsSync6(file)) return void 0;
2119
+ const file = path11.join(componentDir, ".content.xml");
2120
+ if (!existsSync9(file)) return void 0;
1641
2121
  try {
1642
2122
  const parser = new XMLParser2({ ignoreAttributes: false, attributeNamePrefix: "@_" });
1643
- const parsed = parser.parse(await readFile4(file, "utf-8"));
2123
+ const parsed = parser.parse(await readFile6(file, "utf-8"));
1644
2124
  const root = parsed["jcr:root"] ?? {};
1645
2125
  const title = root["@_jcr:title"];
1646
2126
  return typeof title === "string" ? title.replace(/^\{[^}]+\}/, "") : void 0;
@@ -1651,10 +2131,10 @@ async function readComponentTitle(componentDir) {
1651
2131
  async function readDialogFields(componentDir) {
1652
2132
  const dialog = findDialogFile(componentDir);
1653
2133
  if (!dialog) return null;
1654
- return parseDialog(await readFile4(dialog, "utf-8"));
2134
+ return parseDialog(await readFile6(dialog, "utf-8"));
1655
2135
  }
1656
2136
  function migratedModelFile(blockName, title, fields) {
1657
- const model = {
2137
+ const model2 = {
1658
2138
  definitions: [
1659
2139
  {
1660
2140
  title,
@@ -1672,11 +2152,11 @@ function migratedModelFile(blockName, title, fields) {
1672
2152
  models: [{ id: blockName, fields }],
1673
2153
  filters: []
1674
2154
  };
1675
- return `${JSON.stringify(model, null, 2)}
2155
+ return `${JSON.stringify(model2, null, 2)}
1676
2156
  `;
1677
2157
  }
1678
2158
  function migratedBlockJs(blockName, fields) {
1679
- const hasContainer = fields.some((f) => f.component === "container" && f.multi);
2159
+ const hasContainer = fields.some((f2) => f2.component === "container" && f2.multi);
1680
2160
  const body = hasContainer ? ` // This block was migrated from a classic AEM component with a multifield.
1681
2161
  // Each child row is one repeated item - enhance them here.
1682
2162
  [...block.children].forEach((row) => {
@@ -1780,10 +2260,10 @@ var track_exports = {};
1780
2260
  __export(track_exports, {
1781
2261
  trackBlock: () => trackBlock
1782
2262
  });
1783
- import { existsSync as existsSync7 } from "fs";
1784
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
1785
- import path9 from "path";
1786
- import chalk4 from "chalk";
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";
1787
2267
  async function trackBlock(blockName, options = {}) {
1788
2268
  const projectRoot = findProjectRoot();
1789
2269
  if (!projectRoot) {
@@ -1792,201 +2272,750 @@ async function trackBlock(blockName, options = {}) {
1792
2272
  return;
1793
2273
  }
1794
2274
  logger.logoOnce(logo("Track block"));
1795
- const jsPath = path9.join(projectRoot, "blocks", blockName, `${blockName}.js`);
1796
- if (!existsSync7(jsPath)) {
2275
+ const jsPath = path12.join(projectRoot, "blocks", blockName, `${blockName}.js`);
2276
+ if (!existsSync10(jsPath)) {
1797
2277
  logger.error(`Block "${blockName}" not found (no blocks/${blockName}/${blockName}.js).`);
1798
2278
  process.exitCode = 1;
1799
2279
  return;
1800
2280
  }
1801
2281
  const events = { click: options.click !== false, submit: options.submit !== false };
1802
- const res = instrumentJs(await readFile5(jsPath, "utf-8"), blockName, events);
2282
+ const res = instrumentJs(await readFile7(jsPath, "utf-8"), blockName, events);
1803
2283
  if (!res.changed) {
1804
2284
  logger.warn(`Not instrumented: ${res.reason}.`);
1805
2285
  return;
1806
2286
  }
1807
- await writeFile5(jsPath, res.js);
1808
- logger.info(heading("Instrumented"));
1809
- const picked = [events.click ? "block_click" : null, events.submit ? "form_submit" : null].filter(
1810
- Boolean
1811
- );
1812
- logger.info(accentLine(`blocks/${blockName}/${blockName}.js`, picked.join(" + ")));
1813
- if (!existsSync7(path9.join(projectRoot, "scripts", "analytics.js"))) {
1814
- logger.info("");
1815
- logger.warn("scripts/analytics.js is missing - run `eds integrate gtm` to create it.");
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.");
2296
+ }
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();
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
+ `;
2326
+ }
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 = {}) {
2353
+ const projectRoot = findProjectRoot();
2354
+ if (!projectRoot) {
2355
+ logger.error("Not inside an EDS project (no EDS project markers found).");
2356
+ process.exitCode = 1;
2357
+ return;
2358
+ }
2359
+ await logger.logoOnceAnimated("Migrate component");
2360
+ const { input, confirm } = await import("@inquirer/prompts");
2361
+ const interactive = !options.yes;
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
+ });
2368
+ }
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(", ")}`);
2395
+ }
2396
+ logger.info("");
2397
+ if (interactive) {
2398
+ const go = await confirm({ message: `Generate block "${blockName}"?`, default: true });
2399
+ if (!go) {
2400
+ logger.info("Cancelled.");
2401
+ return;
2402
+ }
2403
+ }
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;
2411
+ }
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;
2423
+ }
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(", ")}`);
2443
+ }
2444
+ logger.info("");
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
+ );
2458
+ }
2459
+ var slug3;
2460
+ var init_migrate = __esm({
2461
+ "src/commands/migrate.ts"() {
2462
+ "use strict";
2463
+ init_esm_shims();
2464
+ init_logger();
2465
+ init_clientlib();
2466
+ init_component();
2467
+ init_project();
2468
+ init_ui();
2469
+ slug3 = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
2470
+ }
2471
+ });
2472
+
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
+ };
2585
+ }
2586
+ });
2587
+
2588
+ // src/lib/model/partials.ts
2589
+ function partialById(id) {
2590
+ return PARTIALS.find((p) => p.id === id);
2591
+ }
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"() {
2595
+ "use strict";
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
+ ];
2673
+ }
2674
+ });
2675
+
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;
2690
+ }
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;
2705
+ }
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;
2764
+ }
2765
+ existing.add(field.name);
2766
+ target.fields.push(field);
2767
+ added.push(field.name);
2768
+ }
1816
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(", ")));
1817
2776
  logger.info("");
1818
- logger.info(chalk4.dim(" Events push to window.dataLayer via trackEvent - GTM picks them up."));
2777
+ logger.info(box([`${added.length} field(s) added to "${blockName}"`]));
1819
2778
  }
1820
- var init_track2 = __esm({
1821
- "src/commands/track.ts"() {
2779
+ var init_model = __esm({
2780
+ "src/commands/model.ts"() {
1822
2781
  "use strict";
1823
2782
  init_esm_shims();
1824
2783
  init_logger();
2784
+ init_partials();
1825
2785
  init_project();
1826
- init_track();
1827
2786
  init_ui();
1828
2787
  }
1829
2788
  });
1830
2789
 
1831
- // src/commands/migrate.ts
1832
- var migrate_exports = {};
1833
- __export(migrate_exports, {
1834
- migrateComponent: () => migrateComponent
1835
- });
1836
- import { existsSync as existsSync8 } from "fs";
1837
- import { mkdir as mkdir3, writeFile as writeFile6 } from "fs/promises";
1838
- import path10 from "path";
1839
- function clientlibBlockCss(blockName, css, files) {
1840
- return `/* Block: ${blockName} */
1841
- /* Migrated from the component clientlib: ${files.join(", ")}. */
1842
- /* TODO: review selectors and scope them under .${blockName} (and port any LESS/SCSS). */
1843
-
1844
- ${css.trim()}
1845
- `;
2790
+ // src/lib/schema/jsonld.ts
2791
+ function schemaTypeById(id) {
2792
+ return SCHEMA_TYPES.find((t) => t.id.toLowerCase() === id.toLowerCase());
1846
2793
  }
1847
- function previewFields(fields, depth = 0) {
1848
- const lines = [];
1849
- for (const f of fields) {
1850
- const indent = " ".repeat(depth);
1851
- if (f.component === "tab") {
1852
- lines.push(`${indent}${brand.blue("\u25B8")} ${f.label ?? f.name}`);
1853
- } else {
1854
- const opts = f.options?.length ? ` (${f.options.length} options)` : "";
1855
- const multi = f.multi ? " \xB7multi" : "";
1856
- const label = f.label ? `- ${f.label}` : "";
1857
- lines.push(`${indent}${brand.accent(f.component)}${multi} ${f.name}${opts} ${label}`);
1858
- if (f.fields?.length) lines.push(...previewFields(f.fields, depth + 1));
1859
- }
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" };
1860
2801
  }
1861
- return lines;
1862
- }
1863
- function countLeaves(fields) {
1864
- let n = 0;
1865
- for (const f of fields) {
1866
- if (f.component === "tab") continue;
1867
- n += 1;
1868
- if (f.fields) n += countLeaves(f.fields);
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);
1869
2813
  }
1870
- return n;
2814
+ `;
2815
+ return { js: `${js.slice(0, at)}${snippet}${js.slice(at)}`, changed: true };
1871
2816
  }
1872
- async function migrateComponent(dirArg, options = {}) {
2817
+ var SCHEMA_MARKER, SCHEMA_TYPES;
2818
+ var init_jsonld = __esm({
2819
+ "src/lib/schema/jsonld.ts"() {
2820
+ "use strict";
2821
+ init_esm_shims();
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
+ ];
2954
+ }
2955
+ });
2956
+
2957
+ // src/commands/schema.ts
2958
+ var schema_exports = {};
2959
+ __export(schema_exports, {
2960
+ schemaBlock: () => schemaBlock
2961
+ });
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 = {}) {
1873
2967
  const projectRoot = findProjectRoot();
1874
2968
  if (!projectRoot) {
1875
2969
  logger.error("Not inside an EDS project (no EDS project markers found).");
1876
2970
  process.exitCode = 1;
1877
2971
  return;
1878
2972
  }
1879
- await logger.logoOnceAnimated("Migrate component");
1880
- const { input, confirm } = await import("@inquirer/prompts");
1881
- const interactive = !options.yes;
1882
- let componentDir = dirArg;
1883
- if (!componentDir && interactive) {
1884
- componentDir = await input({
1885
- message: "Path to the classic component folder",
1886
- validate: (v) => existsSync8(v.trim()) || "Folder not found"
1887
- });
1888
- }
1889
- if (!componentDir) {
1890
- logger.error("Provide the path to the classic component folder.");
1891
- process.exitCode = 1;
1892
- return;
1893
- }
1894
- componentDir = path10.resolve(componentDir.trim());
1895
- if (!findDialogFile(componentDir)) {
1896
- logger.error(`No Touch UI dialog (_cq_dialog/.content.xml) found in ${componentDir}.`);
1897
- process.exitCode = 1;
1898
- return;
1899
- }
1900
- const parsed = await readDialogFields(componentDir);
1901
- if (!parsed || parsed.fields.length === 0) {
1902
- logger.warn("Dialog found but no mappable fields were detected.");
1903
- if (parsed?.unmapped.length)
1904
- logger.info(`Unmapped resourceTypes: ${parsed.unmapped.join(", ")}`);
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).`);
1905
2977
  process.exitCode = 1;
1906
2978
  return;
1907
2979
  }
1908
- const title = await readComponentTitle(componentDir) ?? path10.basename(componentDir);
1909
- const blockName = options.name && slug2(options.name) || (interactive ? slug2(await input({ message: "Block name", default: slug2(title) })) : slug2(title));
1910
- logger.info(heading("Mapped fields", `${countLeaves(parsed.fields)} from dialog`));
1911
- for (const line of previewFields(parsed.fields)) logger.info(` ${line}`);
1912
- if (parsed.unmapped.length) {
1913
- logger.info("");
1914
- logger.warn(`Skipped (unrecognised): ${parsed.unmapped.join(", ")}`);
1915
- }
1916
- logger.info("");
1917
- if (interactive) {
1918
- const go = await confirm({ message: `Generate block "${blockName}"?`, default: true });
1919
- if (!go) {
1920
- logger.info("Cancelled.");
1921
- return;
1922
- }
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
+ });
1923
2987
  }
1924
- const blockDir = path10.join(projectRoot, "blocks", blockName);
1925
- if (existsSync8(blockDir)) {
2988
+ const type = typeId ? schemaTypeById(typeId) : void 0;
2989
+ if (!type) {
1926
2990
  logger.error(
1927
- `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(", ")}`
1928
2992
  );
1929
2993
  process.exitCode = 1;
1930
2994
  return;
1931
2995
  }
1932
- const clibDir = options.clientlib ? path10.resolve(options.clientlib.trim()) : await findClientlibDir(componentDir);
1933
- let cssContent = migratedBlockCss(blockName);
1934
- let cssMessage = "empty - add styles from clientlib / rendered CSS";
1935
- let preprocessedNote = [];
1936
- if (clibDir && existsSync8(clibDir)) {
1937
- const lib = await readClientlibCss(clibDir);
1938
- if (lib.css.trim()) {
1939
- cssContent = clientlibBlockCss(blockName, lib.css, lib.files);
1940
- cssMessage = `from clientlib (${lib.files.length} file(s)) - review & scope`;
1941
- }
1942
- preprocessedNote = lib.preprocessed;
1943
- }
1944
- await mkdir3(blockDir, { recursive: true });
1945
- await writeFile6(
1946
- path10.join(blockDir, `${blockName}.js`),
1947
- migratedBlockJs(blockName, parsed.fields)
1948
- );
1949
- await writeFile6(path10.join(blockDir, `${blockName}.css`), cssContent);
1950
- await writeFile6(
1951
- path10.join(blockDir, `_${blockName}.json`),
1952
- migratedModelFile(blockName, title, parsed.fields)
1953
- );
1954
- logger.info(heading("Created"));
1955
- const files = [`${blockName}.js`, `${blockName}.css`, `_${blockName}.json`];
1956
- const w = columnWidth(files);
1957
- logger.info(accentLine(`${blockName}.js`, "decorator stub (port HTL logic)", w));
1958
- logger.info(accentLine(`${blockName}.css`, cssMessage, w));
1959
- logger.info(accentLine(`_${blockName}.json`, "Universal Editor model", w));
1960
- if (preprocessedNote.length) {
1961
- logger.info("");
1962
- logger.warn(`LESS/SCSS found (needs a build, not copied): ${preprocessedNote.join(", ")}`);
2996
+ const res = injectJsonLd(await readFile9(jsPath, "utf-8"), type);
2997
+ if (!res.changed) {
2998
+ logger.warn(`Not added: ${res.reason}.`);
2999
+ return;
1963
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})`));
1964
3004
  logger.info("");
1965
- if (interactive) {
1966
- const addTracking = await confirm({
1967
- message: "Instrument this block for analytics (dataLayer)?",
1968
- default: false
1969
- });
1970
- if (addTracking) {
1971
- const { trackBlock: trackBlock2 } = await Promise.resolve().then(() => (init_track2(), track_exports));
1972
- await trackBlock2(blockName, {});
1973
- }
1974
- }
1975
3005
  logger.info(
1976
- 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
+ )
1977
3009
  );
1978
3010
  }
1979
- var slug2;
1980
- var init_migrate = __esm({
1981
- "src/commands/migrate.ts"() {
3011
+ var init_schema = __esm({
3012
+ "src/commands/schema.ts"() {
1982
3013
  "use strict";
1983
3014
  init_esm_shims();
1984
3015
  init_logger();
1985
- init_clientlib();
1986
- init_component();
1987
3016
  init_project();
3017
+ init_jsonld();
1988
3018
  init_ui();
1989
- slug2 = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1990
3019
  }
1991
3020
  });
1992
3021
 
@@ -1996,16 +3025,16 @@ __export(create_exports, {
1996
3025
  createBlock: () => createBlock,
1997
3026
  ueModelFor: () => ueModelFor
1998
3027
  });
1999
- import { existsSync as existsSync9 } from "fs";
2000
- import { mkdir as mkdir4, writeFile as writeFile7 } from "fs/promises";
2001
- import path11 from "path";
2002
- import chalk5 from "chalk";
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";
2003
3032
  function titleCase(name) {
2004
3033
  return name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2005
3034
  }
2006
3035
  function ueModelFor(name) {
2007
3036
  const title = titleCase(name);
2008
- const model = {
3037
+ const model2 = {
2009
3038
  definitions: [
2010
3039
  {
2011
3040
  title,
@@ -2058,7 +3087,7 @@ function ueModelFor(name) {
2058
3087
  ],
2059
3088
  filters: []
2060
3089
  };
2061
- return `${JSON.stringify(model, null, 2)}
3090
+ return `${JSON.stringify(model2, null, 2)}
2062
3091
  `;
2063
3092
  }
2064
3093
  async function createBlock(name, options = {}) {
@@ -2077,13 +3106,13 @@ async function createBlock(name, options = {}) {
2077
3106
  process.exitCode = 1;
2078
3107
  return;
2079
3108
  }
2080
- const blocksDir = path11.join(projectRoot, "blocks", name);
2081
- if (existsSync9(blocksDir)) {
3109
+ const blocksDir = path16.join(projectRoot, "blocks", name);
3110
+ if (existsSync14(blocksDir)) {
2082
3111
  logger.error(`Block "${name}" already exists at ${blocksDir}`);
2083
3112
  process.exitCode = 1;
2084
3113
  return;
2085
3114
  }
2086
- await mkdir4(blocksDir, { recursive: true });
3115
+ await mkdir6(blocksDir, { recursive: true });
2087
3116
  const jsContent = `export default function decorate(block) {
2088
3117
  // TODO: implement ${name} block
2089
3118
  }
@@ -2092,14 +3121,14 @@ async function createBlock(name, options = {}) {
2092
3121
  /* TODO: style ${name} block */
2093
3122
  }
2094
3123
  `;
2095
- await writeFile7(path11.join(blocksDir, `${name}.js`), jsContent);
2096
- await writeFile7(path11.join(blocksDir, `${name}.css`), cssContent);
3124
+ await writeFile11(path16.join(blocksDir, `${name}.js`), jsContent);
3125
+ await writeFile11(path16.join(blocksDir, `${name}.css`), cssContent);
2097
3126
  const withUe = options.ueModel !== false;
2098
3127
  if (withUe) {
2099
- await writeFile7(path11.join(blocksDir, `_${name}.json`), ueModelFor(name));
3128
+ await writeFile11(path16.join(blocksDir, `_${name}.json`), ueModelFor(name));
2100
3129
  }
2101
3130
  logger.success(
2102
- `Block "${name}" created at ${chalk5.cyan(path11.relative(projectRoot, blocksDir))}${withUe ? chalk5.dim(" (+ Universal Editor model)") : ""}`
3131
+ `Block "${name}" created at ${chalk7.cyan(path16.relative(projectRoot, blocksDir))}${withUe ? chalk7.dim(" (+ Universal Editor model)") : ""}`
2103
3132
  );
2104
3133
  }
2105
3134
  var init_create = __esm({
@@ -2113,10 +3142,10 @@ var init_create = __esm({
2113
3142
  });
2114
3143
 
2115
3144
  // src/lib/agents/detect.ts
2116
- import { existsSync as existsSync10 } from "fs";
2117
- import { readFile as readFile6 } from "fs/promises";
3145
+ import { existsSync as existsSync15 } from "fs";
3146
+ import { readFile as readFile10 } from "fs/promises";
2118
3147
  import { homedir } from "os";
2119
- import path12 from "path";
3148
+ import path17 from "path";
2120
3149
  async function whichBinary(name) {
2121
3150
  const { execaCommand } = await import("execa");
2122
3151
  try {
@@ -2144,8 +3173,8 @@ function findFigmaServerKey(node) {
2144
3173
  async function findFigmaMcpServer(configPaths) {
2145
3174
  for (const configPath of configPaths) {
2146
3175
  try {
2147
- if (!existsSync10(configPath)) continue;
2148
- const content = await readFile6(configPath, "utf-8");
3176
+ if (!existsSync15(configPath)) continue;
3177
+ const content = await readFile10(configPath, "utf-8");
2149
3178
  try {
2150
3179
  const key = findFigmaServerKey(JSON.parse(content));
2151
3180
  if (key) return key;
@@ -2167,7 +3196,7 @@ async function detectAgent(preferred) {
2167
3196
  return {
2168
3197
  type: def.type,
2169
3198
  path: binPath,
2170
- configDir: existsSync10(def.configDir) ? def.configDir : void 0,
3199
+ configDir: existsSync15(def.configDir) ? def.configDir : void 0,
2171
3200
  hasFigmaMcp: !!figmaMcpServerName,
2172
3201
  figmaMcpServerName
2173
3202
  };
@@ -2182,7 +3211,7 @@ async function detectAgent(preferred) {
2182
3211
  return {
2183
3212
  type: def.type,
2184
3213
  path: binPath,
2185
- configDir: existsSync10(def.configDir) ? def.configDir : void 0,
3214
+ configDir: existsSync15(def.configDir) ? def.configDir : void 0,
2186
3215
  hasFigmaMcp: !!figmaMcpServerName,
2187
3216
  figmaMcpServerName
2188
3217
  };
@@ -2199,7 +3228,7 @@ async function detectAllAgents() {
2199
3228
  agents.push({
2200
3229
  type: def.type,
2201
3230
  path: binPath,
2202
- configDir: existsSync10(def.configDir) ? def.configDir : void 0,
3231
+ configDir: existsSync15(def.configDir) ? def.configDir : void 0,
2203
3232
  hasFigmaMcp: !!figmaMcpServerName,
2204
3233
  figmaMcpServerName
2205
3234
  });
@@ -2217,22 +3246,22 @@ var init_detect = __esm({
2217
3246
  {
2218
3247
  type: "claude",
2219
3248
  binary: "claude",
2220
- configDir: path12.join(homedir(), ".claude"),
3249
+ configDir: path17.join(homedir(), ".claude"),
2221
3250
  mcpConfigPaths: [
2222
- path12.join(homedir(), ".claude", "plugins.json"),
2223
- path12.join(homedir(), ".claude.json")
3251
+ path17.join(homedir(), ".claude", "plugins.json"),
3252
+ path17.join(homedir(), ".claude.json")
2224
3253
  ]
2225
3254
  },
2226
3255
  {
2227
3256
  type: "cursor",
2228
3257
  binary: "cursor",
2229
- configDir: path12.join(homedir(), ".cursor"),
2230
- mcpConfigPaths: [path12.join(homedir(), ".cursor", "mcp.json")]
3258
+ configDir: path17.join(homedir(), ".cursor"),
3259
+ mcpConfigPaths: [path17.join(homedir(), ".cursor", "mcp.json")]
2231
3260
  },
2232
3261
  {
2233
3262
  type: "codex",
2234
3263
  binary: "codex",
2235
- configDir: path12.join(homedir(), ".codex"),
3264
+ configDir: path17.join(homedir(), ".codex"),
2236
3265
  mcpConfigPaths: []
2237
3266
  }
2238
3267
  ];
@@ -2240,27 +3269,27 @@ var init_detect = __esm({
2240
3269
  });
2241
3270
 
2242
3271
  // src/lib/agents/invoke.ts
2243
- import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
3272
+ import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile12 } from "fs/promises";
2244
3273
  import { tmpdir } from "os";
2245
- import path13 from "path";
3274
+ import path18 from "path";
2246
3275
  import readline from "readline";
2247
- import chalk6 from "chalk";
3276
+ import chalk8 from "chalk";
2248
3277
  async function savePromptToFile(prompt, projectRoot) {
2249
- const base = projectRoot ? path13.join(projectRoot, "tmp") : tmpdir();
2250
- const dir = path13.join(base, "eds-cli");
2251
- 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 });
2252
3281
  const filename = `from-design-${Date.now()}.md`;
2253
- const filePath = path13.join(dir, filename);
2254
- await writeFile8(filePath, prompt, "utf-8");
3282
+ const filePath = path18.join(dir, filename);
3283
+ await writeFile12(filePath, prompt, "utf-8");
2255
3284
  return filePath;
2256
3285
  }
2257
3286
  async function invokeAgent(agent, promptFile, cwd) {
2258
3287
  const { execa } = await import("execa");
2259
- const prompt = await readFile7(promptFile, "utf-8");
3288
+ const prompt = await readFile11(promptFile, "utf-8");
2260
3289
  try {
2261
3290
  switch (agent.type) {
2262
3291
  case "claude": {
2263
- logger.info(`Running Claude Code ${chalk6.dim("- live log:")}`);
3292
+ logger.info(`Running Claude Code ${chalk8.dim("- live log:")}`);
2264
3293
  const figmaServers = [
2265
3294
  ...agent.figmaMcpServerName ? [agent.figmaMcpServerName] : [],
2266
3295
  "figma-sse",
@@ -2323,10 +3352,10 @@ async function renderClaudeStream(sub, cwd) {
2323
3352
  const ora4 = (await import("ora")).default;
2324
3353
  const start = Date.now();
2325
3354
  const elapsed = () => `${Math.round((Date.now() - start) / 1e3)}s`;
2326
- const spinner = ora4({ text: `thinking ${chalk6.dim(elapsed())}`, color: "yellow" }).start();
3355
+ const spinner = ora4({ text: `thinking ${chalk8.dim(elapsed())}`, color: "yellow" }).start();
2327
3356
  let activity = "thinking";
2328
3357
  const timer = setInterval(() => {
2329
- spinner.text = `${activity} ${chalk6.dim(elapsed())}`;
3358
+ spinner.text = `${activity} ${chalk8.dim(elapsed())}`;
2330
3359
  }, 300);
2331
3360
  const pending = /* @__PURE__ */ new Map();
2332
3361
  const emit = (line) => {
@@ -2352,7 +3381,7 @@ async function renderClaudeStream(sub, cwd) {
2352
3381
  for (const block2 of content) {
2353
3382
  if (block2.type === "text" && "text" in block2) {
2354
3383
  const first = block2.text.trim().split("\n")[0].slice(0, 100);
2355
- if (first) emit(chalk6.dim(first));
3384
+ if (first) emit(chalk8.dim(first));
2356
3385
  } else if (block2.type === "tool_use" && "name" in block2) {
2357
3386
  const id = "id" in block2 && block2.id ? block2.id : `${Math.random()}`;
2358
3387
  pending.set(id, {
@@ -2368,8 +3397,8 @@ async function renderClaudeStream(sub, cwd) {
2368
3397
  const p = pending.get(block2.tool_use_id);
2369
3398
  if (p) {
2370
3399
  const dur = `${((Date.now() - p.start) / 1e3).toFixed(1)}s`;
2371
- const failed = "is_error" in block2 && block2.is_error ? ` ${chalk6.red("failed")}` : "";
2372
- emit(`${p.label} ${chalk6.dim(dur)}${failed}`);
3400
+ const failed = "is_error" in block2 && block2.is_error ? ` ${chalk8.red("failed")}` : "";
3401
+ emit(`${p.label} ${chalk8.dim(dur)}${failed}`);
2373
3402
  pending.delete(block2.tool_use_id);
2374
3403
  }
2375
3404
  }
@@ -2381,9 +3410,9 @@ async function renderClaudeStream(sub, cwd) {
2381
3410
  }
2382
3411
  }
2383
3412
  }
2384
- for (const p of pending.values()) emit(`${p.label} ${chalk6.dim("\u2026")}`);
3413
+ for (const p of pending.values()) emit(`${p.label} ${chalk8.dim("\u2026")}`);
2385
3414
  clearInterval(timer);
2386
- if (result) spinner.succeed(`${brand.accent("agent finished")} ${chalk6.dim(result)}`);
3415
+ if (result) spinner.succeed(`${brand.accent("agent finished")} ${chalk8.dim(result)}`);
2387
3416
  else spinner.stop();
2388
3417
  } catch (err) {
2389
3418
  clearInterval(timer);
@@ -2396,22 +3425,22 @@ function toolLabel(name, input, cwd) {
2396
3425
  const rel = (key) => {
2397
3426
  const p = input?.[key];
2398
3427
  if (typeof p !== "string") return "";
2399
- return p.startsWith(cwd) ? path13.relative(cwd, p) : path13.basename(p);
3428
+ return p.startsWith(cwd) ? path18.relative(cwd, p) : path18.basename(p);
2400
3429
  };
2401
3430
  if (name.includes("figma")) {
2402
3431
  const short = name.split("__").pop() ?? name;
2403
- return `${icon.figma} ${chalk6.bold(short)} ${chalk6.dim("Figma MCP")}`;
3432
+ return `${icon.figma} ${chalk8.bold(short)} ${chalk8.dim("Figma MCP")}`;
2404
3433
  }
2405
3434
  switch (name) {
2406
3435
  case "Write":
2407
3436
  case "Edit":
2408
3437
  case "NotebookEdit":
2409
- return `${brand.accent("\u270E")} ${chalk6.cyan(rel("file_path"))}`;
3438
+ return `${brand.accent("\u270E")} ${chalk8.cyan(rel("file_path"))}`;
2410
3439
  case "Read":
2411
- return chalk6.dim(`\xB7 read ${rel("file_path")}`);
3440
+ return chalk8.dim(`\xB7 read ${rel("file_path")}`);
2412
3441
  case "Bash": {
2413
3442
  const cmd = typeof input?.command === "string" ? input.command.split("\n")[0].slice(0, 70) : "";
2414
- return chalk6.dim(`$ ${cmd}`);
3443
+ return chalk8.dim(`$ ${cmd}`);
2415
3444
  }
2416
3445
  default:
2417
3446
  return `${brand.blue("\u2192")} ${name}`;
@@ -2427,13 +3456,13 @@ var init_invoke = __esm({
2427
3456
  });
2428
3457
 
2429
3458
  // src/lib/config.ts
2430
- import { existsSync as existsSync11 } from "fs";
2431
- import { readFile as readFile8 } from "fs/promises";
3459
+ import { existsSync as existsSync16 } from "fs";
3460
+ import { readFile as readFile12 } from "fs/promises";
2432
3461
  import { homedir as homedir2 } from "os";
2433
- import path14 from "path";
3462
+ import path19 from "path";
2434
3463
  async function loadJsonFile(filePath) {
2435
3464
  try {
2436
- const content = await readFile8(filePath, "utf-8");
3465
+ const content = await readFile12(filePath, "utf-8");
2437
3466
  return JSON.parse(content);
2438
3467
  } catch {
2439
3468
  return void 0;
@@ -2441,7 +3470,7 @@ async function loadJsonFile(filePath) {
2441
3470
  }
2442
3471
  async function loadConfig(projectRoot) {
2443
3472
  const globalRaw = await loadJsonFile(GLOBAL_CONFIG_PATH);
2444
- const projectRaw = projectRoot ? await loadJsonFile(path14.join(projectRoot, PROJECT_CONFIG_NAME)) : void 0;
3473
+ const projectRaw = projectRoot ? await loadJsonFile(path19.join(projectRoot, PROJECT_CONFIG_NAME)) : void 0;
2445
3474
  const merged = {
2446
3475
  ...typeof globalRaw === "object" && globalRaw !== null ? globalRaw : {},
2447
3476
  ...typeof projectRaw === "object" && projectRaw !== null ? projectRaw : {}
@@ -2460,7 +3489,7 @@ function globalConfigPath() {
2460
3489
  return GLOBAL_CONFIG_PATH;
2461
3490
  }
2462
3491
  function globalConfigExists() {
2463
- return existsSync11(GLOBAL_CONFIG_PATH);
3492
+ return existsSync16(GLOBAL_CONFIG_PATH);
2464
3493
  }
2465
3494
  var GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_PATH, PROJECT_CONFIG_NAME;
2466
3495
  var init_config = __esm({
@@ -2469,8 +3498,8 @@ var init_config = __esm({
2469
3498
  init_esm_shims();
2470
3499
  init_logger();
2471
3500
  init_schemas();
2472
- GLOBAL_CONFIG_DIR = path14.join(homedir2(), ".eds");
2473
- GLOBAL_CONFIG_PATH = path14.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");
2474
3503
  PROJECT_CONFIG_NAME = ".edsrc.json";
2475
3504
  }
2476
3505
  });
@@ -2520,9 +3549,9 @@ var init_node_id = __esm({
2520
3549
  });
2521
3550
 
2522
3551
  // src/lib/figma/prompt-builder.ts
2523
- import { existsSync as existsSync12 } from "fs";
2524
- import { readFile as readFile9 } from "fs/promises";
2525
- import path15 from "path";
3552
+ import { existsSync as existsSync17 } from "fs";
3553
+ import { readFile as readFile13 } from "fs/promises";
3554
+ import path20 from "path";
2526
3555
  function contentSourceGuidance(source, cfHint, blockName) {
2527
3556
  if (!source || source === "document" || source === "ue") return "";
2528
3557
  const q = cfHint ? ` (model / persisted query: \`${cfHint}\`)` : "";
@@ -2651,6 +3680,9 @@ Rules:
2651
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.
2652
3681
 
2653
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")}
2654
3686
  ` : "";
2655
3687
  return `# Pixel-Perfect EDS Block from ${providerName}
2656
3688
 
@@ -2734,10 +3766,10 @@ ${tokensHint}
2734
3766
  `;
2735
3767
  }
2736
3768
  async function getTokensHint(projectRoot) {
2737
- const stylesPath = path15.join(projectRoot, "styles", "styles.css");
2738
- if (!existsSync12(stylesPath)) return "";
3769
+ const stylesPath = path20.join(projectRoot, "styles", "styles.css");
3770
+ if (!existsSync17(stylesPath)) return "";
2739
3771
  try {
2740
- const css = await readFile9(stylesPath, "utf-8");
3772
+ const css = await readFile13(stylesPath, "utf-8");
2741
3773
  const rootMatch = css.match(/:root\s*\{([^}]+)\}/);
2742
3774
  if (!rootMatch) return "";
2743
3775
  const props = rootMatch[1].split("\n").filter((line) => line.trim().startsWith("--")).map((line) => line.trim()).join("\n ");
@@ -2762,6 +3794,7 @@ var init_prompt_builder = __esm({
2762
3794
  "src/lib/figma/prompt-builder.ts"() {
2763
3795
  "use strict";
2764
3796
  init_esm_shims();
3797
+ init_partials();
2765
3798
  PROMPT_VERSION = "0.5.0";
2766
3799
  PROVIDER_NAMES = {
2767
3800
  figma: "Figma",
@@ -2878,11 +3911,11 @@ var init_harness = __esm({
2878
3911
  });
2879
3912
 
2880
3913
  // src/lib/preview/server.ts
2881
- import { createReadStream, existsSync as existsSync13, statSync } from "fs";
3914
+ import { createReadStream, existsSync as existsSync18, statSync } from "fs";
2882
3915
  import http from "http";
2883
- import path16 from "path";
3916
+ import path21 from "path";
2884
3917
  function startServer(root, routes, port, host = "127.0.0.1") {
2885
- const resolvedRoot = path16.resolve(root);
3918
+ const resolvedRoot = path21.resolve(root);
2886
3919
  const clients = /* @__PURE__ */ new Set();
2887
3920
  const server = http.createServer((req, res) => {
2888
3921
  const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
@@ -2908,19 +3941,19 @@ function startServer(root, routes, port, host = "127.0.0.1") {
2908
3941
  res.end(route.body);
2909
3942
  return;
2910
3943
  }
2911
- const filePath = path16.join(resolvedRoot, urlPath);
3944
+ const filePath = path21.join(resolvedRoot, urlPath);
2912
3945
  if (!filePath.startsWith(resolvedRoot)) {
2913
3946
  res.writeHead(403);
2914
3947
  res.end("Forbidden");
2915
3948
  return;
2916
3949
  }
2917
- if (!existsSync13(filePath) || !statSync(filePath).isFile()) {
3950
+ if (!existsSync18(filePath) || !statSync(filePath).isFile()) {
2918
3951
  res.writeHead(404, { "content-type": "text/plain" });
2919
3952
  res.end(`Not found: ${urlPath}`);
2920
3953
  return;
2921
3954
  }
2922
3955
  res.writeHead(200, {
2923
- "content-type": MIME[path16.extname(filePath).toLowerCase()] ?? "application/octet-stream"
3956
+ "content-type": MIME[path21.extname(filePath).toLowerCase()] ?? "application/octet-stream"
2924
3957
  });
2925
3958
  createReadStream(filePath).pipe(res);
2926
3959
  });
@@ -2984,10 +4017,10 @@ var preview_exports = {};
2984
4017
  __export(preview_exports, {
2985
4018
  previewBlock: () => previewBlock
2986
4019
  });
2987
- import { existsSync as existsSync14, watch } from "fs";
2988
- import { readFile as readFile10 } from "fs/promises";
2989
- import path17 from "path";
2990
- import chalk7 from "chalk";
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";
2991
4024
  function labelFor(width) {
2992
4025
  if (width <= 480) return `${width} mobile`;
2993
4026
  if (width <= 1024) return `${width} tablet`;
@@ -3039,30 +4072,30 @@ async function previewBlock(name, options = {}) {
3039
4072
  process.exitCode = 1;
3040
4073
  return;
3041
4074
  }
3042
- const blockDir = path17.join(projectRoot, "blocks", name);
3043
- if (!existsSync14(blockDir)) {
4075
+ const blockDir = path22.join(projectRoot, "blocks", name);
4076
+ if (!existsSync19(blockDir)) {
3044
4077
  logger.error(`Block "${name}" not found at blocks/${name}/.`);
3045
4078
  process.exitCode = 1;
3046
4079
  return;
3047
4080
  }
3048
- if (!existsSync14(path17.join(blockDir, `${name}.css`)) && !existsSync14(path17.join(blockDir, `${name}.js`))) {
4081
+ if (!existsSync19(path22.join(blockDir, `${name}.css`)) && !existsSync19(path22.join(blockDir, `${name}.js`))) {
3049
4082
  logger.error(`Block "${name}" has no ${name}.css / ${name}.js to preview.`);
3050
4083
  process.exitCode = 1;
3051
4084
  return;
3052
4085
  }
3053
4086
  let meta = null;
3054
- const metaPath = path17.join(blockDir, ".eds-meta.json");
3055
- if (existsSync14(metaPath)) {
4087
+ const metaPath = path22.join(blockDir, ".eds-meta.json");
4088
+ if (existsSync19(metaPath)) {
3056
4089
  try {
3057
- meta = JSON.parse(await readFile10(metaPath, "utf-8"));
4090
+ meta = JSON.parse(await readFile14(metaPath, "utf-8"));
3058
4091
  } catch {
3059
4092
  }
3060
4093
  }
3061
- const samplePath = path17.join(blockDir, `_${name}.preview.html`);
4094
+ const samplePath = path22.join(blockDir, `_${name}.preview.html`);
3062
4095
  let sampleContent;
3063
4096
  let placeholder = false;
3064
- if (existsSync14(samplePath)) {
3065
- sampleContent = await readFile10(samplePath, "utf-8");
4097
+ if (existsSync19(samplePath)) {
4098
+ sampleContent = await readFile14(samplePath, "utf-8");
3066
4099
  } else {
3067
4100
  sampleContent = defaultSample(name);
3068
4101
  placeholder = true;
@@ -3112,9 +4145,9 @@ async function previewBlock(name, options = {}) {
3112
4145
  reloadTimer = setTimeout(() => server.reload(), 150);
3113
4146
  });
3114
4147
  logger.info("");
3115
- logger.info(` ${chalk7.bold("\u279C")} ${chalk7.cyan(server.url)}`);
4148
+ logger.info(` ${chalk9.bold("\u279C")} ${chalk9.cyan(server.url)}`);
3116
4149
  logger.info(statusLine("pass", "live reload", "edits to the block refresh the browser"));
3117
- logger.info(chalk7.dim(" Press Ctrl+C to stop.\n"));
4150
+ logger.info(chalk9.dim(" Press Ctrl+C to stop.\n"));
3118
4151
  if (options.open !== false) await openInBrowser(server.url);
3119
4152
  const stop = async () => {
3120
4153
  watcher.close();
@@ -3143,10 +4176,10 @@ var from_design_exports = {};
3143
4176
  __export(from_design_exports, {
3144
4177
  blockFromDesign: () => blockFromDesign
3145
4178
  });
3146
- import { existsSync as existsSync15 } from "fs";
3147
- import { readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
3148
- import path18 from "path";
3149
- import chalk8 from "chalk";
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";
3150
4183
  import ora from "ora";
3151
4184
  async function previewRunning(port = 8777) {
3152
4185
  try {
@@ -3296,7 +4329,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3296
4329
  try {
3297
4330
  figma2 = parseFigmaUrl(designUrl);
3298
4331
  spinner.succeed(
3299
- `Parsed Figma URL ${chalk8.dim(figma2.nodeId ? `node ${figma2.nodeId}` : `file ${figma2.fileKey}`)}`
4332
+ `Parsed Figma URL ${chalk10.dim(figma2.nodeId ? `node ${figma2.nodeId}` : `file ${figma2.fileKey}`)}`
3300
4333
  );
3301
4334
  } catch (err) {
3302
4335
  spinner.fail(err.message);
@@ -3304,7 +4337,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3304
4337
  return;
3305
4338
  }
3306
4339
  } else {
3307
- logger.info(` ${icon.pass} ${providerName} design ${chalk8.dim(designRef)}`);
4340
+ logger.info(` ${icon.pass} ${providerName} design ${chalk10.dim(designRef)}`);
3308
4341
  }
3309
4342
  const blockName = options.name || (figma2 ? inferBlockName(figma2.fileName) : "");
3310
4343
  if (!kebabCaseRegex.test(blockName)) {
@@ -3327,20 +4360,20 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3327
4360
  screenshot: options.screenshot !== false
3328
4361
  });
3329
4362
  const promptFile = await savePromptToFile(prompt, projectRoot);
3330
- const hasTokens = existsSync15(path18.join(projectRoot, "styles", "styles.css"));
4363
+ const hasTokens = existsSync20(path23.join(projectRoot, "styles", "styles.css"));
3331
4364
  promptSpinner.succeed(
3332
- hasTokens ? `Loaded design tokens ${chalk8.dim("styles/styles.css")}` : `Built prompt ${chalk8.dim("no project tokens found")}`
4365
+ hasTokens ? `Loaded design tokens ${chalk10.dim("styles/styles.css")}` : `Built prompt ${chalk10.dim("no project tokens found")}`
3333
4366
  );
3334
- logger.info(chalk8.dim(` prompt \u2192 ${promptFile}`));
4367
+ logger.info(chalk10.dim(` prompt \u2192 ${promptFile}`));
3335
4368
  if (options.dryRun) {
3336
4369
  logger.info("");
3337
- logger.info(chalk8.yellow("Dry run mode \u2014 prompt generated but agent not invoked."));
3338
- logger.info(`Prompt file: ${chalk8.cyan(promptFile)}`);
3339
- logger.info(`Block name: ${chalk8.cyan(blockName)}`);
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)}`);
3340
4373
  logger.info(`Prompt version: ${PROMPT_VERSION}`);
3341
4374
  logger.info("");
3342
4375
  logger.info("Preview:");
3343
- logger.info(chalk8.dim("\u2500".repeat(60)));
4376
+ logger.info(chalk10.dim("\u2500".repeat(60)));
3344
4377
  console.log(prompt);
3345
4378
  return;
3346
4379
  }
@@ -3348,22 +4381,22 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3348
4381
  const preferredAgent = options.agent || config.agent;
3349
4382
  if (options.agent === "none") {
3350
4383
  logger.info("Agent skipped (--agent none). Use the prompt file manually:");
3351
- logger.info(` ${chalk8.cyan(promptFile)}`);
4384
+ logger.info(` ${chalk10.cyan(promptFile)}`);
3352
4385
  return;
3353
4386
  }
3354
4387
  const agentSpinner = ora("Detecting AI agent...").start();
3355
4388
  const agent = await detectAgent(preferredAgent);
3356
4389
  if (!agent) {
3357
4390
  agentSpinner.fail("No AI agent found (claude, cursor, or codex).");
3358
- logger.info(`Install Claude Code: ${chalk8.cyan("npm install -g @anthropic-ai/claude-code")}`);
3359
- logger.info(`Or use the prompt file manually: ${chalk8.cyan(promptFile)}`);
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)}`);
3360
4393
  process.exitCode = 1;
3361
4394
  return;
3362
4395
  }
3363
- agentSpinner.succeed(`Agent ready ${chalk8.cyan(agent.type)} ${chalk8.dim(agent.path)}`);
4396
+ agentSpinner.succeed(`Agent ready ${chalk10.cyan(agent.type)} ${chalk10.dim(agent.path)}`);
3364
4397
  if (provider === "figma" && !agent.hasFigmaMcp) {
3365
4398
  logger.warn(`Figma MCP may not be configured for ${agent.type}.`);
3366
- logger.info(`Run ${chalk8.cyan("eds figma setup")} to configure it.`);
4399
+ logger.info(`Run ${chalk10.cyan("eds figma setup")} to configure it.`);
3367
4400
  }
3368
4401
  if (!options.yes) {
3369
4402
  const { confirm } = await import("@inquirer/prompts");
@@ -3372,24 +4405,24 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3372
4405
  default: true
3373
4406
  });
3374
4407
  if (!proceed) {
3375
- logger.info(`Prompt saved to: ${chalk8.cyan(promptFile)}`);
4408
+ logger.info(`Prompt saved to: ${chalk10.cyan(promptFile)}`);
3376
4409
  return;
3377
4410
  }
3378
4411
  }
3379
4412
  logger.info(
3380
- ` ${chalk8.dim("\u2192")} Handing off to ${chalk8.cyan(agent.type)} ${chalk8.dim(`via ${providerName} MCP`)}`
4413
+ ` ${chalk10.dim("\u2192")} Handing off to ${chalk10.cyan(agent.type)} ${chalk10.dim(`via ${providerName} MCP`)}`
3381
4414
  );
3382
4415
  if (provider === "figma") {
3383
- logger.info(chalk8.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"));
3384
4417
  }
3385
4418
  const success = await invokeAgent(agent, promptFile, projectRoot);
3386
4419
  if (!success) {
3387
4420
  process.exitCode = 1;
3388
4421
  return;
3389
4422
  }
3390
- const blockDir = path18.join(projectRoot, "blocks", blockName);
4423
+ const blockDir = path23.join(projectRoot, "blocks", blockName);
3391
4424
  const ok = await verifyGeneratedBlock(blockDir, blockName, options.ueModel !== false);
3392
- if (existsSync15(blockDir)) {
4425
+ if (existsSync20(blockDir)) {
3393
4426
  const meta = {
3394
4427
  provider,
3395
4428
  designRef,
@@ -3399,7 +4432,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3399
4432
  promptVersion: PROMPT_VERSION,
3400
4433
  agentUsed: agent.type
3401
4434
  };
3402
- await writeFile9(path18.join(blockDir, ".eds-meta.json"), `${JSON.stringify(meta, null, 2)}
4435
+ await writeFile13(path23.join(blockDir, ".eds-meta.json"), `${JSON.stringify(meta, null, 2)}
3403
4436
  `);
3404
4437
  }
3405
4438
  if (!ok) {
@@ -3424,7 +4457,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3424
4457
  if (options.serve !== false) {
3425
4458
  if (await previewRunning()) {
3426
4459
  logger.info(
3427
- chalk8.dim(
4460
+ chalk10.dim(
3428
4461
  " A preview server is already running at http://127.0.0.1:8777 \u2014 your change will refresh there."
3429
4462
  )
3430
4463
  );
@@ -3434,15 +4467,15 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3434
4467
  await previewBlock2(blockName, {});
3435
4468
  }
3436
4469
  } else {
3437
- logger.info(` ${chalk8.dim("\u2192")} Next: ${chalk8.cyan(`eds block preview ${blockName}`)}`);
4470
+ logger.info(` ${chalk10.dim("\u2192")} Next: ${chalk10.cyan(`eds block preview ${blockName}`)}`);
3438
4471
  }
3439
4472
  }
3440
4473
  async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3441
4474
  const checks = [];
3442
- const jsPath = path18.join(blockDir, `${blockName}.js`);
3443
- const cssPath = path18.join(blockDir, `${blockName}.css`);
3444
- const hasJs = existsSync15(jsPath);
3445
- const hasCss = existsSync15(cssPath);
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);
3446
4479
  checks.push({
3447
4480
  status: hasJs ? "pass" : "fail",
3448
4481
  name: `${blockName}.js`,
@@ -3454,7 +4487,7 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3454
4487
  message: hasCss ? "created" : "missing \u2014 agent did not write the CSS file"
3455
4488
  });
3456
4489
  if (hasJs) {
3457
- const js = await readFile11(jsPath, "utf-8");
4490
+ const js = await readFile15(jsPath, "utf-8");
3458
4491
  checks.push({
3459
4492
  status: /export\s+default/.test(js) ? "pass" : "fail",
3460
4493
  name: "decorate export",
@@ -3469,7 +4502,7 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3469
4502
  }
3470
4503
  }
3471
4504
  if (hasCss) {
3472
- const css = await readFile11(cssPath, "utf-8");
4505
+ const css = await readFile15(cssPath, "utf-8");
3473
4506
  checks.push({
3474
4507
  status: css.includes(`.${blockName}`) ? "pass" : "warn",
3475
4508
  name: "scoped CSS",
@@ -3477,12 +4510,12 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3477
4510
  });
3478
4511
  }
3479
4512
  if (withUeModel) {
3480
- const hasPerBlock = existsSync15(path18.join(blockDir, `_${blockName}.json`));
3481
- const modelsFile = path18.join(blockDir, "..", "..", "component-models.json");
4513
+ const hasPerBlock = existsSync20(path23.join(blockDir, `_${blockName}.json`));
4514
+ const modelsFile = path23.join(blockDir, "..", "..", "component-models.json");
3482
4515
  let hasAggregated = false;
3483
- if (!hasPerBlock && existsSync15(modelsFile)) {
4516
+ if (!hasPerBlock && existsSync20(modelsFile)) {
3484
4517
  try {
3485
- hasAggregated = (await readFile11(modelsFile, "utf-8")).includes(`"${blockName}"`);
4518
+ hasAggregated = (await readFile15(modelsFile, "utf-8")).includes(`"${blockName}"`);
3486
4519
  } catch {
3487
4520
  }
3488
4521
  }
@@ -3494,8 +4527,8 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3494
4527
  });
3495
4528
  if (hasPerBlock && hasJs) {
3496
4529
  try {
3497
- const modelRaw = await readFile11(path18.join(blockDir, `_${blockName}.json`), "utf-8");
3498
- const js = await readFile11(jsPath, "utf-8");
4530
+ const modelRaw = await readFile15(path23.join(blockDir, `_${blockName}.json`), "utf-8");
4531
+ const js = await readFile15(jsPath, "utf-8");
3499
4532
  const isRepeatable = /"resourceType"\s*:\s*"[^"]*\/item"/.test(modelRaw) || /"filter"\s*:/.test(modelRaw) || /"multi"\s*:\s*true/.test(modelRaw);
3500
4533
  const iterates = /\.children/.test(js) || /\bfor\s*\(/.test(js) || /\.forEach\s*\(/.test(js);
3501
4534
  if (isRepeatable) {
@@ -3538,10 +4571,10 @@ var list_exports = {};
3538
4571
  __export(list_exports, {
3539
4572
  listBlocks: () => listBlocks
3540
4573
  });
3541
- import { existsSync as existsSync16 } from "fs";
3542
- import { readFile as readFile12, readdir as readdir2 } from "fs/promises";
3543
- import path19 from "path";
3544
- import chalk9 from "chalk";
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";
3545
4578
  async function listBlocks(options = {}) {
3546
4579
  const projectRoot = findProjectRoot();
3547
4580
  if (!projectRoot) {
@@ -3549,27 +4582,27 @@ async function listBlocks(options = {}) {
3549
4582
  process.exitCode = 1;
3550
4583
  return;
3551
4584
  }
3552
- const blocksDir = path19.join(projectRoot, "blocks");
3553
- if (!existsSync16(blocksDir)) {
4585
+ const blocksDir = path24.join(projectRoot, "blocks");
4586
+ if (!existsSync21(blocksDir)) {
3554
4587
  logger.info("No blocks/ directory found.");
3555
4588
  return;
3556
4589
  }
3557
- const entries = await readdir2(blocksDir, { withFileTypes: true });
4590
+ const entries = await readdir3(blocksDir, { withFileTypes: true });
3558
4591
  const blocks = [];
3559
4592
  for (const entry of entries) {
3560
4593
  if (!entry.isDirectory()) continue;
3561
4594
  const name = entry.name;
3562
- const blockDir = path19.join(blocksDir, name);
4595
+ const blockDir = path24.join(blocksDir, name);
3563
4596
  const info = {
3564
4597
  name,
3565
- hasJs: existsSync16(path19.join(blockDir, `${name}.js`)),
3566
- hasCss: existsSync16(path19.join(blockDir, `${name}.css`)),
3567
- hasUeModel: existsSync16(path19.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`))
3568
4601
  };
3569
- const metaFile = path19.join(blockDir, ".eds-meta.json");
3570
- if (existsSync16(metaFile)) {
4602
+ const metaFile = path24.join(blockDir, ".eds-meta.json");
4603
+ if (existsSync21(metaFile)) {
3571
4604
  try {
3572
- const meta = JSON.parse(await readFile12(metaFile, "utf-8"));
4605
+ const meta = JSON.parse(await readFile16(metaFile, "utf-8"));
3573
4606
  info.figmaSource = {
3574
4607
  fileKey: meta.figmaFileKey,
3575
4608
  nodeId: meta.figmaNodeId,
@@ -3592,17 +4625,17 @@ async function listBlocks(options = {}) {
3592
4625
  const nameWidth = columnWidth(blocks.map((b) => b.name));
3593
4626
  for (const b of blocks) {
3594
4627
  const badges = [
3595
- b.hasJs ? brand.ok("js") : chalk9.dim.strikethrough("js"),
3596
- b.hasCss ? brand.ok("css") : chalk9.dim.strikethrough("css")
4628
+ b.hasJs ? brand.ok("js") : chalk11.dim.strikethrough("js"),
4629
+ b.hasCss ? brand.ok("css") : chalk11.dim.strikethrough("css")
3597
4630
  ];
3598
4631
  if (b.hasUeModel) badges.push(brand.blue("ue-model"));
3599
4632
  if (b.figmaSource) badges.push(`${icon.figma} ${brand.dotPurple("figma")}`);
3600
- const name = padEndVisible(chalk9.bold(b.name), nameWidth);
3601
- logger.info(` ${name} ${chalk9.dim("[")}${badges.join(" ")}${chalk9.dim("]")}`);
4633
+ const name = padEndVisible(chalk11.bold(b.name), nameWidth);
4634
+ logger.info(` ${name} ${chalk11.dim("[")}${badges.join(" ")}${chalk11.dim("]")}`);
3602
4635
  if (b.figmaSource) {
3603
4636
  const synced = new Date(b.figmaSource.lastSyncedAt).toLocaleDateString();
3604
4637
  logger.info(
3605
- ` ${" ".repeat(nameWidth)} ${icon.arrow} ${chalk9.dim(
4638
+ ` ${" ".repeat(nameWidth)} ${icon.arrow} ${chalk11.dim(
3606
4639
  `figma:${b.figmaSource.fileKey} \xB7 synced ${synced}`
3607
4640
  )}`
3608
4641
  );
@@ -3782,10 +4815,10 @@ var integrate_exports = {};
3782
4815
  __export(integrate_exports, {
3783
4816
  integrate: () => integrate
3784
4817
  });
3785
- import { existsSync as existsSync17 } from "fs";
3786
- import { mkdir as mkdir6, readFile as readFile13, writeFile as writeFile10 } from "fs/promises";
3787
- import path20 from "path";
3788
- import chalk10 from "chalk";
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";
3789
4822
  async function integrate(type) {
3790
4823
  const projectRoot = findProjectRoot();
3791
4824
  if (!projectRoot) {
@@ -3808,16 +4841,16 @@ async function integrate(type) {
3808
4841
  return;
3809
4842
  }
3810
4843
  const values = {};
3811
- for (const f of integration.fields) {
3812
- const answer = await input({ message: ` ${f.message}`, default: f.placeholder });
3813
- values[f.name] = answer.trim();
4844
+ for (const f2 of integration.fields) {
4845
+ const answer = await input({ message: ` ${f2.message}`, default: f2.placeholder });
4846
+ values[f2.name] = answer.trim();
3814
4847
  }
3815
4848
  const modulePaths = [];
3816
4849
  if (integration.module) {
3817
4850
  for (const mod of integration.module(values)) {
3818
- const abs = path20.join(projectRoot, mod.file);
3819
- await mkdir6(path20.dirname(abs), { recursive: true });
3820
- await writeFile10(abs, mod.content);
4851
+ const abs = path25.join(projectRoot, mod.file);
4852
+ await mkdir8(path25.dirname(abs), { recursive: true });
4853
+ await writeFile14(abs, mod.content);
3821
4854
  modulePaths.push(mod.file);
3822
4855
  }
3823
4856
  }
@@ -3825,26 +4858,26 @@ async function integrate(type) {
3825
4858
  // ${integration.name} (eds integrate)
3826
4859
  ${integration.code(values)}
3827
4860
  ` : wrapBlock(integration.name, integration.code(values));
3828
- const delayedPath = path20.join(projectRoot, "scripts", "delayed.js");
3829
- await mkdir6(path20.dirname(delayedPath), { recursive: true });
3830
- const existing = existsSync17(delayedPath) ? await readFile13(delayedPath, "utf-8") : DELAYED_HEADER;
3831
- await writeFile10(delayedPath, `${existing.replace(/\s*$/, "")}
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*$/, "")}
3832
4865
  ${block2}`);
3833
4866
  logger.info(heading("Added"));
3834
4867
  for (const p of modulePaths) logger.info(accentLine(integration.name, `wrote ${p}`));
3835
4868
  logger.info(accentLine(integration.name, "wired into scripts/delayed.js"));
3836
4869
  logger.info("");
3837
4870
  logger.info(
3838
- chalk10.dim(" Loads after LCP (delayed phase) - zero impact on your Core Web Vitals.")
4871
+ chalk12.dim(" Loads after LCP (delayed phase) - zero impact on your Core Web Vitals.")
3839
4872
  );
3840
4873
  if (integration.id === "gtm" || integration.id === "ga4") {
3841
4874
  logger.info(
3842
- chalk10.dim(" Tip: gate analytics behind user consent (add the Cookiebot integration).")
4875
+ chalk12.dim(" Tip: gate analytics behind user consent (add the Cookiebot integration).")
3843
4876
  );
3844
4877
  }
3845
4878
  if (integration.id === "chat") {
3846
4879
  logger.info(
3847
- chalk10.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).")
3848
4881
  );
3849
4882
  }
3850
4883
  logger.info("");
@@ -3870,9 +4903,9 @@ var setup_exports = {};
3870
4903
  __export(setup_exports, {
3871
4904
  figmaSetup: () => figmaSetup
3872
4905
  });
3873
- import { existsSync as existsSync18 } from "fs";
3874
- import { mkdir as mkdir7, writeFile as writeFile11 } from "fs/promises";
3875
- import chalk11 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";
3876
4909
  async function figmaSetup() {
3877
4910
  const { select, confirm } = await import("@inquirer/prompts");
3878
4911
  logger.logoOnce(logo());
@@ -3882,15 +4915,15 @@ async function figmaSetup() {
3882
4915
  let selectedAgent;
3883
4916
  if (agents.length === 0) {
3884
4917
  logger.warn("No AI agents found (claude, cursor, codex).");
3885
- logger.info(`Install Claude Code: ${chalk11.cyan("npm install -g @anthropic-ai/claude-code")}`);
4918
+ logger.info(`Install Claude Code: ${chalk13.cyan("npm install -g @anthropic-ai/claude-code")}`);
3886
4919
  selectedAgent = "none";
3887
4920
  } else {
3888
- logger.success(`Found: ${agents.map((a) => chalk11.cyan(a.type)).join(", ")}`);
4921
+ logger.success(`Found: ${agents.map((a) => chalk13.cyan(a.type)).join(", ")}`);
3889
4922
  selectedAgent = await select({
3890
4923
  message: "Which agent do you want to use with eds-cli?",
3891
4924
  choices: [
3892
4925
  ...agents.map((a) => ({
3893
- name: `${a.type}${a.hasFigmaMcp ? chalk11.green(" (Figma MCP detected)") : ""}`,
4926
+ name: `${a.type}${a.hasFigmaMcp ? chalk13.green(" (Figma MCP detected)") : ""}`,
3894
4927
  value: a.type
3895
4928
  })),
3896
4929
  { name: "None (manual mode)", value: "none" }
@@ -3906,17 +4939,17 @@ async function figmaSetup() {
3906
4939
  switch (selectedAgent) {
3907
4940
  case "claude":
3908
4941
  logger.info(
3909
- ` ${chalk11.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")}`
3910
4943
  );
3911
4944
  logger.info(" or for Figma Desktop app:");
3912
4945
  logger.info(
3913
- ` ${chalk11.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")}`
3914
4947
  );
3915
4948
  break;
3916
4949
  case "cursor":
3917
- logger.info(` Add to ${chalk11.cyan("~/.cursor/mcp.json")}:`);
4950
+ logger.info(` Add to ${chalk13.cyan("~/.cursor/mcp.json")}:`);
3918
4951
  logger.info(
3919
- chalk11.dim(
4952
+ chalk13.dim(
3920
4953
  JSON.stringify(
3921
4954
  {
3922
4955
  mcpServers: {
@@ -3941,7 +4974,7 @@ async function figmaSetup() {
3941
4974
  }
3942
4975
  }
3943
4976
  logger.info(
3944
- chalk11.dim(
4977
+ chalk13.dim(
3945
4978
  "\nNote: Figma MCP requires a Figma seat (Dev or Full on paid plans for desktop server)."
3946
4979
  )
3947
4980
  );
@@ -3951,22 +4984,22 @@ async function figmaSetup() {
3951
4984
  });
3952
4985
  if (shouldSave) {
3953
4986
  const configDir = globalConfigDir();
3954
- if (!existsSync18(configDir)) {
3955
- await mkdir7(configDir, { recursive: true });
4987
+ if (!existsSync23(configDir)) {
4988
+ await mkdir9(configDir, { recursive: true });
3956
4989
  }
3957
4990
  const config = {};
3958
4991
  if (selectedAgent !== "none") {
3959
4992
  config.agent = selectedAgent;
3960
4993
  }
3961
- await writeFile11(globalConfigPath(), `${JSON.stringify(config, null, 2)}
4994
+ await writeFile15(globalConfigPath(), `${JSON.stringify(config, null, 2)}
3962
4995
  `, {
3963
4996
  mode: 384
3964
4997
  });
3965
- logger.success(`Config saved to ${chalk11.cyan(globalConfigPath())}`);
4998
+ logger.success(`Config saved to ${chalk13.cyan(globalConfigPath())}`);
3966
4999
  }
3967
- logger.info(chalk11.bold("\nSetup complete!"));
5000
+ logger.info(chalk13.bold("\nSetup complete!"));
3968
5001
  if (selectedAgent !== "none") {
3969
- logger.info(`Try: ${chalk11.cyan('eds block from-design "<figma-url>" --dry-run')}`);
5002
+ logger.info(`Try: ${chalk13.cyan('eds block from-design "<figma-url>" --dry-run')}`);
3970
5003
  }
3971
5004
  }
3972
5005
  var init_setup = __esm({
@@ -3985,10 +5018,10 @@ var doctor_exports = {};
3985
5018
  __export(doctor_exports, {
3986
5019
  doctor: () => doctor
3987
5020
  });
3988
- import { existsSync as existsSync19 } from "fs";
3989
- import { readFile as readFile14, readdir as readdir3 } from "fs/promises";
3990
- import path21 from "path";
3991
- 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";
3992
5025
  async function doctor(options = {}) {
3993
5026
  const projectRoot = findProjectRoot();
3994
5027
  if (!projectRoot) {
@@ -4009,8 +5042,8 @@ async function doctor(options = {}) {
4009
5042
  });
4010
5043
  if (project.hasFstab) {
4011
5044
  try {
4012
- const content = await readFile14(path21.join(projectRoot, "fstab.yaml"), "utf-8");
4013
- parseYaml(content);
5045
+ const content = await readFile18(path26.join(projectRoot, "fstab.yaml"), "utf-8");
5046
+ parseYaml2(content);
4014
5047
  results.push({ name: "fstab.yaml valid YAML", status: "pass", message: "Valid" });
4015
5048
  } catch {
4016
5049
  results.push({
@@ -4031,28 +5064,28 @@ async function doctor(options = {}) {
4031
5064
  message: project.hasBlocks ? "Found" : "Missing - no blocks yet"
4032
5065
  });
4033
5066
  if (project.hasBlocks) {
4034
- const blocksDir = path21.join(projectRoot, "blocks");
4035
- const entries = await readdir3(blocksDir, { withFileTypes: true });
5067
+ const blocksDir = path26.join(projectRoot, "blocks");
5068
+ const entries = await readdir4(blocksDir, { withFileTypes: true });
4036
5069
  let aggregatedModels = "";
4037
- const aggregatedPath = path21.join(projectRoot, "component-models.json");
4038
- if (existsSync19(aggregatedPath)) {
5070
+ const aggregatedPath = path26.join(projectRoot, "component-models.json");
5071
+ if (existsSync24(aggregatedPath)) {
4039
5072
  try {
4040
- aggregatedModels = await readFile14(aggregatedPath, "utf-8");
5073
+ aggregatedModels = await readFile18(aggregatedPath, "utf-8");
4041
5074
  } catch {
4042
5075
  }
4043
5076
  }
4044
5077
  for (const entry of entries) {
4045
5078
  if (!entry.isDirectory()) continue;
4046
5079
  const blockName = entry.name;
4047
- const jsFile = path21.join(blocksDir, blockName, `${blockName}.js`);
4048
- const hasJs = existsSync19(jsFile);
5080
+ const jsFile = path26.join(blocksDir, blockName, `${blockName}.js`);
5081
+ const hasJs = existsSync24(jsFile);
4049
5082
  results.push({
4050
5083
  name: `blocks/${blockName} has JS`,
4051
5084
  status: hasJs ? "pass" : "warn",
4052
5085
  message: hasJs ? "Found" : `Missing ${blockName}.js`
4053
5086
  });
4054
5087
  if (hasJs) {
4055
- const jsContent = await readFile14(jsFile, "utf-8");
5088
+ const jsContent = await readFile18(jsFile, "utf-8");
4056
5089
  if (jsContent.includes("console.log")) {
4057
5090
  results.push({
4058
5091
  name: `blocks/${blockName} no console.log`,
@@ -4061,17 +5094,17 @@ async function doctor(options = {}) {
4061
5094
  });
4062
5095
  }
4063
5096
  }
4064
- const hasPerBlockModel = existsSync19(path21.join(blocksDir, blockName, `_${blockName}.json`));
5097
+ const hasPerBlockModel = existsSync24(path26.join(blocksDir, blockName, `_${blockName}.json`));
4065
5098
  const hasAggregatedModel = aggregatedModels.includes(`"${blockName}"`);
4066
5099
  results.push({
4067
5100
  name: `blocks/${blockName} UE model`,
4068
5101
  status: hasPerBlockModel || hasAggregatedModel ? "pass" : "warn",
4069
5102
  message: hasPerBlockModel || hasAggregatedModel ? hasPerBlockModel ? `_${blockName}.json` : "in component-models.json" : "no Universal Editor model - authors can't edit this block in UE"
4070
5103
  });
4071
- const metaFile = path21.join(blocksDir, blockName, ".eds-meta.json");
4072
- if (existsSync19(metaFile)) {
5104
+ const metaFile = path26.join(blocksDir, blockName, ".eds-meta.json");
5105
+ if (existsSync24(metaFile)) {
4073
5106
  try {
4074
- const meta = JSON.parse(await readFile14(metaFile, "utf-8"));
5107
+ const meta = JSON.parse(await readFile18(metaFile, "utf-8"));
4075
5108
  const syncDate = new Date(meta.lastSyncedAt);
4076
5109
  const daysSinceSync = Math.floor(
4077
5110
  (Date.now() - syncDate.getTime()) / (1e3 * 60 * 60 * 24)
@@ -4091,11 +5124,11 @@ async function doctor(options = {}) {
4091
5124
  }
4092
5125
  }
4093
5126
  }
4094
- const queryFile = path21.join(projectRoot, "helix-query.yaml");
4095
- if (existsSync19(queryFile)) {
5127
+ const queryFile = path26.join(projectRoot, "helix-query.yaml");
5128
+ if (existsSync24(queryFile)) {
4096
5129
  try {
4097
- const content = await readFile14(queryFile, "utf-8");
4098
- parseYaml(content);
5130
+ const content = await readFile18(queryFile, "utf-8");
5131
+ parseYaml2(content);
4099
5132
  results.push({ name: "helix-query.yaml valid", status: "pass", message: "Valid YAML" });
4100
5133
  } catch {
4101
5134
  results.push({
@@ -4151,10 +5184,10 @@ var audit_loading_exports = {};
4151
5184
  __export(audit_loading_exports, {
4152
5185
  auditLoading: () => auditLoading
4153
5186
  });
4154
- import { existsSync as existsSync20 } from "fs";
4155
- import { readFile as readFile15, readdir as readdir4, stat as stat3 } from "fs/promises";
4156
- import path22 from "path";
4157
- import chalk12 from "chalk";
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";
4158
5191
  async function auditLoading(options = {}) {
4159
5192
  const projectRoot = findProjectRoot();
4160
5193
  if (!projectRoot) {
@@ -4187,8 +5220,8 @@ async function auditLoading(options = {}) {
4187
5220
  printResults(results);
4188
5221
  }
4189
5222
  async function checkHeadHtml(root, results) {
4190
- const filePath = path22.join(root, "head.html");
4191
- if (!existsSync20(filePath)) {
5223
+ const filePath = path27.join(root, "head.html");
5224
+ if (!existsSync25(filePath)) {
4192
5225
  results.push({
4193
5226
  rule: "head.html exists",
4194
5227
  status: "warn",
@@ -4196,7 +5229,7 @@ async function checkHeadHtml(root, results) {
4196
5229
  });
4197
5230
  return;
4198
5231
  }
4199
- const content = await readFile15(filePath, "utf-8");
5232
+ const content = await readFile19(filePath, "utf-8");
4200
5233
  const lines = content.split("\n");
4201
5234
  for (let i = 0; i < lines.length; i++) {
4202
5235
  const line = lines[i];
@@ -4268,8 +5301,8 @@ async function checkHeadHtml(root, results) {
4268
5301
  }
4269
5302
  }
4270
5303
  async function checkScriptsJs(root, results) {
4271
- const filePath = path22.join(root, "scripts", "scripts.js");
4272
- if (!existsSync20(filePath)) {
5304
+ const filePath = path27.join(root, "scripts", "scripts.js");
5305
+ if (!existsSync25(filePath)) {
4273
5306
  results.push({
4274
5307
  rule: "scripts/scripts.js exists",
4275
5308
  status: "warn",
@@ -4277,7 +5310,7 @@ async function checkScriptsJs(root, results) {
4277
5310
  });
4278
5311
  return;
4279
5312
  }
4280
- const content = await readFile15(filePath, "utf-8");
5313
+ const content = await readFile19(filePath, "utf-8");
4281
5314
  const lines = content.split("\n");
4282
5315
  const hasLoadEager = /function\s+loadEager|loadEager\s*=/i.test(content);
4283
5316
  const hasLoadLazy = /function\s+loadLazy|loadLazy\s*=/i.test(content);
@@ -4355,8 +5388,8 @@ async function checkScriptsJs(root, results) {
4355
5388
  }
4356
5389
  }
4357
5390
  async function checkStylesCss(root, results) {
4358
- const filePath = path22.join(root, "styles", "styles.css");
4359
- if (!existsSync20(filePath)) return;
5391
+ const filePath = path27.join(root, "styles", "styles.css");
5392
+ if (!existsSync25(filePath)) return;
4360
5393
  const stats = await stat3(filePath);
4361
5394
  const sizeKb = stats.size / 1024;
4362
5395
  results.push({
@@ -4365,7 +5398,7 @@ async function checkStylesCss(root, results) {
4365
5398
  message: `${sizeKb.toFixed(1)} KB${sizeKb > 40 ? " \u2014 large CSS competes with 100KB LCP budget" : ""}`,
4366
5399
  file: "styles/styles.css"
4367
5400
  });
4368
- const content = await readFile15(filePath, "utf-8");
5401
+ const content = await readFile19(filePath, "utf-8");
4369
5402
  if (/@import\s/.test(content)) {
4370
5403
  results.push({
4371
5404
  rule: "No @import in styles.css",
@@ -4376,8 +5409,8 @@ async function checkStylesCss(root, results) {
4376
5409
  }
4377
5410
  }
4378
5411
  async function checkDelayedJs(root, results) {
4379
- const filePath = path22.join(root, "scripts", "delayed.js");
4380
- if (!existsSync20(filePath)) {
5412
+ const filePath = path27.join(root, "scripts", "delayed.js");
5413
+ if (!existsSync25(filePath)) {
4381
5414
  results.push({
4382
5415
  rule: "scripts/delayed.js exists",
4383
5416
  status: "info",
@@ -4385,7 +5418,7 @@ async function checkDelayedJs(root, results) {
4385
5418
  });
4386
5419
  return;
4387
5420
  }
4388
- const content = await readFile15(filePath, "utf-8");
5421
+ const content = await readFile19(filePath, "utf-8");
4389
5422
  const stripped = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").trim();
4390
5423
  if (!stripped) {
4391
5424
  results.push({
@@ -4397,15 +5430,15 @@ async function checkDelayedJs(root, results) {
4397
5430
  }
4398
5431
  }
4399
5432
  async function checkBlocks(root, results) {
4400
- const blocksDir = path22.join(root, "blocks");
4401
- if (!existsSync20(blocksDir)) return;
4402
- 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 });
4403
5436
  for (const entry of entries) {
4404
5437
  if (!entry.isDirectory()) continue;
4405
5438
  const blockName = entry.name;
4406
- const jsFile = path22.join(blocksDir, blockName, `${blockName}.js`);
4407
- if (!existsSync20(jsFile)) continue;
4408
- const content = await readFile15(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");
4409
5442
  const lines = content.split("\n");
4410
5443
  const heavyLibs = [
4411
5444
  "jquery",
@@ -4455,9 +5488,9 @@ async function checkBlocks(root, results) {
4455
5488
  for (const entry of entries) {
4456
5489
  if (!entry.isDirectory()) continue;
4457
5490
  const blockName = entry.name;
4458
- const cssFile = path22.join(blocksDir, blockName, `${blockName}.css`);
4459
- if (!existsSync20(cssFile)) continue;
4460
- const content = await readFile15(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");
4461
5494
  const selectorPattern = /^([a-z][a-z0-9-]*)\s*\{/gm;
4462
5495
  for (const selectorMatch of content.matchAll(selectorPattern)) {
4463
5496
  const selector = selectorMatch[1];
@@ -4473,8 +5506,8 @@ async function checkBlocks(root, results) {
4473
5506
  }
4474
5507
  }
4475
5508
  async function checkAemJs(root, results) {
4476
- const filePath = path22.join(root, "scripts", "aem.js");
4477
- if (!existsSync20(filePath)) {
5509
+ const filePath = path27.join(root, "scripts", "aem.js");
5510
+ if (!existsSync25(filePath)) {
4478
5511
  results.push({
4479
5512
  rule: "scripts/aem.js exists",
4480
5513
  status: "warn",
@@ -4512,12 +5545,12 @@ function printResults(results) {
4512
5545
  grouped.set(key, bucket);
4513
5546
  }
4514
5547
  for (const [file, fileResults] of grouped) {
4515
- logger.info(` ${icon.bullet} ${chalk12.underline(file)}`);
5548
+ logger.info(` ${icon.bullet} ${chalk14.underline(file)}`);
4516
5549
  for (const r of fileResults) {
4517
- const loc = r.line ? chalk12.dim(`:${r.line}`) : "";
5550
+ const loc = r.line ? chalk14.dim(`:${r.line}`) : "";
4518
5551
  logger.info(` ${statusIcon(r.status)} ${r.rule}${loc}`);
4519
5552
  if (r.status !== "pass") {
4520
- logger.info(chalk12.dim(` ${r.message}`));
5553
+ logger.info(chalk14.dim(` ${r.message}`));
4521
5554
  }
4522
5555
  }
4523
5556
  logger.info("");
@@ -4546,14 +5579,14 @@ var audit_security_exports = {};
4546
5579
  __export(audit_security_exports, {
4547
5580
  auditSecurity: () => auditSecurity
4548
5581
  });
4549
- import { existsSync as existsSync21 } from "fs";
4550
- import { readFile as readFile16, readdir as readdir5 } from "fs/promises";
4551
- import path23 from "path";
4552
- import chalk13 from "chalk";
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";
4553
5586
  async function* walk2(dir) {
4554
- const entries = await readdir5(dir, { withFileTypes: true });
5587
+ const entries = await readdir6(dir, { withFileTypes: true });
4555
5588
  for (const entry of entries) {
4556
- const full = path23.join(dir, entry.name);
5589
+ const full = path28.join(dir, entry.name);
4557
5590
  if (entry.isDirectory()) {
4558
5591
  if (IGNORE_DIRS.has(entry.name)) continue;
4559
5592
  yield* walk2(full);
@@ -4587,11 +5620,11 @@ function scanContent(content, relFile, ext) {
4587
5620
  return findings;
4588
5621
  }
4589
5622
  async function auditDependencies(root) {
4590
- if (!existsSync21(path23.join(root, "package.json"))) {
5623
+ if (!existsSync26(path28.join(root, "package.json"))) {
4591
5624
  return { status: "skipped", note: "no package.json" };
4592
5625
  }
4593
- const hasLock = existsSync21(path23.join(root, "package-lock.json")) || existsSync21(path23.join(root, "npm-shrinkwrap.json"));
4594
- if (!hasLock && !existsSync21(path23.join(root, "node_modules"))) {
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"))) {
4595
5628
  return { status: "skipped", note: "no package-lock.json / node_modules \u2014 run npm install" };
4596
5629
  }
4597
5630
  try {
@@ -4623,15 +5656,15 @@ async function auditSecurity(options = {}) {
4623
5656
  }
4624
5657
  const findings = [];
4625
5658
  for await (const file of walk2(projectRoot)) {
4626
- const ext = path23.extname(file).slice(1).toLowerCase();
5659
+ const ext = path28.extname(file).slice(1).toLowerCase();
4627
5660
  if (!SCAN_EXT.has(ext)) continue;
4628
- const rel = path23.relative(projectRoot, file);
5661
+ const rel = path28.relative(projectRoot, file);
4629
5662
  if (rel.endsWith("aem.js") || rel.endsWith("lib-franklin.js")) continue;
4630
- const content = await readFile16(file, "utf-8");
5663
+ const content = await readFile20(file, "utf-8");
4631
5664
  findings.push(...scanContent(content, rel, ext));
4632
5665
  }
4633
5666
  const deps = await auditDependencies(projectRoot);
4634
- const bySeverity = (s) => findings.filter((f) => f.severity === s);
5667
+ const bySeverity = (s) => findings.filter((f2) => f2.severity === s);
4635
5668
  const codeCounts = {
4636
5669
  critical: bySeverity("critical").length,
4637
5670
  high: bySeverity("high").length,
@@ -4673,11 +5706,11 @@ async function auditSecurity(options = {}) {
4673
5706
  const sorted = [...findings].sort(
4674
5707
  (a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity)
4675
5708
  );
4676
- for (const f of sorted) {
4677
- const meta = SEVERITY_META[f.severity];
4678
- const loc = chalk13.dim(`${f.file}:${f.line}`);
4679
- logger.info(` ${statusIcon(meta.status)} ${chalk13.bold(meta.label)} ${f.message} ${loc}`);
4680
- logger.info(chalk13.dim(` ${f.snippet}`));
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}`));
4681
5714
  }
4682
5715
  }
4683
5716
  const summaryLine = summary({
@@ -4884,7 +5917,7 @@ __export(preview_exports2, {
4884
5917
  preview: () => preview,
4885
5918
  resolveAdminConfig: () => resolveAdminConfig
4886
5919
  });
4887
- import chalk14 from "chalk";
5920
+ import chalk16 from "chalk";
4888
5921
  import ora2 from "ora";
4889
5922
  async function preview(paths, options) {
4890
5923
  const projectRoot = findProjectRoot();
@@ -4897,7 +5930,7 @@ async function preview(paths, options) {
4897
5930
  let hasErrors = false;
4898
5931
  for (const r of results) {
4899
5932
  if (r.ok) {
4900
- logger.success(`${r.path} \u2192 ${chalk14.cyan(r.previewUrl || "preview updated")}`);
5933
+ logger.success(`${r.path} \u2192 ${chalk16.cyan(r.previewUrl || "preview updated")}`);
4901
5934
  } else {
4902
5935
  logger.error(`${r.path} \u2014 ${r.message}`);
4903
5936
  hasErrors = true;
@@ -4911,7 +5944,7 @@ function resolveAdminConfig(config, options) {
4911
5944
  const ref = options.ref || config.admin?.ref || "main";
4912
5945
  if (!org || !site) {
4913
5946
  logger.error("Missing --org and --site. Set them via flags or in .edsrc.json:");
4914
- logger.info(chalk14.dim(' { "admin": { "org": "my-org", "site": "my-site" } }'));
5947
+ logger.info(chalk16.dim(' { "admin": { "org": "my-org", "site": "my-site" } }'));
4915
5948
  process.exitCode = 1;
4916
5949
  return void 0;
4917
5950
  }
@@ -4933,7 +5966,7 @@ var publish_exports = {};
4933
5966
  __export(publish_exports, {
4934
5967
  publish: () => publish
4935
5968
  });
4936
- import chalk15 from "chalk";
5969
+ import chalk17 from "chalk";
4937
5970
  import ora3 from "ora";
4938
5971
  async function publish(paths, options) {
4939
5972
  const projectRoot = findProjectRoot();
@@ -4946,7 +5979,7 @@ async function publish(paths, options) {
4946
5979
  let hasErrors = false;
4947
5980
  for (const r of results) {
4948
5981
  if (r.ok) {
4949
- logger.success(`${r.path} \u2192 ${chalk15.cyan(r.liveUrl || "published")}`);
5982
+ logger.success(`${r.path} \u2192 ${chalk17.cyan(r.liveUrl || "published")}`);
4950
5983
  } else {
4951
5984
  logger.error(`${r.path} \u2014 ${r.message}`);
4952
5985
  hasErrors = true;
@@ -5045,13 +6078,13 @@ var menu_exports = {};
5045
6078
  __export(menu_exports, {
5046
6079
  mainMenu: () => mainMenu
5047
6080
  });
5048
- import { existsSync as existsSync22 } from "fs";
5049
- import { readdir as readdir6 } from "fs/promises";
5050
- import path24 from "path";
6081
+ import { existsSync as existsSync27 } from "fs";
6082
+ import { readdir as readdir7 } from "fs/promises";
6083
+ import path29 from "path";
5051
6084
  async function listBlockNames(projectRoot) {
5052
- const dir = path24.join(projectRoot, "blocks");
5053
- if (!existsSync22(dir)) return [];
5054
- 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 });
5055
6088
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
5056
6089
  }
5057
6090
  async function mainMenu() {
@@ -5061,6 +6094,7 @@ async function mainMenu() {
5061
6094
  message: "Choose an action",
5062
6095
  choices: [
5063
6096
  { name: "Set up this project (authoring model, CI, naming)", value: "init" },
6097
+ { name: "Generate a page template (initial content)", value: "template" },
5064
6098
  {
5065
6099
  name: "Generate a block from a design (Figma/Stitch/Canva/Sketch/Framer)",
5066
6100
  value: "from-design"
@@ -5069,7 +6103,9 @@ async function mainMenu() {
5069
6103
  { name: "Scaffold Universal Editor components", value: "scaffold" },
5070
6104
  { name: "Create an empty block", value: "create" },
5071
6105
  { name: "Add a third-party integration (GTM, chat, consent\u2026)", value: "integrate" },
6106
+ { name: "Add fields to a block model (partials)", value: "model" },
5072
6107
  { name: "Instrument a block for analytics (dataLayer)", value: "track" },
6108
+ { name: "Add JSON-LD structured data to a block", value: "schema" },
5073
6109
  { name: "Preview a block in the browser", value: "preview-block" },
5074
6110
  { name: "Preview / publish pages (Admin API)", value: "admin" },
5075
6111
  { name: "Audit the project", value: "audit" },
@@ -5082,6 +6118,15 @@ async function mainMenu() {
5082
6118
  await initProject2({});
5083
6119
  return;
5084
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
+ }
5085
6130
  case "from-design": {
5086
6131
  const { blockFromDesign: blockFromDesign2 } = await Promise.resolve().then(() => (init_from_design(), from_design_exports));
5087
6132
  await blockFromDesign2(void 0, {});
@@ -5130,6 +6175,11 @@ async function mainMenu() {
5130
6175
  await previewBlock2(name, {});
5131
6176
  return;
5132
6177
  }
6178
+ case "model": {
6179
+ const { modelAdd: modelAdd2 } = await Promise.resolve().then(() => (init_model(), model_exports));
6180
+ await modelAdd2(void 0, [], {});
6181
+ return;
6182
+ }
5133
6183
  case "track": {
5134
6184
  const projectRoot = findProjectRoot();
5135
6185
  if (!projectRoot) {
@@ -5149,6 +6199,25 @@ async function mainMenu() {
5149
6199
  await trackBlock2(name, {});
5150
6200
  return;
5151
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
+ }
5152
6221
  case "admin": {
5153
6222
  const { adminWizard: adminWizard2 } = await Promise.resolve().then(() => (init_admin(), admin_exports));
5154
6223
  await adminWizard2();
@@ -5194,13 +6263,13 @@ var init_menu = __esm({
5194
6263
 
5195
6264
  // src/bin/eds.ts
5196
6265
  init_esm_shims();
5197
- import chalk16 from "chalk";
6266
+ import chalk18 from "chalk";
5198
6267
  import { Command } from "commander";
5199
6268
 
5200
6269
  // package.json
5201
6270
  var package_default = {
5202
6271
  name: "aem-eds-kit",
5203
- version: "0.4.12",
6272
+ version: "0.4.16",
5204
6273
  description: "CLI for AEM Edge Delivery Services with Figma MCP integration",
5205
6274
  type: "module",
5206
6275
  engines: {
@@ -5276,9 +6345,18 @@ program.name("eds").description("CLI for AEM Edge Delivery Services with Figma i
5276
6345
  const opts = thisCommand.opts();
5277
6346
  if (opts.verbose) logger.setLevel("debug");
5278
6347
  if (opts.quiet) logger.setQuiet(true);
5279
- if (opts.color === false) chalk16.level = 0;
6348
+ if (opts.color === false) chalk18.level = 0;
5280
6349
  });
5281
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
+ });
5282
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) => {
5283
6361
  const { initProject: initProject2 } = await Promise.resolve().then(() => (init_init(), init_exports));
5284
6362
  await initProject2(options);
@@ -5288,6 +6366,16 @@ migrate.command("component [dir]").description("Migrate a classic AEM component'
5288
6366
  const { migrateComponent: migrateComponent2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
5289
6367
  await migrateComponent2(dir, options);
5290
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
+ });
5291
6379
  var track = program.command("track").description("Instrument blocks for analytics");
5292
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) => {
5293
6381
  const { trackBlock: trackBlock2 } = await Promise.resolve().then(() => (init_track2(), track_exports));
@@ -5328,6 +6416,10 @@ scaffold.command("ci").description("Scaffold a GitHub Actions workflow (doctor +
5328
6416
  const { scaffoldCi: scaffoldCi2 } = await Promise.resolve().then(() => (init_scaffold(), scaffold_exports));
5329
6417
  await scaffoldCi2();
5330
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
+ });
5331
6423
  program.command("integrate [type]").description("Add a third-party service (GTM, GA4, chat, consent) to scripts/delayed.js").action(async (type) => {
5332
6424
  const { integrate: integrate2 } = await Promise.resolve().then(() => (init_integrate(), integrate_exports));
5333
6425
  await integrate2(type);