aem-eds-kit 0.4.12 → 0.4.22

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({
@@ -273,6 +273,178 @@ ${logoArt()}
273
273
  }
274
274
  });
275
275
 
276
+ // src/lib/sandbox/api.ts
277
+ function headers(token) {
278
+ return {
279
+ Authorization: `Bearer ${token}`,
280
+ Accept: "application/vnd.github+json",
281
+ "X-GitHub-Api-Version": "2022-11-28",
282
+ "User-Agent": "aem-eds-kit",
283
+ "Content-Type": "application/json"
284
+ };
285
+ }
286
+ function generateEndpoint(template2) {
287
+ return `${API}/repos/${template2}/generate`;
288
+ }
289
+ async function getUser(token) {
290
+ const res = await fetch(`${API}/user`, { headers: headers(token) });
291
+ if (!res.ok) throw new Error(`GitHub auth failed: ${res.status}`);
292
+ return (await res.json()).login;
293
+ }
294
+ async function repoExists(token, owner, repo) {
295
+ const res = await fetch(`${API}/repos/${owner}/${repo}`, { headers: headers(token) });
296
+ if (res.status === 404) return false;
297
+ if (res.ok) return true;
298
+ throw new Error(`existence check failed: ${res.status} ${await res.text()}`);
299
+ }
300
+ async function createFromTemplate(token, template2, opts) {
301
+ const res = await fetch(generateEndpoint(template2), {
302
+ method: "POST",
303
+ headers: headers(token),
304
+ body: JSON.stringify({
305
+ owner: opts.owner,
306
+ name: opts.name,
307
+ private: !!opts.private,
308
+ include_all_branches: false
309
+ })
310
+ });
311
+ if (!res.ok) throw new Error(`create failed: ${res.status} ${await res.text()}`);
312
+ const r = await res.json();
313
+ return { fullName: r.full_name, htmlUrl: r.html_url, cloneUrl: r.clone_url };
314
+ }
315
+ async function addTopics(token, owner, repo, topics) {
316
+ await fetch(`${API}/repos/${owner}/${repo}/topics`, {
317
+ method: "PUT",
318
+ headers: headers(token),
319
+ body: JSON.stringify({ names: topics })
320
+ });
321
+ }
322
+ async function deleteRepo(token, owner, repo) {
323
+ const res = await fetch(`${API}/repos/${owner}/${repo}`, {
324
+ method: "DELETE",
325
+ headers: headers(token)
326
+ });
327
+ if (!res.ok && res.status !== 204) {
328
+ throw new Error(`delete failed: ${res.status} ${await res.text()}`);
329
+ }
330
+ }
331
+ async function searchByTopic(token, login, topic) {
332
+ const q = encodeURIComponent(`user:${login} topic:${topic} fork:true`);
333
+ const res = await fetch(`${API}/search/repositories?q=${q}&per_page=100`, {
334
+ headers: headers(token)
335
+ });
336
+ if (!res.ok) throw new Error(`search failed: ${res.status}`);
337
+ const data = await res.json();
338
+ return data.items.map((i) => i.full_name);
339
+ }
340
+ var API;
341
+ var init_api = __esm({
342
+ "src/lib/sandbox/api.ts"() {
343
+ "use strict";
344
+ init_esm_shims();
345
+ API = "https://api.github.com";
346
+ }
347
+ });
348
+
349
+ // src/lib/sandbox/auth.ts
350
+ import { existsSync } from "fs";
351
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
352
+ import os from "os";
353
+ import path2 from "path";
354
+ async function getToken() {
355
+ const env = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
356
+ if (env) return env;
357
+ if (existsSync(TOKEN_FILE)) {
358
+ try {
359
+ return JSON.parse(await readFile(TOKEN_FILE, "utf-8")).token ?? null;
360
+ } catch {
361
+ return null;
362
+ }
363
+ }
364
+ return null;
365
+ }
366
+ async function saveToken(token) {
367
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
368
+ await writeFile(TOKEN_FILE, `${JSON.stringify({ token }, null, 2)}
369
+ `, { mode: 384 });
370
+ }
371
+ async function clearToken() {
372
+ if (existsSync(TOKEN_FILE)) await rm(TOKEN_FILE);
373
+ }
374
+ async function startDeviceFlow(scopes) {
375
+ const res = await fetch("https://github.com/login/device/code", {
376
+ method: "POST",
377
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
378
+ body: JSON.stringify({ client_id: CLIENT_ID, scope: scopes.join(" ") })
379
+ });
380
+ if (!res.ok) throw new Error(`device code request failed: ${res.status}`);
381
+ return await res.json();
382
+ }
383
+ async function pollDeviceToken(deviceCode, interval, expiresIn) {
384
+ let wait = Math.max(interval, 1);
385
+ const deadline = Date.now() + expiresIn * 1e3;
386
+ while (Date.now() < deadline) {
387
+ await new Promise((r) => setTimeout(r, wait * 1e3));
388
+ const res = await fetch("https://github.com/login/oauth/access_token", {
389
+ method: "POST",
390
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
391
+ body: JSON.stringify({
392
+ client_id: CLIENT_ID,
393
+ device_code: deviceCode,
394
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
395
+ })
396
+ });
397
+ const data = await res.json();
398
+ if (data.access_token) return data.access_token;
399
+ if (data.error === "slow_down") wait += 5;
400
+ else if (data.error !== "authorization_pending") {
401
+ throw new Error(data.error ?? "device flow failed");
402
+ }
403
+ }
404
+ throw new Error("device flow timed out");
405
+ }
406
+ var CONFIG_DIR, TOKEN_FILE, CLIENT_ID;
407
+ var init_auth = __esm({
408
+ "src/lib/sandbox/auth.ts"() {
409
+ "use strict";
410
+ init_esm_shims();
411
+ CONFIG_DIR = path2.join(os.homedir(), ".eds");
412
+ TOKEN_FILE = path2.join(CONFIG_DIR, "github.json");
413
+ CLIENT_ID = process.env.EDS_GITHUB_CLIENT_ID ?? "";
414
+ }
415
+ });
416
+
417
+ // src/lib/sandbox/github.ts
418
+ function previewUrl(owner, repo, ref = "main") {
419
+ return `https://${ref}--${repo}--${owner}.aem.page/`;
420
+ }
421
+ function codeSyncInstallUrl() {
422
+ return "https://github.com/apps/aem-code-sync/installations/new";
423
+ }
424
+ function createArgs(repoRef, opts) {
425
+ return [
426
+ "repo",
427
+ "create",
428
+ repoRef,
429
+ "--template",
430
+ opts.template,
431
+ opts.private ? "--private" : "--public",
432
+ ...opts.clone ? ["--clone"] : []
433
+ ];
434
+ }
435
+ var SANDBOX_TOPIC, BOILERPLATES;
436
+ var init_github = __esm({
437
+ "src/lib/sandbox/github.ts"() {
438
+ "use strict";
439
+ init_esm_shims();
440
+ SANDBOX_TOPIC = "eds-sandbox";
441
+ BOILERPLATES = {
442
+ document: "adobe/aem-boilerplate",
443
+ ue: "adobe/aem-boilerplate-xwalk"
444
+ };
445
+ }
446
+ });
447
+
276
448
  // src/lib/schemas.ts
277
449
  import { z } from "zod";
278
450
  var figmaUrlSchema, edsConfigSchema, edsProjectSchema, edsMetaSchema, kebabCaseRegex;
@@ -315,26 +487,26 @@ var init_schemas = __esm({
315
487
  });
316
488
 
317
489
  // src/lib/project.ts
318
- import { existsSync } from "fs";
319
- import path2 from "path";
490
+ import { existsSync as existsSync2 } from "fs";
491
+ import path3 from "path";
320
492
  function findProjectRoot(startDir = process.cwd()) {
321
- let dir = path2.resolve(startDir);
322
- const { root } = path2.parse(dir);
493
+ let dir = path3.resolve(startDir);
494
+ const { root } = path3.parse(dir);
323
495
  while (dir !== root) {
324
- if (ROOT_MARKERS.some((m) => existsSync(path2.join(dir, m)))) {
496
+ if (ROOT_MARKERS.some((m) => existsSync2(path3.join(dir, m)))) {
325
497
  return dir;
326
498
  }
327
- dir = path2.dirname(dir);
499
+ dir = path3.dirname(dir);
328
500
  }
329
- if (ROOT_MARKERS.some((m) => existsSync(path2.join(root, m)))) return root;
501
+ if (ROOT_MARKERS.some((m) => existsSync2(path3.join(root, m)))) return root;
330
502
  return void 0;
331
503
  }
332
504
  function detectProject(rootDir) {
333
505
  return edsProjectSchema.parse({
334
506
  root: rootDir,
335
- hasFstab: existsSync(path2.join(rootDir, "fstab.yaml")),
336
- hasBlocks: existsSync(path2.join(rootDir, "blocks")),
337
- hasHeadHtml: existsSync(path2.join(rootDir, "head.html"))
507
+ hasFstab: existsSync2(path3.join(rootDir, "fstab.yaml")),
508
+ hasBlocks: existsSync2(path3.join(rootDir, "blocks")),
509
+ hasHeadHtml: existsSync2(path3.join(rootDir, "head.html"))
338
510
  });
339
511
  }
340
512
  var ROOT_MARKERS;
@@ -356,14 +528,14 @@ var init_project = __esm({
356
528
  });
357
529
 
358
530
  // src/lib/scaffold/ci.ts
359
- import { existsSync as existsSync2 } from "fs";
360
- import { mkdir, writeFile } from "fs/promises";
361
- import path3 from "path";
531
+ import { existsSync as existsSync3 } from "fs";
532
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
533
+ import path4 from "path";
362
534
  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);
535
+ const file = path4.join(projectRoot, ".github", "workflows", "eds.yml");
536
+ if (existsSync3(file)) return { path: CI_WORKFLOW_PATH, created: false };
537
+ await mkdir2(path4.dirname(file), { recursive: true });
538
+ await writeFile2(file, CI_WORKFLOW);
367
539
  return { path: CI_WORKFLOW_PATH, created: true };
368
540
  }
369
541
  var CI_WORKFLOW_PATH, CI_WORKFLOW;
@@ -419,9 +591,46 @@ jobs:
419
591
  }
420
592
  });
421
593
 
594
+ // src/lib/scaffold/env.ts
595
+ import { existsSync as existsSync4 } from "fs";
596
+ import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
597
+ import path5 from "path";
598
+ function buildEnv(opts = {}) {
599
+ const open = opts.open ?? "/";
600
+ const port = opts.port ?? 3007;
601
+ const pagesUrl = opts.pagesUrl ?? "";
602
+ return `AEM_OPEN=${open}
603
+ AEM_PORT=${port}
604
+ AEM_PAGES_URL=${pagesUrl}
605
+ `;
606
+ }
607
+ async function ensureEnvIgnored(projectRoot) {
608
+ const file = path5.join(projectRoot, ".gitignore");
609
+ let current = "";
610
+ if (existsSync4(file)) current = await readFile2(file, "utf-8");
611
+ if (current.split(/\r?\n/).some((l) => l.trim() === ".env")) return false;
612
+ const prefix = current && !current.endsWith("\n") ? "\n" : "";
613
+ await writeFile3(file, `${current}${prefix}.env
614
+ `);
615
+ return true;
616
+ }
617
+ async function writeEnv(projectRoot, opts = {}) {
618
+ const file = path5.join(projectRoot, ".env");
619
+ const created = !existsSync4(file);
620
+ if (created) await writeFile3(file, buildEnv(opts));
621
+ const gitignoreUpdated = await ensureEnvIgnored(projectRoot);
622
+ return { path: ".env", created, gitignoreUpdated };
623
+ }
624
+ var init_env = __esm({
625
+ "src/lib/scaffold/env.ts"() {
626
+ "use strict";
627
+ init_esm_shims();
628
+ }
629
+ });
630
+
422
631
  // src/lib/scaffold/fstab.ts
423
- import { writeFile as writeFile2 } from "fs/promises";
424
- import path4 from "path";
632
+ import { writeFile as writeFile4 } from "fs/promises";
633
+ import path6 from "path";
425
634
  function needsFstab(a) {
426
635
  return a !== "ue";
427
636
  }
@@ -434,7 +643,7 @@ function fstabYaml(mountpoint) {
434
643
  `;
435
644
  }
436
645
  async function writeFstab(projectRoot, mountpoint) {
437
- await writeFile2(path4.join(projectRoot, "fstab.yaml"), fstabYaml(mountpoint));
646
+ await writeFile4(path6.join(projectRoot, "fstab.yaml"), fstabYaml(mountpoint));
438
647
  return "fstab.yaml";
439
648
  }
440
649
  function pathsJson(site) {
@@ -449,7 +658,7 @@ function pathsJson(site) {
449
658
  `;
450
659
  }
451
660
  async function writePaths(projectRoot, site) {
452
- await writeFile2(path4.join(projectRoot, "paths.json"), pathsJson(site));
661
+ await writeFile4(path6.join(projectRoot, "paths.json"), pathsJson(site));
453
662
  return "paths.json";
454
663
  }
455
664
  var AUTHORING_NAMES;
@@ -466,6 +675,58 @@ var init_fstab = __esm({
466
675
  }
467
676
  });
468
677
 
678
+ // src/lib/scaffold/xwalk.ts
679
+ import { existsSync as existsSync5 } from "fs";
680
+ import { writeFile as writeFile5 } from "fs/promises";
681
+ import path7 from "path";
682
+ function buildXwalk() {
683
+ return `${JSON.stringify(XWALK_CONFIG, null, 2)}
684
+ `;
685
+ }
686
+ async function writeXwalk(projectRoot) {
687
+ const file = path7.join(projectRoot, "xwalk.json");
688
+ if (existsSync5(file)) return { path: "xwalk.json", created: false };
689
+ await writeFile5(file, buildXwalk());
690
+ return { path: "xwalk.json", created: true };
691
+ }
692
+ var XWALK_CONFIG;
693
+ var init_xwalk = __esm({
694
+ "src/lib/scaffold/xwalk.ts"() {
695
+ "use strict";
696
+ init_esm_shims();
697
+ XWALK_CONFIG = {
698
+ public: {
699
+ xwalk: {
700
+ "multi-field": {
701
+ enabled: true
702
+ }
703
+ }
704
+ }
705
+ };
706
+ }
707
+ });
708
+
709
+ // src/lib/ue/link.ts
710
+ function parseRemote(url) {
711
+ const m = url.trim().match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/i);
712
+ return m ? { owner: m[1], repo: m[2] } : null;
713
+ }
714
+ function previewHost(owner, repo, ref = "main") {
715
+ return `${ref}--${repo}--${owner}.aem.page`;
716
+ }
717
+ function editorLink({ host, path: path36 = "/", org }) {
718
+ const cleanHost = host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
719
+ const p = path36.startsWith("/") ? path36 : `/${path36}`;
720
+ const orgSeg = org ? `@${org}/` : "";
721
+ return `https://experience.adobe.com/#/${orgSeg}aem/editor/canvas/${cleanHost}${p}`;
722
+ }
723
+ var init_link = __esm({
724
+ "src/lib/ue/link.ts"() {
725
+ "use strict";
726
+ init_esm_shims();
727
+ }
728
+ });
729
+
469
730
  // src/lib/scaffold/blocks.ts
470
731
  function json(value) {
471
732
  return `${JSON.stringify(value, null, 2)}
@@ -479,14 +740,14 @@ var init_blocks = __esm({
479
740
  "src/lib/scaffold/blocks.ts"() {
480
741
  "use strict";
481
742
  init_esm_shims();
482
- blockDef = (title, id, model) => ({
743
+ blockDef = (title, id, model2) => ({
483
744
  title,
484
745
  id,
485
746
  plugins: {
486
747
  xwalk: {
487
748
  page: {
488
749
  resourceType: "core/franklin/components/block/v1/block",
489
- template: { name: title, model }
750
+ template: { name: title, model: model2 }
490
751
  }
491
752
  }
492
753
  }
@@ -503,14 +764,14 @@ var init_blocks = __esm({
503
764
  }
504
765
  }
505
766
  });
506
- itemDef = (title, id, model) => ({
767
+ itemDef = (title, id, model2) => ({
507
768
  title,
508
769
  id,
509
770
  plugins: {
510
771
  xwalk: {
511
772
  page: {
512
773
  resourceType: "core/franklin/components/block/v1/block/item",
513
- template: { name: title, model }
774
+ template: { name: title, model: model2 }
514
775
  }
515
776
  }
516
777
  }
@@ -787,6 +1048,204 @@ export default function decorate(block) {
787
1048
  }
788
1049
  });
789
1050
 
1051
+ // src/lib/scaffold/helpers.ts
1052
+ import { existsSync as existsSync6 } from "fs";
1053
+ import { mkdir as mkdir3, writeFile as writeFile6 } from "fs/promises";
1054
+ import path8 from "path";
1055
+ function helperById(id) {
1056
+ return HELPERS.find((h) => h.id === id);
1057
+ }
1058
+ function resolveHelpers(ids) {
1059
+ const want = /* @__PURE__ */ new Set();
1060
+ const add = (id) => {
1061
+ if (want.has(id)) return;
1062
+ const h = helperById(id);
1063
+ if (!h) return;
1064
+ for (const dep of h.deps ?? []) add(dep);
1065
+ want.add(id);
1066
+ };
1067
+ for (const id of ids) add(id);
1068
+ return HELPERS.filter((h) => want.has(h.id)).map((h) => h.id);
1069
+ }
1070
+ function buildHelpers(ids) {
1071
+ const resolved = resolveHelpers(ids);
1072
+ const body = resolved.map((id) => helperById(id)).filter((h) => !!h).map((h) => h.code).join("\n\n");
1073
+ return `// scripts/utils.js - common EDS helpers (generated by \`eds scaffold helpers\`).
1074
+
1075
+ ${body}
1076
+ `;
1077
+ }
1078
+ async function writeHelpers(projectRoot, ids = ALL_HELPER_IDS) {
1079
+ const resolved = resolveHelpers(ids);
1080
+ const file = path8.join(projectRoot, "scripts", "utils.js");
1081
+ if (existsSync6(file)) return { path: HELPERS_PATH, created: false, helpers: resolved };
1082
+ await mkdir3(path8.dirname(file), { recursive: true });
1083
+ await writeFile6(file, buildHelpers(ids));
1084
+ return { path: HELPERS_PATH, created: true, helpers: resolved };
1085
+ }
1086
+ var HELPERS_PATH, HELPERS, ALL_HELPER_IDS;
1087
+ var init_helpers = __esm({
1088
+ "src/lib/scaffold/helpers.ts"() {
1089
+ "use strict";
1090
+ init_esm_shims();
1091
+ HELPERS_PATH = "scripts/utils.js";
1092
+ HELPERS = [
1093
+ {
1094
+ id: "getMetadata",
1095
+ label: "getMetadata (read a page meta value)",
1096
+ code: `/**
1097
+ * Reads a page metadata value by name (meta[name] or meta[property]).
1098
+ * @param {string} name The metadata name.
1099
+ * @returns {string} The value, or an empty string.
1100
+ */
1101
+ export function getMetadata(name) {
1102
+ const meta =
1103
+ document.querySelector(\`meta[name="\${name}"]\`) ||
1104
+ document.querySelector(\`meta[property="\${name}"]\`);
1105
+ return meta ? meta.content : '';
1106
+ }`
1107
+ },
1108
+ {
1109
+ id: "toCamelCase",
1110
+ label: "toCamelCase (dash/underscore/space -> camelCase)",
1111
+ code: `/**
1112
+ * Converts a string to camelCase, handling dashes, underscores and spaces.
1113
+ * @param {string} value The string to convert.
1114
+ * @returns {string} The camelCased string.
1115
+ */
1116
+ export function toCamelCase(value) {
1117
+ return String(value || '')
1118
+ .trim()
1119
+ .toLowerCase()
1120
+ .replace(/[-_\\s]+(.)/g, (_, char) => char.toUpperCase());
1121
+ }`
1122
+ },
1123
+ {
1124
+ id: "getLanguageRootPath",
1125
+ label: "getLanguageRootPath (e.g. /en, /en-us)",
1126
+ code: `/**
1127
+ * Returns the language root of a path, e.g. '/en' or '/en-us' ('' if none).
1128
+ * @param {string} path The page path.
1129
+ * @returns {string} The language root prefix.
1130
+ */
1131
+ export function getLanguageRootPath(path) {
1132
+ const match = String(path || '').match(/^\\/[a-z]{2}(?:-[a-z]{2})?(?=\\/|$)/i);
1133
+ return match ? match[0] : '';
1134
+ }`
1135
+ },
1136
+ {
1137
+ id: "getPagePath",
1138
+ label: "getPagePath (path without content root)",
1139
+ deps: ["getLanguageRootPath"],
1140
+ code: `// Content root stripped from page paths. Set this if your site is mounted under
1141
+ // a sub-path (e.g. '/content/my-site'); leave empty for a root-mounted site.
1142
+ const ROOT_PATH = '';
1143
+
1144
+ /**
1145
+ * Returns the page path without the content root; \`clean\` drops the language root.
1146
+ * @param {boolean} [clean=false] Also strip the language root.
1147
+ * @returns {string} The page path or 'NA' for the language root.
1148
+ */
1149
+ export function getPagePath({ clean = false } = {}) {
1150
+ const path = window.location.pathname.replace(ROOT_PATH, '').replace(/\\.html$/i, '');
1151
+
1152
+ if (clean) {
1153
+ const langRoot = getLanguageRootPath(path);
1154
+ const cleaned = path.startsWith(langRoot) ? path.slice(langRoot.length) : path;
1155
+ return cleaned || 'NA';
1156
+ }
1157
+
1158
+ return path || 'NA';
1159
+ }`
1160
+ },
1161
+ {
1162
+ id: "getSiteArea",
1163
+ label: "getSiteArea (blog / products / home)",
1164
+ deps: ["getPagePath"],
1165
+ code: `/**
1166
+ * Returns the top-level area of the site from the current path - e.g. 'blog' or
1167
+ * 'products', and 'home' at the site root. Uses the language-cleaned path, so you
1168
+ * can tell whether you're on the blog or somewhere else.
1169
+ * @returns {string} The site area.
1170
+ */
1171
+ export function getSiteArea() {
1172
+ const path = getPagePath({ clean: true });
1173
+ if (!path || path === 'NA') return 'home';
1174
+ return path.split('/').filter(Boolean)[0] || 'home';
1175
+ }`
1176
+ },
1177
+ {
1178
+ id: "getEnvironment",
1179
+ label: "getEnvironment (dev / stage / prod)",
1180
+ code: `/**
1181
+ * Detects the current environment from the hostname.
1182
+ * @returns {'NA'|'dev'|'stage'|'prod'} The environment name.
1183
+ */
1184
+ export function getEnvironment() {
1185
+ const { hostname } = window.location;
1186
+ if (hostname.includes('author')) return 'NA';
1187
+ if (hostname === 'localhost') return 'dev';
1188
+ const repo = hostname.match(/^[^.]+--([^.]+)--[^.]+\\.aem\\.(?:page|live)$/)?.[1] || '';
1189
+ if (repo.includes('dev')) return 'dev';
1190
+ if (repo.includes('stage')) return 'stage';
1191
+ return 'prod';
1192
+ }`
1193
+ },
1194
+ {
1195
+ id: "getContentTopic",
1196
+ label: "getContentTopic (cq-tags content-topic values)",
1197
+ deps: ["getMetadata"],
1198
+ code: `/**
1199
+ * Reads all content-topic values from the page's cq-tags metadata.
1200
+ * @returns {string[]} The topics (part after \`content-topic/\`), empty if none.
1201
+ */
1202
+ export function getContentTopic() {
1203
+ return getMetadata('cq-tags')
1204
+ .split(',')
1205
+ .map((t) => t.trim())
1206
+ .filter((t) => t.includes('content-topic/'))
1207
+ .map((t) => t.split('content-topic/')[1]);
1208
+ }`
1209
+ },
1210
+ {
1211
+ id: "isUEEdit",
1212
+ label: "isUEEdit (Universal Editor edit mode)",
1213
+ code: `/**
1214
+ * Checks whether the current page is opened in the Universal Editor Edit mode.
1215
+ * @returns {boolean} True in Universal Editor Edit mode.
1216
+ */
1217
+ export function isUEEdit() {
1218
+ return document.documentElement.classList.contains('adobe-ue-edit');
1219
+ }`
1220
+ },
1221
+ {
1222
+ id: "isUEPreview",
1223
+ label: "isUEPreview (Universal Editor preview mode)",
1224
+ code: `/**
1225
+ * Checks whether the current page is opened in the Universal Editor Preview mode.
1226
+ * @returns {boolean} True in Universal Editor Preview mode.
1227
+ */
1228
+ export function isUEPreview() {
1229
+ return document.documentElement.classList.contains('adobe-ue-preview');
1230
+ }`
1231
+ },
1232
+ {
1233
+ id: "isUE",
1234
+ label: "isUE (Universal Editor, any mode)",
1235
+ deps: ["isUEEdit", "isUEPreview"],
1236
+ code: `/**
1237
+ * Checks whether the current page is opened in the Universal Editor (any mode).
1238
+ * @returns {boolean} True in the Universal Editor.
1239
+ */
1240
+ export function isUE() {
1241
+ return isUEEdit() || isUEPreview();
1242
+ }`
1243
+ }
1244
+ ];
1245
+ ALL_HELPER_IDS = HELPERS.map((h) => h.id);
1246
+ }
1247
+ });
1248
+
790
1249
  // src/lib/scaffold/ue.ts
791
1250
  function fieldReferenceFields() {
792
1251
  return [
@@ -872,7 +1331,7 @@ function fieldReferenceFields() {
872
1331
  ];
873
1332
  }
874
1333
  function fieldReferenceModelFile() {
875
- const model = {
1334
+ const model2 = {
876
1335
  definitions: [
877
1336
  {
878
1337
  title: "Field Reference",
@@ -890,7 +1349,7 @@ function fieldReferenceModelFile() {
890
1349
  models: [{ id: "field-reference", fields: fieldReferenceFields() }],
891
1350
  filters: []
892
1351
  };
893
- return `${JSON.stringify(model, null, 2)}
1352
+ return `${JSON.stringify(model2, null, 2)}
894
1353
  `;
895
1354
  }
896
1355
  function mergeById(existing, additions) {
@@ -913,7 +1372,7 @@ function mergeDefinitions(existing, defaultContent, blockDefs) {
913
1372
  }
914
1373
  function mergeSectionFilter(existing, ids) {
915
1374
  const filters = existing ? [...existing] : [];
916
- let section = filters.find((f) => f.id === "section");
1375
+ let section = filters.find((f2) => f2.id === "section");
917
1376
  if (!section) {
918
1377
  section = { id: "section", components: ["text", "image", "button", "title"] };
919
1378
  filters.push(section);
@@ -1049,22 +1508,25 @@ var scaffold_exports = {};
1049
1508
  __export(scaffold_exports, {
1050
1509
  scaffoldBlocks: () => scaffoldBlocks,
1051
1510
  scaffoldCi: () => scaffoldCi,
1511
+ scaffoldEnv: () => scaffoldEnv,
1512
+ scaffoldHelpers: () => scaffoldHelpers,
1052
1513
  scaffoldInteractive: () => scaffoldInteractive,
1053
- scaffoldUe: () => scaffoldUe
1514
+ scaffoldUe: () => scaffoldUe,
1515
+ scaffoldXwalk: () => scaffoldXwalk
1054
1516
  });
1055
- import { existsSync as existsSync3 } from "fs";
1056
- import { mkdir as mkdir2, readFile, writeFile as writeFile3 } from "fs/promises";
1057
- import path5 from "path";
1517
+ import { existsSync as existsSync7 } from "fs";
1518
+ import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile7 } from "fs/promises";
1519
+ import path9 from "path";
1058
1520
  async function readJson(file) {
1059
- if (!existsSync3(file)) return null;
1521
+ if (!existsSync7(file)) return null;
1060
1522
  try {
1061
- return JSON.parse(await readFile(file, "utf-8"));
1523
+ return JSON.parse(await readFile3(file, "utf-8"));
1062
1524
  } catch {
1063
1525
  return null;
1064
1526
  }
1065
1527
  }
1066
1528
  async function writeJson(file, data) {
1067
- await writeFile3(file, `${JSON.stringify(data, null, 2)}
1529
+ await writeFile7(file, `${JSON.stringify(data, null, 2)}
1068
1530
  `);
1069
1531
  }
1070
1532
  async function scaffoldUe() {
@@ -1076,15 +1538,15 @@ async function scaffoldUe() {
1076
1538
  }
1077
1539
  logger.logoOnce(logo("Scaffold - Universal Editor"));
1078
1540
  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());
1541
+ const blockDir = path9.join(projectRoot, "blocks", "field-reference");
1542
+ await mkdir4(blockDir, { recursive: true });
1543
+ await writeFile7(path9.join(blockDir, "field-reference.js"), FIELD_REFERENCE_JS);
1544
+ await writeFile7(path9.join(blockDir, "field-reference.css"), FIELD_REFERENCE_CSS);
1545
+ await writeFile7(path9.join(blockDir, "_field-reference.json"), fieldReferenceModelFile());
1084
1546
  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");
1547
+ const defFile = path9.join(projectRoot, "component-definition.json");
1548
+ const modelsFile = path9.join(projectRoot, "component-models.json");
1549
+ const filtersFile = path9.join(projectRoot, "component-filters.json");
1088
1550
  const blockDefs = [
1089
1551
  {
1090
1552
  title: "Field Reference",
@@ -1134,8 +1596,8 @@ async function scaffoldBlocks(names) {
1134
1596
  logger.logoOnce(logo("Scaffold \u2014 standard blocks"));
1135
1597
  let selected;
1136
1598
  if (names?.length) {
1137
- selected = names.map((n) => blockById(n.trim())).filter((b) => !!b);
1138
- const unknown = names.filter((n) => !blockById(n.trim()));
1599
+ selected = names.map((n2) => blockById(n2.trim())).filter((b) => !!b);
1600
+ const unknown = names.filter((n2) => !blockById(n2.trim()));
1139
1601
  if (unknown.length) {
1140
1602
  logger.warn(
1141
1603
  `Unknown block(s): ${unknown.join(", ")}. Available: ${STANDARD_BLOCKS.map((b) => b.id).join(", ")}`
@@ -1147,18 +1609,18 @@ async function scaffoldBlocks(names) {
1147
1609
  const createdIds = [];
1148
1610
  const skipped = [];
1149
1611
  for (const b of selected) {
1150
- const dir = path5.join(projectRoot, "blocks", b.id);
1151
- if (existsSync3(dir)) {
1612
+ const dir = path9.join(projectRoot, "blocks", b.id);
1613
+ if (existsSync7(dir)) {
1152
1614
  skipped.push(b.id);
1153
1615
  continue;
1154
1616
  }
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);
1617
+ await mkdir4(dir, { recursive: true });
1618
+ await writeFile7(path9.join(dir, `${b.id}.js`), b.js);
1619
+ await writeFile7(path9.join(dir, `${b.id}.css`), b.css);
1620
+ await writeFile7(path9.join(dir, `_${b.id}.json`), b.model);
1159
1621
  createdIds.push(b.id);
1160
1622
  }
1161
- const filtersFile = path5.join(projectRoot, "component-filters.json");
1623
+ const filtersFile = path9.join(projectRoot, "component-filters.json");
1162
1624
  const filters = mergeSectionFilter(
1163
1625
  await readJson(filtersFile),
1164
1626
  selected.map((b) => b.id)
@@ -1193,6 +1655,96 @@ async function scaffoldCi() {
1193
1655
  ])
1194
1656
  );
1195
1657
  }
1658
+ async function scaffoldHelpers(ids) {
1659
+ const projectRoot = findProjectRoot();
1660
+ if (!projectRoot) {
1661
+ logger.error("Not inside an EDS project (no EDS project markers found).");
1662
+ process.exitCode = 1;
1663
+ return;
1664
+ }
1665
+ logger.logoOnce(logo("Scaffold - helpers"));
1666
+ let selected = ids?.length ? ids : void 0;
1667
+ if (!selected) {
1668
+ if (process.stdout.isTTY) {
1669
+ const { checkbox } = await import("@inquirer/prompts");
1670
+ selected = await checkbox({
1671
+ message: "Which helpers?",
1672
+ choices: HELPERS.map((h) => ({ name: h.label, value: h.id, checked: true }))
1673
+ });
1674
+ } else {
1675
+ selected = ALL_HELPER_IDS;
1676
+ }
1677
+ }
1678
+ if (!selected.length) {
1679
+ logger.info("Nothing selected.");
1680
+ return;
1681
+ }
1682
+ const res = await writeHelpers(projectRoot, selected);
1683
+ logger.info(heading("Helpers"));
1684
+ if (res.created) {
1685
+ logger.info(accentLine(res.path, res.helpers.join(", ")));
1686
+ } else {
1687
+ logger.info(statusLine("warn", res.path, "already exists - skipped"));
1688
+ }
1689
+ logger.info("");
1690
+ logger.info(
1691
+ box([
1692
+ res.created ? `${res.helpers.length} helper(s) -> scripts/utils.js` : "Helpers left untouched"
1693
+ ])
1694
+ );
1695
+ }
1696
+ async function scaffoldEnv(options = {}) {
1697
+ const projectRoot = findProjectRoot();
1698
+ if (!projectRoot) {
1699
+ logger.error("Not inside an EDS project (no EDS project markers found).");
1700
+ process.exitCode = 1;
1701
+ return;
1702
+ }
1703
+ logger.logoOnce(logo("Scaffold - .env"));
1704
+ let pagesUrl = options.pagesUrl;
1705
+ if (pagesUrl === void 0) {
1706
+ try {
1707
+ const { execa } = await import("execa");
1708
+ const res2 = await execa("git", ["remote", "get-url", "origin"], { cwd: projectRoot });
1709
+ const parsed = parseRemote(res2.stdout.trim());
1710
+ pagesUrl = parsed ? `https://${previewHost(parsed.owner, parsed.repo)}` : "";
1711
+ } catch {
1712
+ pagesUrl = "";
1713
+ }
1714
+ }
1715
+ let open = options.open ?? "/";
1716
+ let port = options.port ?? "3007";
1717
+ if (!options.yes && process.stdout.isTTY) {
1718
+ const { input } = await import("@inquirer/prompts");
1719
+ open = await input({ message: "AEM_OPEN (path opened on start)", default: open });
1720
+ port = await input({ message: "AEM_PORT", default: String(port) });
1721
+ pagesUrl = await input({ message: "AEM_PAGES_URL (content origin)", default: pagesUrl });
1722
+ }
1723
+ const res = await writeEnv(projectRoot, { open, port, pagesUrl });
1724
+ logger.info(heading(".env"));
1725
+ if (res.created) logger.info(accentLine(res.path, `AEM_OPEN=${open} AEM_PORT=${port}`));
1726
+ else logger.info(statusLine("warn", res.path, "already exists - left untouched"));
1727
+ if (res.gitignoreUpdated) logger.info(accentLine(".gitignore", "added .env"));
1728
+ logger.info("");
1729
+ logger.info(box([res.created ? ".env ready for `aem up`" : ".env left untouched"]));
1730
+ }
1731
+ async function scaffoldXwalk() {
1732
+ const projectRoot = findProjectRoot();
1733
+ if (!projectRoot) {
1734
+ logger.error("Not inside an EDS project (no EDS project markers found).");
1735
+ process.exitCode = 1;
1736
+ return;
1737
+ }
1738
+ logger.logoOnce(logo("Scaffold - xwalk.json"));
1739
+ const res = await writeXwalk(projectRoot);
1740
+ logger.info(heading("xwalk.json"));
1741
+ if (res.created) logger.info(accentLine(res.path, "multi-field enabled"));
1742
+ else logger.info(statusLine("warn", res.path, "already exists - left untouched"));
1743
+ logger.info("");
1744
+ logger.info(
1745
+ box([res.created ? "xwalk.json ready (UE multi-field)" : "xwalk.json left untouched"])
1746
+ );
1747
+ }
1196
1748
  async function scaffoldInteractive() {
1197
1749
  const { select, checkbox } = await import("@inquirer/prompts");
1198
1750
  logger.logoOnce(logo("Scaffold"));
@@ -1202,9 +1754,14 @@ async function scaffoldInteractive() {
1202
1754
  { name: "Universal Editor config + field-reference (all 17 field types)", value: "ue" },
1203
1755
  { name: "Standard blocks (hero, cards, columns, accordion, embed)", value: "blocks" },
1204
1756
  { name: "GitHub Actions CI (doctor + audits + lint/build)", value: "ci" },
1757
+ { name: "Helper functions (scripts/utils.js)", value: "helpers" },
1758
+ { name: "Local-dev .env (aem up: port / open / pages URL)", value: "env" },
1759
+ { name: "xwalk.json (enable Universal Editor multi-field)", value: "xwalk" },
1205
1760
  { name: "UE config + standard blocks", value: "both" }
1206
1761
  ]
1207
1762
  });
1763
+ if (what === "env") return scaffoldEnv();
1764
+ if (what === "xwalk") return scaffoldXwalk();
1208
1765
  if (what === "ue" || what === "both") await scaffoldUe();
1209
1766
  if (what === "blocks" || what === "both") {
1210
1767
  const picked = await checkbox({
@@ -1214,6 +1771,7 @@ async function scaffoldInteractive() {
1214
1771
  await scaffoldBlocks(picked);
1215
1772
  }
1216
1773
  if (what === "ci") await scaffoldCi();
1774
+ if (what === "helpers") await scaffoldHelpers();
1217
1775
  }
1218
1776
  var init_scaffold = __esm({
1219
1777
  "src/commands/scaffold.ts"() {
@@ -1223,7 +1781,11 @@ var init_scaffold = __esm({
1223
1781
  init_project();
1224
1782
  init_blocks();
1225
1783
  init_ci();
1784
+ init_env();
1785
+ init_helpers();
1226
1786
  init_ue();
1787
+ init_xwalk();
1788
+ init_link();
1227
1789
  init_ui();
1228
1790
  }
1229
1791
  });
@@ -1233,17 +1795,27 @@ var init_exports = {};
1233
1795
  __export(init_exports, {
1234
1796
  initProject: () => initProject
1235
1797
  });
1236
- import { existsSync as existsSync4 } from "fs";
1237
- import { readFile as readFile2, writeFile as writeFile4 } from "fs/promises";
1238
- import path6 from "path";
1798
+ import { existsSync as existsSync8 } from "fs";
1799
+ import { readFile as readFile4, writeFile as writeFile8 } from "fs/promises";
1800
+ import path10 from "path";
1239
1801
  import chalk3 from "chalk";
1802
+ async function derivePagesUrl(root) {
1803
+ try {
1804
+ const { execa } = await import("execa");
1805
+ const res = await execa("git", ["remote", "get-url", "origin"], { cwd: root });
1806
+ const parsed = parseRemote(res.stdout.trim());
1807
+ return parsed ? `https://${previewHost(parsed.owner, parsed.repo)}` : "";
1808
+ } catch {
1809
+ return "";
1810
+ }
1811
+ }
1240
1812
  async function setPackageName(root, name) {
1241
- const file = path6.join(root, "package.json");
1242
- if (!existsSync4(file)) return false;
1813
+ const file = path10.join(root, "package.json");
1814
+ if (!existsSync8(file)) return false;
1243
1815
  try {
1244
- const pkg = JSON.parse(await readFile2(file, "utf-8"));
1816
+ const pkg = JSON.parse(await readFile4(file, "utf-8"));
1245
1817
  pkg.name = name;
1246
- await writeFile4(file, `${JSON.stringify(pkg, null, 2)}
1818
+ await writeFile8(file, `${JSON.stringify(pkg, null, 2)}
1247
1819
  `);
1248
1820
  return true;
1249
1821
  } catch {
@@ -1253,13 +1825,13 @@ async function setPackageName(root, name) {
1253
1825
  async function replacePlaceholders(root, name) {
1254
1826
  const touched = [];
1255
1827
  for (const rel of ["README.md", "index.html", "head.html"]) {
1256
- const file = path6.join(root, rel);
1257
- if (!existsSync4(file)) continue;
1828
+ const file = path10.join(root, rel);
1829
+ if (!existsSync8(file)) continue;
1258
1830
  try {
1259
- const before = await readFile2(file, "utf-8");
1831
+ const before = await readFile4(file, "utf-8");
1260
1832
  const after = before.replace(/aem-boilerplate/gi, name);
1261
1833
  if (after !== before) {
1262
- await writeFile4(file, after);
1834
+ await writeFile8(file, after);
1263
1835
  touched.push(rel);
1264
1836
  }
1265
1837
  } catch {
@@ -1332,11 +1904,11 @@ async function initProject(options = {}) {
1332
1904
  await logger.logoOnceAnimated("Project setup");
1333
1905
  const { input, select, confirm } = await import("@inquirer/prompts");
1334
1906
  const interactive = !options.yes;
1335
- let defaultName = path6.basename(projectRoot);
1336
- const pkgFile = path6.join(projectRoot, "package.json");
1337
- if (existsSync4(pkgFile)) {
1907
+ let defaultName = path10.basename(projectRoot);
1908
+ const pkgFile = path10.join(projectRoot, "package.json");
1909
+ if (existsSync8(pkgFile)) {
1338
1910
  try {
1339
- defaultName = JSON.parse(await readFile2(pkgFile, "utf-8")).name || defaultName;
1911
+ defaultName = JSON.parse(await readFile4(pkgFile, "utf-8")).name || defaultName;
1340
1912
  } catch {
1341
1913
  }
1342
1914
  }
@@ -1399,11 +1971,19 @@ async function initProject(options = {}) {
1399
1971
  const ci = await writeCiWorkflow(projectRoot);
1400
1972
  done.push(ci.created ? ci.path : `${ci.path} (exists, skipped)`);
1401
1973
  }
1974
+ if (options.env !== false) {
1975
+ const res = await writeEnv(projectRoot, { pagesUrl: await derivePagesUrl(projectRoot) });
1976
+ done.push(res.created ? ".env (aem up config)" : ".env (exists, skipped)");
1977
+ }
1978
+ if (options.xwalk !== false && authoring === "ue") {
1979
+ const res = await writeXwalk(projectRoot);
1980
+ done.push(res.created ? `${res.path} (UE multi-field)` : `${res.path} (exists, skipped)`);
1981
+ }
1402
1982
  logger.info(heading("Configured"));
1403
1983
  const dw = columnWidth(done.map((d) => d.split(" ")[0]));
1404
1984
  for (const d of done) {
1405
- const [n, ...rest] = d.split(" ");
1406
- logger.info(accentLine(n, rest.join(" ").replace(/^\((.*)\)$/, "$1"), dw));
1985
+ const [n2, ...rest] = d.split(" ");
1986
+ logger.info(accentLine(n2, rest.join(" ").replace(/^\((.*)\)$/, "$1"), dw));
1407
1987
  }
1408
1988
  logger.info("");
1409
1989
  if (interactive) await externalSteps(authoring);
@@ -1417,321 +1997,1505 @@ var init_init = __esm({
1417
1997
  init_logger();
1418
1998
  init_project();
1419
1999
  init_ci();
2000
+ init_env();
1420
2001
  init_fstab();
2002
+ init_xwalk();
2003
+ init_link();
1421
2004
  init_ui();
1422
2005
  init_scaffold();
1423
2006
  slug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1424
2007
  }
1425
2008
  });
1426
2009
 
1427
- // src/lib/migrate/clientlib.ts
1428
- import { existsSync as existsSync5 } from "fs";
1429
- import { readFile as readFile3, readdir } from "fs/promises";
1430
- import path7 from "path";
1431
- async function listFiles(dir) {
1432
- const out = [];
1433
- const walk3 = async (d) => {
1434
- for (const e of await readdir(d, { withFileTypes: true })) {
1435
- const p = path7.join(d, e.name);
1436
- if (e.isDirectory()) await walk3(p);
1437
- else out.push(p);
1438
- }
1439
- };
1440
- if (existsSync5(dir)) await walk3(dir);
1441
- return out;
2010
+ // src/commands/sandbox.ts
2011
+ var sandbox_exports = {};
2012
+ __export(sandbox_exports, {
2013
+ sandboxList: () => sandboxList,
2014
+ sandboxLogin: () => sandboxLogin,
2015
+ sandboxLogout: () => sandboxLogout,
2016
+ sandboxNew: () => sandboxNew,
2017
+ sandboxRm: () => sandboxRm
2018
+ });
2019
+ import path11 from "path";
2020
+ import chalk4 from "chalk";
2021
+ async function run(cmd, args, opts = {}) {
2022
+ const { execa } = await import("execa");
2023
+ const res = await execa(cmd, args, opts);
2024
+ return (res.stdout ?? "").toString().trim();
1442
2025
  }
1443
- async function findClientlibDir(componentDir) {
1444
- if (!existsSync5(componentDir)) return null;
1445
- const entries = await readdir(componentDir, { withFileTypes: true });
1446
- const match = entries.find((e) => e.isDirectory() && /clientlib/i.test(e.name));
1447
- if (match) return path7.join(componentDir, match.name);
1448
- const hasCss = entries.some((e) => e.isFile() && e.name.endsWith(".css"));
1449
- return hasCss ? componentDir : null;
2026
+ async function openUrl(url) {
2027
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2028
+ try {
2029
+ await run(cmd, [url], { stdio: "ignore" });
2030
+ } catch {
2031
+ }
1450
2032
  }
1451
- async function readClientlibCss(clientlibDir) {
1452
- const all = await listFiles(clientlibDir);
1453
- const preprocessed = all.filter((f) => /\.(less|scss|sass)$/i.test(f)).map((f) => path7.relative(clientlibDir, f));
1454
- let cssFiles;
1455
- const cssTxt = path7.join(clientlibDir, "css.txt");
1456
- if (existsSync5(cssTxt)) {
1457
- const lines = (await readFile3(cssTxt, "utf-8")).split("\n").map((l) => l.trim());
1458
- let base = clientlibDir;
1459
- const listed = [];
1460
- for (const l of lines) {
1461
- if (!l) continue;
1462
- const baseMatch = l.match(/^#base\s*=\s*(.+)$/);
1463
- if (baseMatch) base = path7.join(clientlibDir, baseMatch[1].trim());
1464
- else if (!l.startsWith("#")) listed.push(l);
1465
- }
1466
- cssFiles = listed.map((rel) => path7.join(base, rel)).filter((f) => existsSync5(f) && f.endsWith(".css"));
1467
- } else {
1468
- cssFiles = all.filter((f) => f.endsWith(".css"));
2033
+ async function ghLogin() {
2034
+ try {
2035
+ await run("gh", ["auth", "status"]);
2036
+ return await run("gh", ["api", "user", "-q", ".login"]);
2037
+ } catch {
2038
+ return null;
1469
2039
  }
1470
- const parts = [];
1471
- for (const f of cssFiles) parts.push(await readFile3(f, "utf-8"));
1472
- return {
1473
- css: parts.join("\n\n"),
1474
- files: cssFiles.map((f) => path7.relative(clientlibDir, f)),
1475
- preprocessed
1476
- };
1477
2040
  }
1478
- var init_clientlib = __esm({
1479
- "src/lib/migrate/clientlib.ts"() {
1480
- "use strict";
1481
- init_esm_shims();
2041
+ async function resolveAuth() {
2042
+ const token = await getToken();
2043
+ if (token) {
2044
+ try {
2045
+ return { mode: "api", token, login: await getUser(token) };
2046
+ } catch {
2047
+ }
1482
2048
  }
1483
- });
1484
-
1485
- // src/lib/migrate/dialog.ts
1486
- import { XMLParser } from "fast-xml-parser";
1487
- function tagOf(n) {
1488
- return Object.keys(n).find((k) => k !== ":@") ?? "";
2049
+ const login = await ghLogin();
2050
+ if (login) return { mode: "gh", login };
2051
+ return null;
1489
2052
  }
1490
- function childrenOf(n) {
1491
- const t = tagOf(n);
1492
- const v = n[t];
1493
- return Array.isArray(v) ? v : [];
2053
+ async function exists(auth, owner, name) {
2054
+ if (auth.mode === "api") return repoExists(auth.token, owner, name);
2055
+ try {
2056
+ await run("gh", ["repo", "view", `${owner}/${name}`, "--json", "name"]);
2057
+ return true;
2058
+ } catch {
2059
+ return false;
2060
+ }
1494
2061
  }
1495
- function attrsOf(n) {
1496
- return n[":@"] ?? {};
2062
+ function noAuth() {
2063
+ logger.error("Not authenticated with GitHub.");
2064
+ logger.info(" Run `eds sandbox login` to authorize GitHub, or install + `gh auth login`.");
2065
+ process.exitCode = 1;
1497
2066
  }
1498
- function stripType(v) {
1499
- return typeof v === "string" ? v.replace(/^\{[^}]+\}/, "") : v;
2067
+ async function sandboxLogin() {
2068
+ await logger.logoOnceAnimated("Sandbox - GitHub login");
2069
+ const { input } = await import("@inquirer/prompts");
2070
+ if (CLIENT_ID) {
2071
+ const code = await startDeviceFlow(["repo", "delete_repo"]);
2072
+ logger.info(heading("Authorize GitHub"));
2073
+ logger.info(accentLine("code", code.user_code));
2074
+ logger.info(accentLine("open", code.verification_uri));
2075
+ logger.info("");
2076
+ await openUrl(code.verification_uri);
2077
+ logger.info(chalk4.dim(" Waiting for you to authorize in the browser..."));
2078
+ try {
2079
+ const token2 = await pollDeviceToken(code.device_code, code.interval, code.expires_in);
2080
+ await saveToken(token2);
2081
+ logger.success(`Authorized as ${await getUser(token2)}.`);
2082
+ } catch (err) {
2083
+ logger.error(`Device flow failed: ${err.message}`);
2084
+ process.exitCode = 1;
2085
+ }
2086
+ return;
2087
+ }
2088
+ logger.info(heading("Authorize GitHub (least-privilege token)"));
2089
+ logger.info(" Create a fine-grained token limited to your sandboxes only:");
2090
+ logger.info(` ${chalk4.cyan("https://github.com/settings/personal-access-tokens/new")}`);
2091
+ logger.info(
2092
+ chalk4.dim(" - Resource owner: a dedicated sandbox org (recommended) or your account")
2093
+ );
2094
+ logger.info(
2095
+ chalk4.dim(" - Repository access: All repositories in that org (to create new sandboxes),")
2096
+ );
2097
+ logger.info(chalk4.dim(" or Only select repositories to manage existing ones"));
2098
+ logger.info(
2099
+ chalk4.dim(" - Permissions: Administration = Read and write, Contents = Read and write,")
2100
+ );
2101
+ logger.info(chalk4.dim(" Metadata = Read. Set a short expiry."));
2102
+ logger.info(
2103
+ chalk4.dim(
2104
+ " Tip: `eds sandbox new --org <sandbox-org>` so nothing in your personal account is touched."
2105
+ )
2106
+ );
2107
+ logger.info("");
2108
+ if (await (await import("@inquirer/prompts")).confirm({
2109
+ message: "Open the token page?",
2110
+ default: true
2111
+ })) {
2112
+ await openUrl("https://github.com/settings/personal-access-tokens/new");
2113
+ }
2114
+ const token = (await input({ message: "Paste the token", validate: (v) => !!v.trim() || "Required" })).trim();
2115
+ try {
2116
+ const login = await getUser(token);
2117
+ await saveToken(token);
2118
+ logger.success(`Authorized as ${login}.`);
2119
+ } catch {
2120
+ logger.error("That token didn't work (check the scopes).");
2121
+ process.exitCode = 1;
2122
+ }
1500
2123
  }
1501
- function attr(n, name) {
1502
- return stripType(attrsOf(n)[`@_${name}`]);
2124
+ async function sandboxLogout() {
2125
+ await clearToken();
2126
+ logger.success("Signed out (stored GitHub token removed).");
1503
2127
  }
1504
- function leafComponent(rt, n) {
1505
- const multiple = attr(n, "multiple") === "true" || attr(n, "multiple") === "{Boolean}true";
1506
- if (rt.endsWith("/form/textfield")) return "text";
1507
- if (rt.endsWith("/form/textarea")) return "textarea";
1508
- if (rt.includes("/richtext")) return "richtext";
1509
- if (rt.endsWith("/form/numberfield")) return "number";
1510
- if (rt.endsWith("/form/checkbox")) return "boolean";
1511
- if (rt.endsWith("/form/radiogroup")) return "radio-group";
1512
- if (rt.endsWith("/form/select")) return multiple ? "multiselect" : "select";
1513
- if (rt.endsWith("/form/datepicker")) return "date-time";
1514
- if (rt.endsWith("/form/pathfield") || rt.endsWith("/form/pathbrowser")) return "aem-content";
1515
- if (rt.includes("/fileupload") || rt.endsWith("/form/fileupload")) return "reference";
1516
- if (rt.includes("tagfield") || rt.includes("tagpicker") || rt.includes("tagbrowser"))
1517
- return "aem-tag";
1518
- if (rt.includes("contentfragment")) return "aem-content-fragment";
1519
- if (rt.includes("experiencefragment")) return "aem-experience-fragment";
1520
- return null;
2128
+ async function sandboxNew(name, options = {}) {
2129
+ const auth = await resolveAuth();
2130
+ if (!auth) return noAuth();
2131
+ await logger.logoOnceAnimated("Sandbox");
2132
+ const interactive = !options.yes;
2133
+ const { select, confirm } = await import("@inquirer/prompts");
2134
+ let template2 = options.boilerplate;
2135
+ if (!template2) {
2136
+ if (options.ue) template2 = BOILERPLATES.ue;
2137
+ else if (interactive) {
2138
+ template2 = await select({
2139
+ message: "Which boilerplate?",
2140
+ choices: [
2141
+ { name: "Document Authoring (adobe/aem-boilerplate)", value: BOILERPLATES.document },
2142
+ {
2143
+ name: "Universal Editor / crosswalk (adobe/aem-boilerplate-xwalk)",
2144
+ value: BOILERPLATES.ue
2145
+ }
2146
+ ]
2147
+ });
2148
+ } else template2 = BOILERPLATES.document;
2149
+ }
2150
+ let priv = options.private ?? false;
2151
+ if (options.private === void 0 && interactive) {
2152
+ priv = await select({
2153
+ message: "Visibility?",
2154
+ choices: [
2155
+ { name: "Public", value: false },
2156
+ { name: "Private", value: true }
2157
+ ],
2158
+ default: false
2159
+ });
2160
+ }
2161
+ const owner = options.org ?? auth.login;
2162
+ if (await exists(auth, owner, name)) {
2163
+ logger.error(`A repository ${owner}/${name} already exists - refusing to touch it.`);
2164
+ logger.info(
2165
+ chalk4.dim(" Pick another name, or remove it first (e.g. `eds sandbox rm` for a sandbox).")
2166
+ );
2167
+ process.exitCode = 1;
2168
+ return;
2169
+ }
2170
+ logger.info(heading("Creating sandbox", `${owner}/${name} \xB7 ${template2}`));
2171
+ try {
2172
+ if (auth.mode === "api") {
2173
+ const repo = await createFromTemplate(auth.token, template2, { owner, name, private: priv });
2174
+ await addTopics(auth.token, owner, name, [SANDBOX_TOPIC]);
2175
+ logger.info(chalk4.dim(" Cloning..."));
2176
+ await run("git", ["clone", repo.cloneUrl, name], { stdio: "inherit" });
2177
+ } else {
2178
+ const repoRef = options.org ? `${options.org}/${name}` : name;
2179
+ await run("gh", createArgs(repoRef, { template: template2, private: priv, clone: true }), {
2180
+ stdio: "inherit"
2181
+ });
2182
+ try {
2183
+ await run("gh", ["repo", "edit", `${owner}/${name}`, "--add-topic", SANDBOX_TOPIC]);
2184
+ } catch {
2185
+ }
2186
+ }
2187
+ } catch (err) {
2188
+ logger.error(`Create failed: ${err.message}`);
2189
+ process.exitCode = 1;
2190
+ return;
2191
+ }
2192
+ logger.info("");
2193
+ logger.info(heading("Sandbox ready"));
2194
+ logger.info(accentLine("repo", `https://github.com/${owner}/${name}`));
2195
+ logger.info(accentLine("preview", previewUrl(owner, name)));
2196
+ logger.info("");
2197
+ if (interactive) {
2198
+ if (await confirm({ message: "Open the AEM Code Sync install page now?", default: true })) {
2199
+ await openUrl(codeSyncInstallUrl());
2200
+ }
2201
+ if (await confirm({ message: `Configure authoring now (eds init in ./${name})?`, default: true })) {
2202
+ const prevCwd = process.cwd();
2203
+ try {
2204
+ process.chdir(path11.join(prevCwd, name));
2205
+ const { initProject: initProject2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2206
+ await initProject2({});
2207
+ } finally {
2208
+ process.chdir(prevCwd);
2209
+ }
2210
+ }
2211
+ } else {
2212
+ logger.info(chalk4.dim(` Install AEM Code Sync: ${codeSyncInstallUrl()}`));
2213
+ logger.info(chalk4.dim(` Configure authoring: cd ${name} && eds init`));
2214
+ }
2215
+ logger.info("");
2216
+ logger.info(box([`Sandbox "${name}" ready - tear down with \`eds sandbox rm ${name}\``]));
1521
2217
  }
1522
- function isWrapper(rt) {
1523
- return rt.includes("/foundation/container") || rt.includes("/foundation/fixedcolumns") || rt.includes("/foundation/well") || rt.includes("/foundation/tabs") || rt.includes("/foundation/form/fieldset") || rt.includes("/foundation/accordion");
2218
+ async function sandboxList() {
2219
+ const auth = await resolveAuth();
2220
+ if (!auth) return noAuth();
2221
+ let names = [];
2222
+ try {
2223
+ if (auth.mode === "api") {
2224
+ names = await searchByTopic(auth.token, auth.login, SANDBOX_TOPIC);
2225
+ } else {
2226
+ const out = await run("gh", [
2227
+ "repo",
2228
+ "list",
2229
+ "--topic",
2230
+ SANDBOX_TOPIC,
2231
+ "--limit",
2232
+ "100",
2233
+ "--json",
2234
+ "nameWithOwner",
2235
+ "-q",
2236
+ ".[].nameWithOwner"
2237
+ ]);
2238
+ names = out ? out.split("\n").filter(Boolean) : [];
2239
+ }
2240
+ } catch (err) {
2241
+ logger.error(`List failed: ${err.message}`);
2242
+ process.exitCode = 1;
2243
+ return;
2244
+ }
2245
+ logger.info(heading("Sandboxes", `${names.length} tagged ${SANDBOX_TOPIC}`));
2246
+ if (names.length === 0) {
2247
+ logger.info(chalk4.dim(" None yet. Create one with `eds sandbox new <name>`."));
2248
+ return;
2249
+ }
2250
+ const w = columnWidth(names);
2251
+ for (const full of names) {
2252
+ const [o, n2] = full.split("/");
2253
+ logger.info(accentLine(full, previewUrl(o, n2), w));
2254
+ }
1524
2255
  }
1525
- function valueTypeFor(c) {
1526
- switch (c) {
1527
- case "number":
1528
- return "number";
2256
+ async function sandboxRm(name, options = {}) {
2257
+ const auth = await resolveAuth();
2258
+ if (!auth) return noAuth();
2259
+ const owner = options.org ?? auth.login;
2260
+ const repoRef = `${owner}/${name}`;
2261
+ if (!options.yes) {
2262
+ const { confirm } = await import("@inquirer/prompts");
2263
+ const go = await confirm({
2264
+ message: `Permanently delete ${repoRef}? This cannot be undone.`,
2265
+ default: false
2266
+ });
2267
+ if (!go) {
2268
+ logger.info("Cancelled.");
2269
+ return;
2270
+ }
2271
+ }
2272
+ try {
2273
+ if (auth.mode === "api") await deleteRepo(auth.token, owner, name);
2274
+ else await run("gh", ["repo", "delete", repoRef, "--yes"]);
2275
+ } catch (err) {
2276
+ logger.error(`Delete failed: ${err.message}`);
2277
+ logger.info(
2278
+ chalk4.dim(
2279
+ " Deleting needs the delete_repo scope (re-run `eds sandbox login`, or `gh auth refresh -s delete_repo`)."
2280
+ )
2281
+ );
2282
+ process.exitCode = 1;
2283
+ return;
2284
+ }
2285
+ logger.info(heading("Deleted"));
2286
+ logger.info(accentLine(repoRef, "removed"));
2287
+ }
2288
+ var init_sandbox = __esm({
2289
+ "src/commands/sandbox.ts"() {
2290
+ "use strict";
2291
+ init_esm_shims();
2292
+ init_logger();
2293
+ init_api();
2294
+ init_auth();
2295
+ init_github();
2296
+ init_ui();
2297
+ }
2298
+ });
2299
+
2300
+ // src/lib/template/authoring.ts
2301
+ import { existsSync as existsSync9 } from "fs";
2302
+ import { readFile as readFile5 } from "fs/promises";
2303
+ import path12 from "path";
2304
+ import { parse as parseYaml } from "yaml";
2305
+ async function detectAuthoring(projectRoot) {
2306
+ const fstab = path12.join(projectRoot, "fstab.yaml");
2307
+ if (existsSync9(fstab)) {
2308
+ try {
2309
+ const parsed = parseYaml(await readFile5(fstab, "utf-8")) ?? {};
2310
+ const mp = parsed.mountpoints?.["/"];
2311
+ const url = typeof mp === "string" ? mp : mp?.url ?? "";
2312
+ const da = url.match(/content\.da\.live\/([^/]+)\/([^/]+)/);
2313
+ if (da) return { model: "da", org: da[1], site: da[2], mountpoint: url };
2314
+ if (/drive\.google\.com/.test(url)) return { model: "gdrive", mountpoint: url };
2315
+ if (/sharepoint\.com|-my\.sharepoint/.test(url))
2316
+ return { model: "sharepoint", mountpoint: url };
2317
+ return { model: "document", mountpoint: url };
2318
+ } catch {
2319
+ }
2320
+ }
2321
+ return { model: "ue" };
2322
+ }
2323
+ var init_authoring = __esm({
2324
+ "src/lib/template/authoring.ts"() {
2325
+ "use strict";
2326
+ init_esm_shims();
2327
+ }
2328
+ });
2329
+
2330
+ // src/lib/template/da.ts
2331
+ function daSourceUrl(org, site, path36) {
2332
+ const clean = path36.replace(/^\/+/, "").replace(/\.html$/i, "");
2333
+ return `https://admin.da.live/source/${org}/${site}/${clean}.html`;
2334
+ }
2335
+ function daEditUrl(org, site, path36) {
2336
+ const clean = path36.replace(/^\/+/, "").replace(/\.html$/i, "");
2337
+ return `https://da.live/edit#/${org}/${site}/${clean}`;
2338
+ }
2339
+ async function pushToDa(org, site, path36, html, token) {
2340
+ const url = daSourceUrl(org, site, path36);
2341
+ const form = new FormData();
2342
+ form.append("data", new Blob([html], { type: "text/html" }), "content.html");
2343
+ const headers2 = {};
2344
+ if (token) headers2.Authorization = `Bearer ${token}`;
2345
+ const res = await fetch(url, { method: "POST", body: form, headers: headers2 });
2346
+ if (!res.ok) {
2347
+ throw new Error(`DA push failed: ${res.status} ${res.statusText}`);
2348
+ }
2349
+ return url;
2350
+ }
2351
+ var init_da = __esm({
2352
+ "src/lib/template/da.ts"() {
2353
+ "use strict";
2354
+ init_esm_shims();
2355
+ }
2356
+ });
2357
+
2358
+ // src/lib/template/page.ts
2359
+ function blockDiv(name) {
2360
+ return ` <div class="${name}">
2361
+ <div>
2362
+ <div>${name} content</div>
2363
+ </div>
2364
+ </div>`;
2365
+ }
2366
+ function metadataBlock(opts) {
2367
+ const rows = [
2368
+ ` <div><div>Title</div><div>${opts.title}</div></div>`,
2369
+ ` <div><div>Description</div><div>${opts.description ?? ""}</div></div>`
2370
+ ];
2371
+ if (opts.area) rows.push(` <div><div>Template</div><div>${opts.area}</div></div>`);
2372
+ return ` <div class="metadata">
2373
+ ${rows.join("\n")}
2374
+ </div>`;
2375
+ }
2376
+ function buildPageHtml(opts) {
2377
+ const blocks = opts.blocks ?? [];
2378
+ const intro = ` <div>
2379
+ <h1>${opts.title}</h1>
2380
+ <p>${opts.description ?? "Intro copy goes here."}</p>
2381
+ </div>`;
2382
+ const blockSections = blocks.map((b) => ` <div>
2383
+ ${blockDiv(b)}
2384
+ </div>`);
2385
+ const metaSection = ` <div>
2386
+ ${metadataBlock(opts)}
2387
+ </div>`;
2388
+ return `<body>
2389
+ <header></header>
2390
+ <main>
2391
+ ${[intro, ...blockSections, metaSection].join("\n")}
2392
+ </main>
2393
+ <footer></footer>
2394
+ </body>
2395
+ `;
2396
+ }
2397
+ var init_page = __esm({
2398
+ "src/lib/template/page.ts"() {
2399
+ "use strict";
2400
+ init_esm_shims();
2401
+ }
2402
+ });
2403
+
2404
+ // src/lib/template/ue.ts
2405
+ import { existsSync as existsSync10 } from "fs";
2406
+ import { readFile as readFile6 } from "fs/promises";
2407
+ import path13 from "path";
2408
+ function fieldDefault(field) {
2409
+ if (field.value !== void 0) return field.value;
2410
+ switch (field.valueType) {
1529
2411
  case "boolean":
1530
- return "boolean";
1531
- case "date-time":
1532
- return "date";
1533
- case "checkbox-group":
1534
- return "string[]";
1535
- case "aem-content":
1536
- case "aem-content-fragment":
1537
- case "aem-experience-fragment":
1538
- return void 0;
2412
+ return false;
2413
+ case "number":
2414
+ return 0;
2415
+ case "string[]":
2416
+ return [];
1539
2417
  default:
1540
- return "string";
2418
+ return "";
1541
2419
  }
1542
2420
  }
1543
- function cleanName(raw, fallback) {
1544
- const n = (raw ?? "").replace(/^\.\//, "").trim();
1545
- return n || fallback;
2421
+ async function blockDefaults(projectRoot, block2) {
2422
+ const file = path13.join(projectRoot, "blocks", block2, `_${block2}.json`);
2423
+ if (!existsSync10(file)) return {};
2424
+ try {
2425
+ const parsed = JSON.parse(await readFile6(file, "utf-8"));
2426
+ const models = Array.isArray(parsed.models) ? parsed.models : [];
2427
+ const model2 = models.find((m) => m.id === block2) ?? models[0];
2428
+ const fields = model2?.fields ?? [];
2429
+ const out = {};
2430
+ for (const f2 of fields) {
2431
+ if (f2.component === "tab") continue;
2432
+ out[f2.name] = f2.component === "container" ? [] : fieldDefault(f2);
2433
+ }
2434
+ return out;
2435
+ } catch {
2436
+ return {};
2437
+ }
1546
2438
  }
1547
- function readOptions(fieldNode) {
1548
- const items = childrenOf(fieldNode).find((c) => tagOf(c) === "items");
1549
- if (!items) return [];
1550
- return childrenOf(items).map((opt) => {
1551
- const value = attr(opt, "value") ?? "";
1552
- const name = attr(opt, "text") ?? attr(opt, "jcr:title") ?? value;
1553
- return { name, value };
1554
- });
2439
+ async function buildUeTemplate(projectRoot, opts) {
2440
+ const components = [];
2441
+ for (const b of opts.blocks) {
2442
+ components.push({ name: b, model: b, fields: await blockDefaults(projectRoot, b) });
2443
+ }
2444
+ const template2 = { title: opts.title, template: opts.name, components };
2445
+ return `${JSON.stringify(template2, null, 2)}
2446
+ `;
1555
2447
  }
1556
- function walk(nodes, out, unmapped, counter) {
1557
- for (const node of nodes) {
1558
- const rt = attr(node, "sling:resourceType") ?? "";
1559
- if (rt.endsWith("/foundation/tabs")) {
1560
- const items = childrenOf(node).find((c) => tagOf(c) === "items");
1561
- for (const tab of items ? childrenOf(items) : []) {
1562
- const label = attr(tab, "jcr:title");
1563
- out.push({ component: "tab", name: cleanName(void 0, tagOf(tab)), label });
1564
- walk(childrenOf(tab), out, unmapped, counter);
1565
- }
1566
- continue;
2448
+ var init_ue2 = __esm({
2449
+ "src/lib/template/ue.ts"() {
2450
+ "use strict";
2451
+ init_esm_shims();
2452
+ }
2453
+ });
2454
+
2455
+ // src/commands/template.ts
2456
+ var template_exports = {};
2457
+ __export(template_exports, {
2458
+ templateNew: () => templateNew
2459
+ });
2460
+ import { existsSync as existsSync11 } from "fs";
2461
+ import { mkdir as mkdir5, readFile as readFile7, readdir, writeFile as writeFile9 } from "fs/promises";
2462
+ import path14 from "path";
2463
+ import chalk5 from "chalk";
2464
+ async function projectBlocks(projectRoot) {
2465
+ const dir = path14.join(projectRoot, "blocks");
2466
+ if (!existsSync11(dir)) return [];
2467
+ return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
2468
+ }
2469
+ async function readEdsrcAdmin(projectRoot) {
2470
+ const rc = path14.join(projectRoot, ".edsrc.json");
2471
+ if (!existsSync11(rc)) return {};
2472
+ try {
2473
+ return JSON.parse(await readFile7(rc, "utf-8")).admin ?? {};
2474
+ } catch {
2475
+ return {};
2476
+ }
2477
+ }
2478
+ async function registerBlocks(projectRoot, blocks) {
2479
+ if (!blocks.length) return false;
2480
+ const file = path14.join(projectRoot, "component-filters.json");
2481
+ let existing = null;
2482
+ if (existsSync11(file)) {
2483
+ try {
2484
+ existing = JSON.parse(await readFile7(file, "utf-8"));
2485
+ } catch {
2486
+ existing = null;
1567
2487
  }
1568
- if (rt.endsWith("/form/multifield")) {
1569
- const sub = [];
1570
- walk(childrenOf(node), sub, unmapped, counter);
1571
- const name = cleanName(attr(node, "name"), `items${counter.i++}`);
1572
- out.push({
1573
- component: "container",
1574
- name,
1575
- label: attr(node, "fieldLabel") ?? "Items",
1576
- multi: true,
1577
- collapsible: true,
1578
- fields: sub
2488
+ }
2489
+ const filters = mergeSectionFilter(existing, blocks);
2490
+ await writeFile9(file, `${JSON.stringify(filters, null, 2)}
2491
+ `);
2492
+ return true;
2493
+ }
2494
+ async function templateNew(nameArg, options = {}) {
2495
+ const projectRoot = findProjectRoot();
2496
+ if (!projectRoot) {
2497
+ logger.error("Not inside an EDS project (no EDS project markers found).");
2498
+ process.exitCode = 1;
2499
+ return;
2500
+ }
2501
+ await logger.logoOnceAnimated("Page template");
2502
+ const { input, select, confirm } = await import("@inquirer/prompts");
2503
+ const interactive = !options.yes;
2504
+ const detected = await detectAuthoring(projectRoot);
2505
+ const model2 = options.authoring ?? detected.model;
2506
+ const name = slug2(nameArg);
2507
+ const title = options.title ?? (interactive ? await input({ message: "Page title", default: nameArg }) : nameArg);
2508
+ const description = options.description ?? (interactive ? await input({ message: "Description (optional)" }) : "");
2509
+ const pagePath = options.path ?? `/templates/${name}`;
2510
+ let blocks = options.blocks;
2511
+ if (!blocks && interactive) {
2512
+ const available = await projectBlocks(projectRoot);
2513
+ const pool = available.length ? available : STANDARD_BLOCKS.map((b) => b.id);
2514
+ const ordered = [];
2515
+ let adding = true;
2516
+ while (adding) {
2517
+ const pick = await select({
2518
+ message: `Add a block in order (${ordered.length} so far)`,
2519
+ choices: [
2520
+ ...pool.map((b) => ({ name: b, value: b })),
2521
+ { name: ordered.length ? "- done -" : "- done (empty page) -", value: "__done" }
2522
+ ]
1579
2523
  });
1580
- continue;
2524
+ if (pick === "__done") adding = false;
2525
+ else ordered.push(pick);
1581
2526
  }
1582
- const comp = rt ? leafComponent(rt, node) : null;
1583
- if (comp) {
1584
- const name = cleanName(attr(node, "name"), `field${counter.i++}`);
1585
- const field = { component: comp, name };
1586
- const vt = valueTypeFor(comp);
1587
- if (vt) field.valueType = vt;
1588
- const label = attr(node, "fieldLabel") ?? attr(node, "text");
1589
- if (label) field.label = label;
1590
- if (comp === "select" || comp === "radio-group" || comp === "multiselect") {
1591
- const options = readOptions(node);
1592
- if (options.length) field.options = options;
2527
+ blocks = ordered;
2528
+ }
2529
+ const seeded = blocks ?? [];
2530
+ const html = buildPageHtml({
2531
+ title,
2532
+ description: description || void 0,
2533
+ blocks: seeded,
2534
+ area: options.area
2535
+ });
2536
+ const localRel = path14.join("templates", `${name}.html`);
2537
+ await mkdir5(path14.join(projectRoot, "templates"), { recursive: true });
2538
+ await writeFile9(path14.join(projectRoot, localRel), html);
2539
+ logger.info(heading("Page template", `${pagePath} \xB7 ${model2}`));
2540
+ logger.info(accentLine(localRel, `${seeded.length} block(s) + metadata`));
2541
+ if (model2 === "da") {
2542
+ const admin = await readEdsrcAdmin(projectRoot);
2543
+ const org = options.org ?? admin.org ?? detected.org;
2544
+ const site = options.site ?? admin.site ?? detected.site;
2545
+ const wantPush = options.push ?? (interactive ? await confirm({ message: "Push to DA (da.live)?", default: true }) : true);
2546
+ if (wantPush) {
2547
+ if (!org || !site) {
2548
+ logger.warn("No DA org/site. Pass --org/--site or set admin in .edsrc.json.");
2549
+ } else {
2550
+ try {
2551
+ await pushToDa(org, site, pagePath, html, process.env.DA_TOKEN);
2552
+ logger.info(accentLine("pushed to DA", daEditUrl(org, site, pagePath)));
2553
+ } catch (err) {
2554
+ logger.error(`DA push failed: ${err.message}`);
2555
+ logger.info(chalk5.dim(" Open projects allow anonymous writes; others need DA_TOKEN."));
2556
+ }
1593
2557
  }
1594
- const def = attr(node, "value") ?? (comp === "boolean" ? attr(node, "checked") : void 0);
1595
- if (def !== void 0 && def !== "") field.value = comp === "boolean" ? def === "true" : def;
1596
- out.push(field);
1597
- continue;
1598
2558
  }
1599
- if (rt && attr(node, "name") && !isWrapper(rt)) unmapped.add(rt);
1600
- walk(childrenOf(node), out, unmapped, counter);
1601
- }
1602
- }
1603
- function parseDialog(xml) {
1604
- const parser = new XMLParser({
1605
- ignoreAttributes: false,
1606
- attributeNamePrefix: "@_",
1607
- preserveOrder: true,
2559
+ } else if (model2 === "gdrive" || model2 === "sharepoint") {
2560
+ const where = model2 === "gdrive" ? "a Google Doc" : "a Word doc";
2561
+ logger.info("");
2562
+ logger.info(
2563
+ chalk5.dim(` Document model: import ${localRel} into ${where} in your mounted folder,`)
2564
+ );
2565
+ logger.info(
2566
+ chalk5.dim(" then preview/publish. (Share the folder + install aem-code-sync first.)")
2567
+ );
2568
+ } else if (model2 === "ue") {
2569
+ const jsonRel = path14.join("templates", `${name}.json`);
2570
+ await writeFile9(
2571
+ path14.join(projectRoot, jsonRel),
2572
+ await buildUeTemplate(projectRoot, { title, name, blocks: seeded })
2573
+ );
2574
+ logger.info(
2575
+ accentLine(jsonRel, `initial content (${seeded.length} component(s), in order)`)
2576
+ );
2577
+ const registered = await registerBlocks(projectRoot, seeded);
2578
+ if (registered) {
2579
+ logger.info(
2580
+ accentLine("component-filters.json", "seeded blocks allowed in Universal Editor")
2581
+ );
2582
+ }
2583
+ logger.info("");
2584
+ logger.info(
2585
+ chalk5.dim(" Universal Editor: use the template JSON as the page's initial content, then")
2586
+ );
2587
+ logger.info(chalk5.dim(" author it in the Universal Editor (wire the repo to AEM)."));
2588
+ }
2589
+ logger.info("");
2590
+ logger.info(box([`"${name}" template (${model2}) - preview with \`eds preview ${pagePath}\``]));
2591
+ }
2592
+ var slug2;
2593
+ var init_template = __esm({
2594
+ "src/commands/template.ts"() {
2595
+ "use strict";
2596
+ init_esm_shims();
2597
+ init_logger();
2598
+ init_project();
2599
+ init_blocks();
2600
+ init_ue();
2601
+ init_authoring();
2602
+ init_da();
2603
+ init_page();
2604
+ init_ue2();
2605
+ init_ui();
2606
+ slug2 = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-/]+/g, "-").replace(/^-+|-+$/g, "");
2607
+ }
2608
+ });
2609
+
2610
+ // src/lib/migrate/clientlib.ts
2611
+ import { existsSync as existsSync12 } from "fs";
2612
+ import { readFile as readFile8, readdir as readdir2 } from "fs/promises";
2613
+ import path15 from "path";
2614
+ async function listFiles(dir) {
2615
+ const out = [];
2616
+ const walk3 = async (d) => {
2617
+ for (const e of await readdir2(d, { withFileTypes: true })) {
2618
+ const p = path15.join(d, e.name);
2619
+ if (e.isDirectory()) await walk3(p);
2620
+ else out.push(p);
2621
+ }
2622
+ };
2623
+ if (existsSync12(dir)) await walk3(dir);
2624
+ return out;
2625
+ }
2626
+ async function findClientlibDir(componentDir) {
2627
+ if (!existsSync12(componentDir)) return null;
2628
+ const entries = await readdir2(componentDir, { withFileTypes: true });
2629
+ const match = entries.find((e) => e.isDirectory() && /clientlib/i.test(e.name));
2630
+ if (match) return path15.join(componentDir, match.name);
2631
+ const hasCss = entries.some((e) => e.isFile() && e.name.endsWith(".css"));
2632
+ return hasCss ? componentDir : null;
2633
+ }
2634
+ async function readClientlibCss(clientlibDir) {
2635
+ const all = await listFiles(clientlibDir);
2636
+ const preprocessed = all.filter((f2) => /\.(less|scss|sass)$/i.test(f2)).map((f2) => path15.relative(clientlibDir, f2));
2637
+ let cssFiles;
2638
+ const cssTxt = path15.join(clientlibDir, "css.txt");
2639
+ if (existsSync12(cssTxt)) {
2640
+ const lines = (await readFile8(cssTxt, "utf-8")).split("\n").map((l) => l.trim());
2641
+ let base = clientlibDir;
2642
+ const listed = [];
2643
+ for (const l of lines) {
2644
+ if (!l) continue;
2645
+ const baseMatch = l.match(/^#base\s*=\s*(.+)$/);
2646
+ if (baseMatch) base = path15.join(clientlibDir, baseMatch[1].trim());
2647
+ else if (!l.startsWith("#")) listed.push(l);
2648
+ }
2649
+ cssFiles = listed.map((rel) => path15.join(base, rel)).filter((f2) => existsSync12(f2) && f2.endsWith(".css"));
2650
+ } else {
2651
+ cssFiles = all.filter((f2) => f2.endsWith(".css"));
2652
+ }
2653
+ const parts = [];
2654
+ for (const f2 of cssFiles) parts.push(await readFile8(f2, "utf-8"));
2655
+ return {
2656
+ css: parts.join("\n\n"),
2657
+ files: cssFiles.map((f2) => path15.relative(clientlibDir, f2)),
2658
+ preprocessed
2659
+ };
2660
+ }
2661
+ var init_clientlib = __esm({
2662
+ "src/lib/migrate/clientlib.ts"() {
2663
+ "use strict";
2664
+ init_esm_shims();
2665
+ }
2666
+ });
2667
+
2668
+ // src/lib/migrate/dialog.ts
2669
+ import { XMLParser } from "fast-xml-parser";
2670
+ function tagOf(n2) {
2671
+ return Object.keys(n2).find((k) => k !== ":@") ?? "";
2672
+ }
2673
+ function childrenOf(n2) {
2674
+ const t = tagOf(n2);
2675
+ const v = n2[t];
2676
+ return Array.isArray(v) ? v : [];
2677
+ }
2678
+ function attrsOf(n2) {
2679
+ return n2[":@"] ?? {};
2680
+ }
2681
+ function stripType(v) {
2682
+ return typeof v === "string" ? v.replace(/^\{[^}]+\}/, "") : v;
2683
+ }
2684
+ function attr(n2, name) {
2685
+ return stripType(attrsOf(n2)[`@_${name}`]);
2686
+ }
2687
+ function leafComponent(rt, n2) {
2688
+ const multiple = attr(n2, "multiple") === "true" || attr(n2, "multiple") === "{Boolean}true";
2689
+ if (rt.endsWith("/form/textfield")) return "text";
2690
+ if (rt.endsWith("/form/textarea")) return "textarea";
2691
+ if (rt.includes("/richtext")) return "richtext";
2692
+ if (rt.endsWith("/form/numberfield")) return "number";
2693
+ if (rt.endsWith("/form/checkbox")) return "boolean";
2694
+ if (rt.endsWith("/form/radiogroup")) return "radio-group";
2695
+ if (rt.endsWith("/form/select")) return multiple ? "multiselect" : "select";
2696
+ if (rt.endsWith("/form/datepicker")) return "date-time";
2697
+ if (rt.endsWith("/form/pathfield") || rt.endsWith("/form/pathbrowser")) return "aem-content";
2698
+ if (rt.includes("/fileupload") || rt.endsWith("/form/fileupload")) return "reference";
2699
+ if (rt.includes("tagfield") || rt.includes("tagpicker") || rt.includes("tagbrowser"))
2700
+ return "aem-tag";
2701
+ if (rt.includes("contentfragment")) return "aem-content-fragment";
2702
+ if (rt.includes("experiencefragment")) return "aem-experience-fragment";
2703
+ return null;
2704
+ }
2705
+ function isWrapper(rt) {
2706
+ return rt.includes("/foundation/container") || rt.includes("/foundation/fixedcolumns") || rt.includes("/foundation/well") || rt.includes("/foundation/tabs") || rt.includes("/foundation/form/fieldset") || rt.includes("/foundation/accordion");
2707
+ }
2708
+ function valueTypeFor(c) {
2709
+ switch (c) {
2710
+ case "number":
2711
+ return "number";
2712
+ case "boolean":
2713
+ return "boolean";
2714
+ case "date-time":
2715
+ return "date";
2716
+ case "checkbox-group":
2717
+ return "string[]";
2718
+ case "aem-content":
2719
+ case "aem-content-fragment":
2720
+ case "aem-experience-fragment":
2721
+ return void 0;
2722
+ default:
2723
+ return "string";
2724
+ }
2725
+ }
2726
+ function cleanName(raw, fallback) {
2727
+ const n2 = (raw ?? "").replace(/^\.\//, "").trim();
2728
+ return n2 || fallback;
2729
+ }
2730
+ function readOptions(fieldNode) {
2731
+ const items = childrenOf(fieldNode).find((c) => tagOf(c) === "items");
2732
+ if (!items) return [];
2733
+ return childrenOf(items).map((opt) => {
2734
+ const value = attr(opt, "value") ?? "";
2735
+ const name = attr(opt, "text") ?? attr(opt, "jcr:title") ?? value;
2736
+ return { name, value };
2737
+ });
2738
+ }
2739
+ function walk(nodes, out, unmapped, counter) {
2740
+ for (const node of nodes) {
2741
+ const rt = attr(node, "sling:resourceType") ?? "";
2742
+ if (rt.endsWith("/foundation/tabs")) {
2743
+ const items = childrenOf(node).find((c) => tagOf(c) === "items");
2744
+ for (const tab of items ? childrenOf(items) : []) {
2745
+ const label = attr(tab, "jcr:title");
2746
+ out.push({ component: "tab", name: cleanName(void 0, tagOf(tab)), label });
2747
+ walk(childrenOf(tab), out, unmapped, counter);
2748
+ }
2749
+ continue;
2750
+ }
2751
+ if (rt.endsWith("/form/multifield")) {
2752
+ const sub = [];
2753
+ walk(childrenOf(node), sub, unmapped, counter);
2754
+ const name = cleanName(attr(node, "name"), `items${counter.i++}`);
2755
+ out.push({
2756
+ component: "container",
2757
+ name,
2758
+ label: attr(node, "fieldLabel") ?? "Items",
2759
+ multi: true,
2760
+ collapsible: true,
2761
+ fields: sub
2762
+ });
2763
+ continue;
2764
+ }
2765
+ const comp = rt ? leafComponent(rt, node) : null;
2766
+ if (comp) {
2767
+ const name = cleanName(attr(node, "name"), `field${counter.i++}`);
2768
+ const field = { component: comp, name };
2769
+ const vt = valueTypeFor(comp);
2770
+ if (vt) field.valueType = vt;
2771
+ const label = attr(node, "fieldLabel") ?? attr(node, "text");
2772
+ if (label) field.label = label;
2773
+ if (comp === "select" || comp === "radio-group" || comp === "multiselect") {
2774
+ const options = readOptions(node);
2775
+ if (options.length) field.options = options;
2776
+ }
2777
+ const def = attr(node, "value") ?? (comp === "boolean" ? attr(node, "checked") : void 0);
2778
+ if (def !== void 0 && def !== "") field.value = comp === "boolean" ? def === "true" : def;
2779
+ out.push(field);
2780
+ continue;
2781
+ }
2782
+ if (rt && attr(node, "name") && !isWrapper(rt)) unmapped.add(rt);
2783
+ walk(childrenOf(node), out, unmapped, counter);
2784
+ }
2785
+ }
2786
+ function parseDialog(xml) {
2787
+ const parser = new XMLParser({
2788
+ ignoreAttributes: false,
2789
+ attributeNamePrefix: "@_",
2790
+ preserveOrder: true,
1608
2791
  removeNSPrefix: false,
1609
2792
  trimValues: true
1610
2793
  });
1611
2794
  const tree = parser.parse(xml);
1612
- const root = tree.find((n) => tagOf(n) === "jcr:root");
2795
+ const root = tree.find((n2) => tagOf(n2) === "jcr:root");
1613
2796
  const fields = [];
1614
2797
  const unmapped = /* @__PURE__ */ new Set();
1615
2798
  if (root) walk(childrenOf(root), fields, unmapped, { i: 1 });
1616
2799
  return { fields, unmapped: [...unmapped] };
1617
2800
  }
1618
- var init_dialog = __esm({
1619
- "src/lib/migrate/dialog.ts"() {
2801
+ var init_dialog = __esm({
2802
+ "src/lib/migrate/dialog.ts"() {
2803
+ "use strict";
2804
+ init_esm_shims();
2805
+ }
2806
+ });
2807
+
2808
+ // src/lib/migrate/component.ts
2809
+ import { existsSync as existsSync13 } from "fs";
2810
+ import { readFile as readFile9 } from "fs/promises";
2811
+ import path16 from "path";
2812
+ import { XMLParser as XMLParser2 } from "fast-xml-parser";
2813
+ function findDialogFile(componentDir) {
2814
+ const candidates = [
2815
+ path16.join(componentDir, "_cq_dialog", ".content.xml"),
2816
+ path16.join(componentDir, "_cq_dialog.xml"),
2817
+ path16.join(componentDir, "cq:dialog", ".content.xml")
2818
+ ];
2819
+ return candidates.find((c) => existsSync13(c)) ?? null;
2820
+ }
2821
+ async function readComponentTitle(componentDir) {
2822
+ const file = path16.join(componentDir, ".content.xml");
2823
+ if (!existsSync13(file)) return void 0;
2824
+ try {
2825
+ const parser = new XMLParser2({ ignoreAttributes: false, attributeNamePrefix: "@_" });
2826
+ const parsed = parser.parse(await readFile9(file, "utf-8"));
2827
+ const root = parsed["jcr:root"] ?? {};
2828
+ const title = root["@_jcr:title"];
2829
+ return typeof title === "string" ? title.replace(/^\{[^}]+\}/, "") : void 0;
2830
+ } catch {
2831
+ return void 0;
2832
+ }
2833
+ }
2834
+ async function readDialogFields(componentDir) {
2835
+ const dialog = findDialogFile(componentDir);
2836
+ if (!dialog) return null;
2837
+ return parseDialog(await readFile9(dialog, "utf-8"));
2838
+ }
2839
+ function migratedModelFile(blockName, title, fields) {
2840
+ const model2 = {
2841
+ definitions: [
2842
+ {
2843
+ title,
2844
+ id: blockName,
2845
+ plugins: {
2846
+ xwalk: {
2847
+ page: {
2848
+ resourceType: "core/franklin/components/block/v1/block",
2849
+ template: { name: title, model: blockName }
2850
+ }
2851
+ }
2852
+ }
2853
+ }
2854
+ ],
2855
+ models: [{ id: blockName, fields }],
2856
+ filters: []
2857
+ };
2858
+ return `${JSON.stringify(model2, null, 2)}
2859
+ `;
2860
+ }
2861
+ function migratedBlockJs(blockName, fields) {
2862
+ const hasContainer = fields.some((f2) => f2.component === "container" && f2.multi);
2863
+ const body = hasContainer ? ` // This block was migrated from a classic AEM component with a multifield.
2864
+ // Each child row is one repeated item - enhance them here.
2865
+ [...block.children].forEach((row) => {
2866
+ row.classList.add('${blockName}-item');
2867
+ });` : ` // Migrated from a classic AEM component. The authored fields arrive as
2868
+ // rows; read and enhance them here (port the old HTL logic as needed).`;
2869
+ return `// Block: ${blockName}
2870
+ // Migrated from a classic AEM component by \`eds migrate component\`.
2871
+ // TODO: port the component's HTL/JS behaviour into this decorator.
2872
+ export default function decorate(block) {
2873
+ block.classList.add('${blockName}');
2874
+ ${body}
2875
+ }
2876
+ `;
2877
+ }
2878
+ function migratedBlockCss(blockName) {
2879
+ return `/* Block: ${blockName} */
2880
+ /* Migrated from a classic AEM component. Bring the component's styles here */
2881
+ /* (from its clientlib or the rendered page) and keep them scoped to the block. */
2882
+ .${blockName} {
2883
+ }
2884
+ `;
2885
+ }
2886
+ var init_component = __esm({
2887
+ "src/lib/migrate/component.ts"() {
2888
+ "use strict";
2889
+ init_esm_shims();
2890
+ init_dialog();
2891
+ }
2892
+ });
2893
+
2894
+ // src/lib/track.ts
2895
+ function trackSnippet(blockName, param, events) {
2896
+ const parts = [` // --- ${TRACK_MARKER} ---`];
2897
+ if (events.click) {
2898
+ parts.push(
2899
+ ` ${param}.addEventListener('click', (ev) => {`,
2900
+ " const el = ev.target.closest('a, button');",
2901
+ " if (!el) return;",
2902
+ ` trackEvent({ event: 'block_click', block: '${blockName}', label: el.dataset.track || el.textContent.trim() });`,
2903
+ " });"
2904
+ );
2905
+ }
2906
+ if (events.submit) {
2907
+ parts.push(
2908
+ ` ${param}.addEventListener('submit', () => {`,
2909
+ ` trackEvent({ event: 'form_submit', block: '${blockName}' });`,
2910
+ " });"
2911
+ );
2912
+ }
2913
+ return parts.join("\n");
2914
+ }
2915
+ function instrumentJs(js, blockName, events) {
2916
+ if (js.includes(TRACK_MARKER)) return { js, changed: false, reason: "already instrumented" };
2917
+ if (!events.click && !events.submit) return { js, changed: false, reason: "no events selected" };
2918
+ const decorate = js.match(
2919
+ /export default (?:async )?function decorate\s*\(\s*([A-Za-z_$][\w$]*)?[^)]*\)\s*\{/
2920
+ );
2921
+ if (!decorate || decorate.index === void 0) {
2922
+ return { js, changed: false, reason: "no `export default function decorate(...)` found" };
2923
+ }
2924
+ const param = decorate[1] || "block";
2925
+ let out = js;
2926
+ if (!out.includes("scripts/analytics.js")) {
2927
+ const imports = [...out.matchAll(/^import .*$/gm)];
2928
+ if (imports.length) {
2929
+ const last = imports[imports.length - 1];
2930
+ const at2 = (last.index ?? 0) + last[0].length;
2931
+ out = `${out.slice(0, at2)}
2932
+ ${IMPORT_LINE}${out.slice(at2)}`;
2933
+ } else {
2934
+ out = `${IMPORT_LINE}
2935
+
2936
+ ${out}`;
2937
+ }
2938
+ }
2939
+ const d2 = out.match(
2940
+ /export default (?:async )?function decorate\s*\(\s*([A-Za-z_$][\w$]*)?[^)]*\)\s*\{/
2941
+ );
2942
+ if (!d2 || d2.index === void 0)
2943
+ return { js: out, changed: false, reason: "decorate not found" };
2944
+ const at = d2.index + d2[0].length;
2945
+ const snippet = trackSnippet(blockName, param, events);
2946
+ out = `${out.slice(0, at)}
2947
+ ${snippet}
2948
+ ${out.slice(at)}`;
2949
+ return { js: out, changed: true };
2950
+ }
2951
+ var TRACK_MARKER, IMPORT_LINE;
2952
+ var init_track = __esm({
2953
+ "src/lib/track.ts"() {
2954
+ "use strict";
2955
+ init_esm_shims();
2956
+ TRACK_MARKER = "analytics (eds track)";
2957
+ IMPORT_LINE = "import { trackEvent } from '../../scripts/analytics.js';";
2958
+ }
2959
+ });
2960
+
2961
+ // src/commands/track.ts
2962
+ var track_exports = {};
2963
+ __export(track_exports, {
2964
+ trackBlock: () => trackBlock
2965
+ });
2966
+ import { existsSync as existsSync14 } from "fs";
2967
+ import { readFile as readFile10, writeFile as writeFile10 } from "fs/promises";
2968
+ import path17 from "path";
2969
+ import chalk6 from "chalk";
2970
+ async function trackBlock(blockName, options = {}) {
2971
+ const projectRoot = findProjectRoot();
2972
+ if (!projectRoot) {
2973
+ logger.error("Not inside an EDS project (no EDS project markers found).");
2974
+ process.exitCode = 1;
2975
+ return;
2976
+ }
2977
+ logger.logoOnce(logo("Track block"));
2978
+ const jsPath = path17.join(projectRoot, "blocks", blockName, `${blockName}.js`);
2979
+ if (!existsSync14(jsPath)) {
2980
+ logger.error(`Block "${blockName}" not found (no blocks/${blockName}/${blockName}.js).`);
2981
+ process.exitCode = 1;
2982
+ return;
2983
+ }
2984
+ const events = { click: options.click !== false, submit: options.submit !== false };
2985
+ const res = instrumentJs(await readFile10(jsPath, "utf-8"), blockName, events);
2986
+ if (!res.changed) {
2987
+ logger.warn(`Not instrumented: ${res.reason}.`);
2988
+ return;
2989
+ }
2990
+ await writeFile10(jsPath, res.js);
2991
+ logger.info(heading("Instrumented"));
2992
+ const picked = [events.click ? "block_click" : null, events.submit ? "form_submit" : null].filter(
2993
+ Boolean
2994
+ );
2995
+ logger.info(accentLine(`blocks/${blockName}/${blockName}.js`, picked.join(" + ")));
2996
+ if (!existsSync14(path17.join(projectRoot, "scripts", "analytics.js"))) {
2997
+ logger.info("");
2998
+ logger.warn("scripts/analytics.js is missing - run `eds integrate gtm` to create it.");
2999
+ }
3000
+ logger.info("");
3001
+ logger.info(chalk6.dim(" Events push to window.dataLayer via trackEvent - GTM picks them up."));
3002
+ }
3003
+ var init_track2 = __esm({
3004
+ "src/commands/track.ts"() {
3005
+ "use strict";
3006
+ init_esm_shims();
3007
+ init_logger();
3008
+ init_project();
3009
+ init_track();
3010
+ init_ui();
3011
+ }
3012
+ });
3013
+
3014
+ // src/commands/migrate.ts
3015
+ var migrate_exports = {};
3016
+ __export(migrate_exports, {
3017
+ migrateComponent: () => migrateComponent
3018
+ });
3019
+ import { existsSync as existsSync15 } from "fs";
3020
+ import { mkdir as mkdir6, writeFile as writeFile11 } from "fs/promises";
3021
+ import path18 from "path";
3022
+ function clientlibBlockCss(blockName, css, files) {
3023
+ return `/* Block: ${blockName} */
3024
+ /* Migrated from the component clientlib: ${files.join(", ")}. */
3025
+ /* TODO: review selectors and scope them under .${blockName} (and port any LESS/SCSS). */
3026
+
3027
+ ${css.trim()}
3028
+ `;
3029
+ }
3030
+ function previewFields(fields, depth = 0) {
3031
+ const lines = [];
3032
+ for (const f2 of fields) {
3033
+ const indent = " ".repeat(depth);
3034
+ if (f2.component === "tab") {
3035
+ lines.push(`${indent}${brand.blue("\u25B8")} ${f2.label ?? f2.name}`);
3036
+ } else {
3037
+ const opts = f2.options?.length ? ` (${f2.options.length} options)` : "";
3038
+ const multi = f2.multi ? " \xB7multi" : "";
3039
+ const label = f2.label ? `- ${f2.label}` : "";
3040
+ lines.push(`${indent}${brand.accent(f2.component)}${multi} ${f2.name}${opts} ${label}`);
3041
+ if (f2.fields?.length) lines.push(...previewFields(f2.fields, depth + 1));
3042
+ }
3043
+ }
3044
+ return lines;
3045
+ }
3046
+ function countLeaves(fields) {
3047
+ let n2 = 0;
3048
+ for (const f2 of fields) {
3049
+ if (f2.component === "tab") continue;
3050
+ n2 += 1;
3051
+ if (f2.fields) n2 += countLeaves(f2.fields);
3052
+ }
3053
+ return n2;
3054
+ }
3055
+ async function migrateComponent(dirArg, options = {}) {
3056
+ const projectRoot = findProjectRoot();
3057
+ if (!projectRoot) {
3058
+ logger.error("Not inside an EDS project (no EDS project markers found).");
3059
+ process.exitCode = 1;
3060
+ return;
3061
+ }
3062
+ await logger.logoOnceAnimated("Migrate component");
3063
+ const { input, confirm } = await import("@inquirer/prompts");
3064
+ const interactive = !options.yes;
3065
+ let componentDir = dirArg;
3066
+ if (!componentDir && interactive) {
3067
+ componentDir = await input({
3068
+ message: "Path to the classic component folder",
3069
+ validate: (v) => existsSync15(v.trim()) || "Folder not found"
3070
+ });
3071
+ }
3072
+ if (!componentDir) {
3073
+ logger.error("Provide the path to the classic component folder.");
3074
+ process.exitCode = 1;
3075
+ return;
3076
+ }
3077
+ componentDir = path18.resolve(componentDir.trim());
3078
+ if (!findDialogFile(componentDir)) {
3079
+ logger.error(`No Touch UI dialog (_cq_dialog/.content.xml) found in ${componentDir}.`);
3080
+ process.exitCode = 1;
3081
+ return;
3082
+ }
3083
+ const parsed = await readDialogFields(componentDir);
3084
+ if (!parsed || parsed.fields.length === 0) {
3085
+ logger.warn("Dialog found but no mappable fields were detected.");
3086
+ if (parsed?.unmapped.length)
3087
+ logger.info(`Unmapped resourceTypes: ${parsed.unmapped.join(", ")}`);
3088
+ process.exitCode = 1;
3089
+ return;
3090
+ }
3091
+ const title = await readComponentTitle(componentDir) ?? path18.basename(componentDir);
3092
+ const blockName = options.name && slug3(options.name) || (interactive ? slug3(await input({ message: "Block name", default: slug3(title) })) : slug3(title));
3093
+ logger.info(heading("Mapped fields", `${countLeaves(parsed.fields)} from dialog`));
3094
+ for (const line of previewFields(parsed.fields)) logger.info(` ${line}`);
3095
+ if (parsed.unmapped.length) {
3096
+ logger.info("");
3097
+ logger.warn(`Skipped (unrecognised): ${parsed.unmapped.join(", ")}`);
3098
+ }
3099
+ logger.info("");
3100
+ if (interactive) {
3101
+ const go = await confirm({ message: `Generate block "${blockName}"?`, default: true });
3102
+ if (!go) {
3103
+ logger.info("Cancelled.");
3104
+ return;
3105
+ }
3106
+ }
3107
+ const blockDir = path18.join(projectRoot, "blocks", blockName);
3108
+ if (existsSync15(blockDir)) {
3109
+ logger.error(
3110
+ `Block "${blockName}" already exists at blocks/${blockName}. Use --name to pick another.`
3111
+ );
3112
+ process.exitCode = 1;
3113
+ return;
3114
+ }
3115
+ const clibDir = options.clientlib ? path18.resolve(options.clientlib.trim()) : await findClientlibDir(componentDir);
3116
+ let cssContent = migratedBlockCss(blockName);
3117
+ let cssMessage = "empty - add styles from clientlib / rendered CSS";
3118
+ let preprocessedNote = [];
3119
+ if (clibDir && existsSync15(clibDir)) {
3120
+ const lib = await readClientlibCss(clibDir);
3121
+ if (lib.css.trim()) {
3122
+ cssContent = clientlibBlockCss(blockName, lib.css, lib.files);
3123
+ cssMessage = `from clientlib (${lib.files.length} file(s)) - review & scope`;
3124
+ }
3125
+ preprocessedNote = lib.preprocessed;
3126
+ }
3127
+ await mkdir6(blockDir, { recursive: true });
3128
+ await writeFile11(
3129
+ path18.join(blockDir, `${blockName}.js`),
3130
+ migratedBlockJs(blockName, parsed.fields)
3131
+ );
3132
+ await writeFile11(path18.join(blockDir, `${blockName}.css`), cssContent);
3133
+ await writeFile11(
3134
+ path18.join(blockDir, `_${blockName}.json`),
3135
+ migratedModelFile(blockName, title, parsed.fields)
3136
+ );
3137
+ logger.info(heading("Created"));
3138
+ const files = [`${blockName}.js`, `${blockName}.css`, `_${blockName}.json`];
3139
+ const w = columnWidth(files);
3140
+ logger.info(accentLine(`${blockName}.js`, "decorator stub (port HTL logic)", w));
3141
+ logger.info(accentLine(`${blockName}.css`, cssMessage, w));
3142
+ logger.info(accentLine(`_${blockName}.json`, "Universal Editor model", w));
3143
+ if (preprocessedNote.length) {
3144
+ logger.info("");
3145
+ logger.warn(`LESS/SCSS found (needs a build, not copied): ${preprocessedNote.join(", ")}`);
3146
+ }
3147
+ logger.info("");
3148
+ if (interactive) {
3149
+ const addTracking = await confirm({
3150
+ message: "Instrument this block for analytics (dataLayer)?",
3151
+ default: false
3152
+ });
3153
+ if (addTracking) {
3154
+ const { trackBlock: trackBlock2 } = await Promise.resolve().then(() => (init_track2(), track_exports));
3155
+ await trackBlock2(blockName, {});
3156
+ }
3157
+ }
3158
+ logger.info(
3159
+ box([`"${blockName}" migrated. Next: add CSS, then \`eds block preview ${blockName}\``])
3160
+ );
3161
+ }
3162
+ var slug3;
3163
+ var init_migrate = __esm({
3164
+ "src/commands/migrate.ts"() {
3165
+ "use strict";
3166
+ init_esm_shims();
3167
+ init_logger();
3168
+ init_clientlib();
3169
+ init_component();
3170
+ init_project();
3171
+ init_ui();
3172
+ slug3 = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
3173
+ }
3174
+ });
3175
+
3176
+ // src/lib/model/helpers.ts
3177
+ function n(prefix, base) {
3178
+ if (!prefix) return base;
3179
+ return prefix + base.charAt(0).toUpperCase() + base.slice(1);
3180
+ }
3181
+ var f;
3182
+ var init_helpers2 = __esm({
3183
+ "src/lib/model/helpers.ts"() {
3184
+ "use strict";
3185
+ init_esm_shims();
3186
+ f = {
3187
+ text: (name, label) => ({
3188
+ component: "text",
3189
+ valueType: "string",
3190
+ name,
3191
+ label
3192
+ }),
3193
+ textarea: (name, label) => ({
3194
+ component: "textarea",
3195
+ valueType: "string",
3196
+ name,
3197
+ label
3198
+ }),
3199
+ richtext: (name, label) => ({
3200
+ component: "richtext",
3201
+ valueType: "string",
3202
+ name,
3203
+ value: "",
3204
+ label
3205
+ }),
3206
+ number: (name, label) => ({
3207
+ component: "number",
3208
+ valueType: "number",
3209
+ name,
3210
+ label
3211
+ }),
3212
+ boolean: (name, label, value = false) => ({
3213
+ component: "boolean",
3214
+ valueType: "boolean",
3215
+ name,
3216
+ value,
3217
+ label
3218
+ }),
3219
+ dateTime: (name, label) => ({
3220
+ component: "date-time",
3221
+ valueType: "date",
3222
+ name,
3223
+ label
3224
+ }),
3225
+ select: (name, label, options, value) => ({
3226
+ component: "select",
3227
+ valueType: "string",
3228
+ name,
3229
+ label,
3230
+ options,
3231
+ ...value !== void 0 ? { value } : {}
3232
+ }),
3233
+ multiselect: (name, label, options) => ({
3234
+ component: "multiselect",
3235
+ valueType: "string",
3236
+ name,
3237
+ label,
3238
+ options
3239
+ }),
3240
+ radioGroup: (name, label, options, value) => ({
3241
+ component: "radio-group",
3242
+ valueType: "string",
3243
+ name,
3244
+ label,
3245
+ options,
3246
+ ...value !== void 0 ? { value } : {}
3247
+ }),
3248
+ checkboxGroup: (name, label, options) => ({
3249
+ component: "checkbox-group",
3250
+ valueType: "string[]",
3251
+ name,
3252
+ label,
3253
+ options
3254
+ }),
3255
+ reference: (name, label) => ({
3256
+ component: "reference",
3257
+ valueType: "string",
3258
+ name,
3259
+ label
3260
+ }),
3261
+ aemContent: (name, label) => ({ component: "aem-content", name, label }),
3262
+ aemContentFragment: (name, label) => ({
3263
+ component: "aem-content-fragment",
3264
+ name,
3265
+ label
3266
+ }),
3267
+ aemExperienceFragment: (name, label) => ({
3268
+ component: "aem-experience-fragment",
3269
+ name,
3270
+ label
3271
+ }),
3272
+ aemTag: (name, label) => ({
3273
+ component: "aem-tag",
3274
+ valueType: "string",
3275
+ name,
3276
+ label
3277
+ }),
3278
+ tab: (name, label) => ({ component: "tab", name, label }),
3279
+ container: (name, label, fields, collapsible = true) => ({
3280
+ component: "container",
3281
+ name,
3282
+ label,
3283
+ multi: true,
3284
+ collapsible,
3285
+ fields
3286
+ })
3287
+ };
3288
+ }
3289
+ });
3290
+
3291
+ // src/lib/model/partials.ts
3292
+ function partialById(id) {
3293
+ return PARTIALS.find((p) => p.id === id);
3294
+ }
3295
+ var HEADING_LEVELS, LINK_TARGETS, BUTTON_STYLES, SECTION_STYLES, heading2, eyebrow, image, richtext, link, cta, ctaGroup, classes, embed, teaser, card, PARTIALS;
3296
+ var init_partials = __esm({
3297
+ "src/lib/model/partials.ts"() {
1620
3298
  "use strict";
1621
3299
  init_esm_shims();
3300
+ init_helpers2();
3301
+ HEADING_LEVELS = [
3302
+ { name: "H1", value: "h1" },
3303
+ { name: "H2", value: "h2" },
3304
+ { name: "H3", value: "h3" },
3305
+ { name: "H4", value: "h4" },
3306
+ { name: "H5", value: "h5" },
3307
+ { name: "H6", value: "h6" }
3308
+ ];
3309
+ LINK_TARGETS = [
3310
+ { name: "Same tab", value: "_self" },
3311
+ { name: "New tab", value: "_blank" }
3312
+ ];
3313
+ BUTTON_STYLES = [
3314
+ { name: "Default", value: "" },
3315
+ { name: "Primary", value: "primary" },
3316
+ { name: "Secondary", value: "secondary" }
3317
+ ];
3318
+ SECTION_STYLES = [
3319
+ { name: "Highlight", value: "highlight" },
3320
+ { name: "Dark", value: "dark" },
3321
+ { name: "Centered", value: "centered" },
3322
+ { name: "Full width", value: "full-width" }
3323
+ ];
3324
+ heading2 = (p = "") => [
3325
+ f.text(n(p, "title"), "Title"),
3326
+ f.select(n(p, "titleType"), "Heading level", HEADING_LEVELS, "h2")
3327
+ ];
3328
+ eyebrow = (p = "") => [f.text(n(p, "eyebrow"), "Eyebrow")];
3329
+ image = (p = "") => [
3330
+ f.reference(n(p, "image"), "Image"),
3331
+ f.text(n(p, "imageAlt"), "Alt text")
3332
+ ];
3333
+ richtext = (p = "") => [f.richtext(n(p, "text"), "Text")];
3334
+ link = (p = "") => [
3335
+ f.text(n(p, "linkText"), "Link text"),
3336
+ f.aemContent(n(p, "link"), "Link"),
3337
+ f.select(n(p, "linkTarget"), "Open in", LINK_TARGETS, "_self")
3338
+ ];
3339
+ cta = (p = "") => [
3340
+ ...link(p),
3341
+ f.select(n(p, "ctaStyle"), "Style", BUTTON_STYLES, "")
3342
+ ];
3343
+ ctaGroup = (p = "") => [
3344
+ f.container(n(p, "ctas") || "ctas", "Buttons", cta())
3345
+ ];
3346
+ classes = (p = "") => [
3347
+ f.multiselect(n(p, "classes"), "Styles", SECTION_STYLES)
3348
+ ];
3349
+ embed = (p = "") => [f.aemContent(n(p, "url"), "URL (video / embed)")];
3350
+ teaser = (p = "") => [
3351
+ ...image(p),
3352
+ ...eyebrow(p),
3353
+ ...heading2(p),
3354
+ ...richtext(p),
3355
+ ...cta(p)
3356
+ ];
3357
+ card = (p = "") => [
3358
+ ...image(p),
3359
+ f.text(n(p, "title"), "Title"),
3360
+ f.richtext(n(p, "description"), "Description"),
3361
+ ...link(p)
3362
+ ];
3363
+ PARTIALS = [
3364
+ { id: "heading", label: "Heading (title + level)", build: heading2 },
3365
+ { id: "eyebrow", label: "Eyebrow (kicker line)", build: eyebrow },
3366
+ { id: "image", label: "Image (reference + alt)", build: image },
3367
+ { id: "richtext", label: "Rich text body", build: richtext },
3368
+ { id: "link", label: "Link (text + url + target)", build: link },
3369
+ { id: "cta", label: "CTA (link + style)", build: cta },
3370
+ { id: "cta-group", label: "CTA group (repeatable buttons)", build: ctaGroup },
3371
+ { id: "classes", label: "Section styles (CSS modifiers)", build: classes },
3372
+ { id: "embed", label: "Embed / video (URL)", build: embed },
3373
+ { id: "teaser", label: "Teaser (image + eyebrow + title + text + CTA)", build: teaser },
3374
+ { id: "card", label: "Card (image + title + description + link)", build: card }
3375
+ ];
1622
3376
  }
1623
3377
  });
1624
3378
 
1625
- // src/lib/migrate/component.ts
1626
- import { existsSync as existsSync6 } from "fs";
1627
- import { readFile as readFile4 } from "fs/promises";
1628
- import path8 from "path";
1629
- import { XMLParser as XMLParser2 } from "fast-xml-parser";
1630
- function findDialogFile(componentDir) {
1631
- const candidates = [
1632
- path8.join(componentDir, "_cq_dialog", ".content.xml"),
1633
- path8.join(componentDir, "_cq_dialog.xml"),
1634
- path8.join(componentDir, "cq:dialog", ".content.xml")
1635
- ];
1636
- return candidates.find((c) => existsSync6(c)) ?? null;
1637
- }
1638
- async function readComponentTitle(componentDir) {
1639
- const file = path8.join(componentDir, ".content.xml");
1640
- if (!existsSync6(file)) return void 0;
3379
+ // src/commands/model.ts
3380
+ var model_exports = {};
3381
+ __export(model_exports, {
3382
+ modelAdd: () => modelAdd
3383
+ });
3384
+ import { existsSync as existsSync16 } from "fs";
3385
+ import { readFile as readFile11, writeFile as writeFile12 } from "fs/promises";
3386
+ import path19 from "path";
3387
+ async function modelAdd(blockArg, partialArgs, options = {}) {
3388
+ const projectRoot = findProjectRoot();
3389
+ if (!projectRoot) {
3390
+ logger.error("Not inside an EDS project (no EDS project markers found).");
3391
+ process.exitCode = 1;
3392
+ return;
3393
+ }
3394
+ logger.logoOnce(logo("Model - add partials"));
3395
+ const { select, checkbox } = await import("@inquirer/prompts");
3396
+ const interactive = !options.yes;
3397
+ let blockName = blockArg;
3398
+ if (!blockName && interactive) {
3399
+ const { readdir: readdir9 } = await import("fs/promises");
3400
+ const blocksDir = path19.join(projectRoot, "blocks");
3401
+ const dirs = existsSync16(blocksDir) ? (await readdir9(blocksDir, { withFileTypes: true })).filter(
3402
+ (e) => e.isDirectory() && existsSync16(path19.join(blocksDir, e.name, `_${e.name}.json`))
3403
+ ).map((e) => e.name) : [];
3404
+ if (dirs.length === 0) {
3405
+ logger.error("No block with a _<name>.json model found. Create one with `eds block create`.");
3406
+ process.exitCode = 1;
3407
+ return;
3408
+ }
3409
+ blockName = await select({
3410
+ message: "Which block's model?",
3411
+ choices: dirs.map((d) => ({ name: d, value: d }))
3412
+ });
3413
+ }
3414
+ if (!blockName) {
3415
+ logger.error("Provide a block name.");
3416
+ process.exitCode = 1;
3417
+ return;
3418
+ }
3419
+ const modelFile = path19.join(projectRoot, "blocks", blockName, `_${blockName}.json`);
3420
+ if (!existsSync16(modelFile)) {
3421
+ logger.error(`No model at blocks/${blockName}/_${blockName}.json. Create the block first.`);
3422
+ process.exitCode = 1;
3423
+ return;
3424
+ }
3425
+ let ids = partialArgs;
3426
+ if (ids.length === 0 && interactive) {
3427
+ ids = await checkbox({
3428
+ message: "Which partials to add?",
3429
+ choices: PARTIALS.map((p) => ({ name: p.label, value: p.id }))
3430
+ });
3431
+ }
3432
+ const partials = ids.map((id) => partialById(id.trim())).filter((p) => !!p);
3433
+ const unknown = ids.filter((id) => !partialById(id.trim()));
3434
+ if (unknown.length) {
3435
+ logger.warn(
3436
+ `Unknown partial(s): ${unknown.join(", ")}. Available: ${PARTIALS.map((p) => p.id).join(", ")}`
3437
+ );
3438
+ }
3439
+ if (partials.length === 0) {
3440
+ logger.info("Nothing to add.");
3441
+ return;
3442
+ }
3443
+ let model2;
1641
3444
  try {
1642
- const parser = new XMLParser2({ ignoreAttributes: false, attributeNamePrefix: "@_" });
1643
- const parsed = parser.parse(await readFile4(file, "utf-8"));
1644
- const root = parsed["jcr:root"] ?? {};
1645
- const title = root["@_jcr:title"];
1646
- return typeof title === "string" ? title.replace(/^\{[^}]+\}/, "") : void 0;
3445
+ model2 = JSON.parse(await readFile11(modelFile, "utf-8"));
1647
3446
  } catch {
1648
- return void 0;
3447
+ logger.error(`Could not parse ${modelFile}.`);
3448
+ process.exitCode = 1;
3449
+ return;
1649
3450
  }
1650
- }
1651
- async function readDialogFields(componentDir) {
1652
- const dialog = findDialogFile(componentDir);
1653
- if (!dialog) return null;
1654
- return parseDialog(await readFile4(dialog, "utf-8"));
1655
- }
1656
- function migratedModelFile(blockName, title, fields) {
1657
- const model = {
1658
- definitions: [
1659
- {
1660
- title,
1661
- id: blockName,
1662
- plugins: {
1663
- xwalk: {
1664
- page: {
1665
- resourceType: "core/franklin/components/block/v1/block",
1666
- template: { name: title, model: blockName }
1667
- }
1668
- }
1669
- }
3451
+ const models = Array.isArray(model2.models) ? model2.models : [];
3452
+ const target = models.find((m) => m.id === blockName) ?? models[0];
3453
+ if (!target) {
3454
+ logger.error("Model has no `models[]` entry to add fields to.");
3455
+ process.exitCode = 1;
3456
+ return;
3457
+ }
3458
+ target.fields = Array.isArray(target.fields) ? target.fields : [];
3459
+ const existing = new Set(target.fields.map((x) => x.name));
3460
+ const added = [];
3461
+ const skipped = [];
3462
+ for (const p of partials) {
3463
+ for (const field of p.build()) {
3464
+ if (existing.has(field.name)) {
3465
+ skipped.push(field.name);
3466
+ continue;
1670
3467
  }
1671
- ],
1672
- models: [{ id: blockName, fields }],
1673
- filters: []
1674
- };
1675
- return `${JSON.stringify(model, null, 2)}
1676
- `;
1677
- }
1678
- function migratedBlockJs(blockName, fields) {
1679
- const hasContainer = fields.some((f) => f.component === "container" && f.multi);
1680
- const body = hasContainer ? ` // This block was migrated from a classic AEM component with a multifield.
1681
- // Each child row is one repeated item - enhance them here.
1682
- [...block.children].forEach((row) => {
1683
- row.classList.add('${blockName}-item');
1684
- });` : ` // Migrated from a classic AEM component. The authored fields arrive as
1685
- // rows; read and enhance them here (port the old HTL logic as needed).`;
1686
- return `// Block: ${blockName}
1687
- // Migrated from a classic AEM component by \`eds migrate component\`.
1688
- // TODO: port the component's HTL/JS behaviour into this decorator.
1689
- export default function decorate(block) {
1690
- block.classList.add('${blockName}');
1691
- ${body}
1692
- }
1693
- `;
1694
- }
1695
- function migratedBlockCss(blockName) {
1696
- return `/* Block: ${blockName} */
1697
- /* Migrated from a classic AEM component. Bring the component's styles here */
1698
- /* (from its clientlib or the rendered page) and keep them scoped to the block. */
1699
- .${blockName} {
1700
- }
1701
- `;
3468
+ existing.add(field.name);
3469
+ target.fields.push(field);
3470
+ added.push(field.name);
3471
+ }
3472
+ }
3473
+ await writeFile12(modelFile, `${JSON.stringify(model2, null, 2)}
3474
+ `);
3475
+ logger.info(heading("Model updated", `blocks/${blockName}/_${blockName}.json`));
3476
+ logger.info(accentLine("partials", partials.map((p) => p.id).join(", ")));
3477
+ logger.info(accentLine("fields added", added.length ? added.join(", ") : "(none)"));
3478
+ if (skipped.length) logger.info(statusLine("warn", "skipped (exists)", skipped.join(", ")));
3479
+ logger.info("");
3480
+ logger.info(box([`${added.length} field(s) added to "${blockName}"`]));
1702
3481
  }
1703
- var init_component = __esm({
1704
- "src/lib/migrate/component.ts"() {
3482
+ var init_model = __esm({
3483
+ "src/commands/model.ts"() {
1705
3484
  "use strict";
1706
3485
  init_esm_shims();
1707
- init_dialog();
3486
+ init_logger();
3487
+ init_partials();
3488
+ init_project();
3489
+ init_ui();
1708
3490
  }
1709
3491
  });
1710
3492
 
1711
- // src/lib/track.ts
1712
- function trackSnippet(blockName, param, events) {
1713
- const parts = [` // --- ${TRACK_MARKER} ---`];
1714
- if (events.click) {
1715
- parts.push(
1716
- ` ${param}.addEventListener('click', (ev) => {`,
1717
- " const el = ev.target.closest('a, button');",
1718
- " if (!el) return;",
1719
- ` trackEvent({ event: 'block_click', block: '${blockName}', label: el.dataset.track || el.textContent.trim() });`,
1720
- " });"
1721
- );
1722
- }
1723
- if (events.submit) {
1724
- parts.push(
1725
- ` ${param}.addEventListener('submit', () => {`,
1726
- ` trackEvent({ event: 'form_submit', block: '${blockName}' });`,
1727
- " });"
1728
- );
1729
- }
1730
- return parts.join("\n");
3493
+ // src/lib/schema/jsonld.ts
3494
+ function schemaTypeById(id) {
3495
+ return SCHEMA_TYPES.find((t) => t.id.toLowerCase() === id.toLowerCase());
1731
3496
  }
1732
- function instrumentJs(js, blockName, events) {
1733
- if (js.includes(TRACK_MARKER)) return { js, changed: false, reason: "already instrumented" };
1734
- if (!events.click && !events.submit) return { js, changed: false, reason: "no events selected" };
3497
+ function injectJsonLd(js, type) {
3498
+ if (js.includes(SCHEMA_MARKER)) return { js, changed: false, reason: "already has JSON-LD" };
1735
3499
  const decorate = js.match(
1736
3500
  /export default (?:async )?function decorate\s*\(\s*([A-Za-z_$][\w$]*)?[^)]*\)\s*\{/
1737
3501
  );
@@ -1739,254 +3503,404 @@ function instrumentJs(js, blockName, events) {
1739
3503
  return { js, changed: false, reason: "no `export default function decorate(...)` found" };
1740
3504
  }
1741
3505
  const param = decorate[1] || "block";
1742
- let out = js;
1743
- if (!out.includes("scripts/analytics.js")) {
1744
- const imports = [...out.matchAll(/^import .*$/gm)];
1745
- if (imports.length) {
1746
- const last = imports[imports.length - 1];
1747
- const at2 = (last.index ?? 0) + last[0].length;
1748
- out = `${out.slice(0, at2)}
1749
- ${IMPORT_LINE}${out.slice(at2)}`;
1750
- } else {
1751
- out = `${IMPORT_LINE}
1752
-
1753
- ${out}`;
1754
- }
3506
+ const at = decorate.index + decorate[0].length;
3507
+ const snippet = `
3508
+ // --- ${SCHEMA_MARKER}: ${type.id}) ---
3509
+ {
3510
+ const root = ${param};
3511
+ const ld = ${type.object};
3512
+ const ldScript = document.createElement('script');
3513
+ ldScript.type = 'application/ld+json';
3514
+ ldScript.textContent = JSON.stringify(ld);
3515
+ document.head.append(ldScript);
1755
3516
  }
1756
- const d2 = out.match(
1757
- /export default (?:async )?function decorate\s*\(\s*([A-Za-z_$][\w$]*)?[^)]*\)\s*\{/
1758
- );
1759
- if (!d2 || d2.index === void 0)
1760
- return { js: out, changed: false, reason: "decorate not found" };
1761
- const at = d2.index + d2[0].length;
1762
- const snippet = trackSnippet(blockName, param, events);
1763
- out = `${out.slice(0, at)}
1764
- ${snippet}
1765
- ${out.slice(at)}`;
1766
- return { js: out, changed: true };
3517
+ `;
3518
+ return { js: `${js.slice(0, at)}${snippet}${js.slice(at)}`, changed: true };
1767
3519
  }
1768
- var TRACK_MARKER, IMPORT_LINE;
1769
- var init_track = __esm({
1770
- "src/lib/track.ts"() {
3520
+ var SCHEMA_MARKER, SCHEMA_TYPES;
3521
+ var init_jsonld = __esm({
3522
+ "src/lib/schema/jsonld.ts"() {
1771
3523
  "use strict";
1772
3524
  init_esm_shims();
1773
- TRACK_MARKER = "analytics (eds track)";
1774
- IMPORT_LINE = "import { trackEvent } from '../../scripts/analytics.js';";
3525
+ SCHEMA_MARKER = "json-ld (eds schema";
3526
+ SCHEMA_TYPES = [
3527
+ {
3528
+ id: "Article",
3529
+ label: "Article / BlogPosting",
3530
+ object: `{
3531
+ '@context': 'https://schema.org',
3532
+ '@type': 'Article',
3533
+ headline: root.querySelector('h1, h2, h3')?.textContent?.trim() || document.title,
3534
+ image: [...root.querySelectorAll('img')].map((img) => img.src),
3535
+ description: root.querySelector('p')?.textContent?.trim(),
3536
+ url: window.location.href,
3537
+ // TODO: author, datePublished, dateModified
3538
+ }`
3539
+ },
3540
+ {
3541
+ id: "FAQPage",
3542
+ label: "FAQ page",
3543
+ object: `{
3544
+ '@context': 'https://schema.org',
3545
+ '@type': 'FAQPage',
3546
+ mainEntity: [...root.children].map((row) => {
3547
+ const [q, a] = row.children;
3548
+ return {
3549
+ '@type': 'Question',
3550
+ name: q?.textContent?.trim(),
3551
+ acceptedAnswer: { '@type': 'Answer', text: a?.innerHTML?.trim() },
3552
+ };
3553
+ }),
3554
+ }`
3555
+ },
3556
+ {
3557
+ id: "BreadcrumbList",
3558
+ label: "Breadcrumb list",
3559
+ object: `{
3560
+ '@context': 'https://schema.org',
3561
+ '@type': 'BreadcrumbList',
3562
+ itemListElement: [...root.querySelectorAll('a')].map((a, i) => ({
3563
+ '@type': 'ListItem',
3564
+ position: i + 1,
3565
+ name: a.textContent?.trim(),
3566
+ item: a.href,
3567
+ })),
3568
+ }`
3569
+ },
3570
+ {
3571
+ id: "Product",
3572
+ label: "Product",
3573
+ object: `{
3574
+ '@context': 'https://schema.org',
3575
+ '@type': 'Product',
3576
+ name: root.querySelector('h1, h2, h3')?.textContent?.trim(),
3577
+ image: [...root.querySelectorAll('img')].map((img) => img.src),
3578
+ description: root.querySelector('p')?.textContent?.trim(),
3579
+ // TODO: brand, sku, offers { '@type': 'Offer', price, priceCurrency, availability }
3580
+ }`
3581
+ },
3582
+ {
3583
+ id: "Organization",
3584
+ label: "Organization",
3585
+ object: `{
3586
+ '@context': 'https://schema.org',
3587
+ '@type': 'Organization',
3588
+ name: document.title,
3589
+ url: window.location.origin,
3590
+ logo: root.querySelector('img')?.src,
3591
+ // TODO: sameAs: [social profile URLs], contactPoint
3592
+ }`
3593
+ },
3594
+ {
3595
+ id: "LocalBusiness",
3596
+ label: "Local business",
3597
+ object: `{
3598
+ '@context': 'https://schema.org',
3599
+ '@type': 'LocalBusiness',
3600
+ name: root.querySelector('h1, h2, h3')?.textContent?.trim() || document.title,
3601
+ image: root.querySelector('img')?.src,
3602
+ url: window.location.origin,
3603
+ // TODO: address (PostalAddress), telephone, openingHours, geo
3604
+ }`
3605
+ },
3606
+ {
3607
+ id: "Event",
3608
+ label: "Event",
3609
+ object: `{
3610
+ '@context': 'https://schema.org',
3611
+ '@type': 'Event',
3612
+ name: root.querySelector('h1, h2, h3')?.textContent?.trim(),
3613
+ image: [...root.querySelectorAll('img')].map((img) => img.src),
3614
+ url: window.location.href,
3615
+ // TODO: startDate, endDate, location (Place), offers
3616
+ }`
3617
+ },
3618
+ {
3619
+ id: "VideoObject",
3620
+ label: "Video",
3621
+ object: `{
3622
+ '@context': 'https://schema.org',
3623
+ '@type': 'VideoObject',
3624
+ name: root.querySelector('h1, h2, h3')?.textContent?.trim() || document.title,
3625
+ thumbnailUrl: root.querySelector('img')?.src,
3626
+ contentUrl: root.querySelector('a[href], video source[src]')?.href,
3627
+ // TODO: description, uploadDate, duration (ISO 8601)
3628
+ }`
3629
+ },
3630
+ {
3631
+ id: "Recipe",
3632
+ label: "Recipe",
3633
+ object: `{
3634
+ '@context': 'https://schema.org',
3635
+ '@type': 'Recipe',
3636
+ name: root.querySelector('h1, h2, h3')?.textContent?.trim(),
3637
+ image: [...root.querySelectorAll('img')].map((img) => img.src),
3638
+ recipeIngredient: [...root.querySelectorAll('li')].map((li) => li.textContent?.trim()),
3639
+ // TODO: recipeInstructions, prepTime, cookTime, nutrition
3640
+ }`
3641
+ },
3642
+ {
3643
+ id: "HowTo",
3644
+ label: "How-to",
3645
+ object: `{
3646
+ '@context': 'https://schema.org',
3647
+ '@type': 'HowTo',
3648
+ name: root.querySelector('h1, h2, h3')?.textContent?.trim(),
3649
+ step: [...root.querySelectorAll('li')].map((li, i) => ({
3650
+ '@type': 'HowToStep',
3651
+ position: i + 1,
3652
+ text: li.textContent?.trim(),
3653
+ })),
3654
+ }`
3655
+ }
3656
+ ];
1775
3657
  }
1776
3658
  });
1777
3659
 
1778
- // src/commands/track.ts
1779
- var track_exports = {};
1780
- __export(track_exports, {
1781
- trackBlock: () => trackBlock
3660
+ // src/commands/schema.ts
3661
+ var schema_exports = {};
3662
+ __export(schema_exports, {
3663
+ schemaBlock: () => schemaBlock
1782
3664
  });
1783
- import { existsSync as existsSync7 } from "fs";
1784
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
1785
- import path9 from "path";
1786
- import chalk4 from "chalk";
1787
- async function trackBlock(blockName, options = {}) {
3665
+ import { existsSync as existsSync17 } from "fs";
3666
+ import { readFile as readFile12, writeFile as writeFile13 } from "fs/promises";
3667
+ import path20 from "path";
3668
+ import chalk7 from "chalk";
3669
+ async function schemaBlock(blockName, options = {}) {
1788
3670
  const projectRoot = findProjectRoot();
1789
3671
  if (!projectRoot) {
1790
3672
  logger.error("Not inside an EDS project (no EDS project markers found).");
1791
3673
  process.exitCode = 1;
1792
3674
  return;
1793
3675
  }
1794
- logger.logoOnce(logo("Track block"));
1795
- const jsPath = path9.join(projectRoot, "blocks", blockName, `${blockName}.js`);
1796
- if (!existsSync7(jsPath)) {
3676
+ logger.logoOnce(logo("Schema - JSON-LD"));
3677
+ const jsPath = path20.join(projectRoot, "blocks", blockName, `${blockName}.js`);
3678
+ if (!existsSync17(jsPath)) {
1797
3679
  logger.error(`Block "${blockName}" not found (no blocks/${blockName}/${blockName}.js).`);
1798
3680
  process.exitCode = 1;
1799
3681
  return;
1800
3682
  }
1801
- const events = { click: options.click !== false, submit: options.submit !== false };
1802
- const res = instrumentJs(await readFile5(jsPath, "utf-8"), blockName, events);
3683
+ let typeId = options.type;
3684
+ if (!typeId && !options.yes) {
3685
+ const { select } = await import("@inquirer/prompts");
3686
+ typeId = await select({
3687
+ message: "Which schema.org type?",
3688
+ choices: SCHEMA_TYPES.map((t) => ({ name: `${t.id} - ${t.label}`, value: t.id }))
3689
+ });
3690
+ }
3691
+ const type = typeId ? schemaTypeById(typeId) : void 0;
3692
+ if (!type) {
3693
+ logger.error(
3694
+ `Unknown schema type "${typeId}". Options: ${SCHEMA_TYPES.map((t) => t.id).join(", ")}`
3695
+ );
3696
+ process.exitCode = 1;
3697
+ return;
3698
+ }
3699
+ const res = injectJsonLd(await readFile12(jsPath, "utf-8"), type);
1803
3700
  if (!res.changed) {
1804
- logger.warn(`Not instrumented: ${res.reason}.`);
3701
+ logger.warn(`Not added: ${res.reason}.`);
1805
3702
  return;
1806
3703
  }
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
3704
+ await writeFile13(jsPath, res.js);
3705
+ logger.info(heading("JSON-LD added"));
3706
+ logger.info(accentLine(`blocks/${blockName}/${blockName}.js`, `${type.id} (${type.label})`));
3707
+ logger.info("");
3708
+ logger.info(
3709
+ chalk7.dim(
3710
+ " Review the generated object - some fields are heuristic or TODO. Validate at search.google.com/test/rich-results."
3711
+ )
1811
3712
  );
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.");
3713
+ }
3714
+ var init_schema = __esm({
3715
+ "src/commands/schema.ts"() {
3716
+ "use strict";
3717
+ init_esm_shims();
3718
+ init_logger();
3719
+ init_project();
3720
+ init_jsonld();
3721
+ init_ui();
1816
3722
  }
1817
- logger.info("");
1818
- logger.info(chalk4.dim(" Events push to window.dataLayer via trackEvent - GTM picks them up."));
3723
+ });
3724
+
3725
+ // src/lib/ue/check.ts
3726
+ function metaContent(html, name) {
3727
+ for (const tag of html.match(/<meta\b[^>]*>/gi) ?? []) {
3728
+ if (tag.match(/\bname=["']([^"']+)["']/i)?.[1] === name) {
3729
+ return tag.match(/\bcontent=["']([^"']*)["']/i)?.[1] ?? "";
3730
+ }
3731
+ }
3732
+ return null;
3733
+ }
3734
+ function hasAemConnection(html) {
3735
+ return metaContent(html, AEM_CONNECTION_META) !== null;
1819
3736
  }
1820
- var init_track2 = __esm({
1821
- "src/commands/track.ts"() {
3737
+ function modelIds(componentModels) {
3738
+ if (!Array.isArray(componentModels)) return [];
3739
+ return componentModels.map((m) => m && typeof m === "object" ? m.id : void 0).filter((id) => typeof id === "string");
3740
+ }
3741
+ function assessReadiness(input) {
3742
+ const isUeProject = !!input.configPresent["component-definition.json"] && !!input.configPresent["component-models.json"];
3743
+ const ids = new Set(input.modelIds);
3744
+ const blocks = input.blocks.map((b) => {
3745
+ const editable = b.hasModelFile || ids.has(b.name);
3746
+ return {
3747
+ name: b.name,
3748
+ editable,
3749
+ reason: editable ? void 0 : "no UE model (_<name>.json or component-models entry)"
3750
+ };
3751
+ });
3752
+ return { isUeProject, blocks };
3753
+ }
3754
+ var UE_CONFIG_FILES, AEM_CONNECTION_META;
3755
+ var init_check = __esm({
3756
+ "src/lib/ue/check.ts"() {
1822
3757
  "use strict";
1823
3758
  init_esm_shims();
1824
- init_logger();
1825
- init_project();
1826
- init_track();
1827
- init_ui();
3759
+ UE_CONFIG_FILES = [
3760
+ "component-definition.json",
3761
+ "component-models.json",
3762
+ "component-filters.json"
3763
+ ];
3764
+ AEM_CONNECTION_META = "urn:adobe:aue:system:aemconnection";
1828
3765
  }
1829
3766
  });
1830
3767
 
1831
- // src/commands/migrate.ts
1832
- var migrate_exports = {};
1833
- __export(migrate_exports, {
1834
- migrateComponent: () => migrateComponent
3768
+ // src/commands/ue.ts
3769
+ var ue_exports = {};
3770
+ __export(ue_exports, {
3771
+ ueCheck: () => ueCheck,
3772
+ ueOpen: () => ueOpen
1835
3773
  });
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
- `;
3774
+ import { existsSync as existsSync18 } from "fs";
3775
+ import { readFile as readFile13, readdir as readdir3 } from "fs/promises";
3776
+ import path21 from "path";
3777
+ import chalk8 from "chalk";
3778
+ async function readJson2(file) {
3779
+ if (!existsSync18(file)) return null;
3780
+ try {
3781
+ return JSON.parse(await readFile13(file, "utf-8"));
3782
+ } catch {
3783
+ return null;
3784
+ }
1846
3785
  }
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
- }
3786
+ async function scanBlocks(root) {
3787
+ const blocksDir = path21.join(root, "blocks");
3788
+ if (!existsSync18(blocksDir)) return [];
3789
+ const entries = await readdir3(blocksDir, { withFileTypes: true });
3790
+ return entries.filter((e) => e.isDirectory()).map((e) => ({
3791
+ name: e.name,
3792
+ hasModelFile: existsSync18(path21.join(blocksDir, e.name, `_${e.name}.json`))
3793
+ }));
3794
+ }
3795
+ async function openUrl2(url) {
3796
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3797
+ try {
3798
+ const { execa } = await import("execa");
3799
+ await execa(cmd, [url], { stdio: "ignore" });
3800
+ } catch {
1860
3801
  }
1861
- return lines;
1862
3802
  }
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);
3803
+ async function gitRemote(root) {
3804
+ try {
3805
+ const { execa } = await import("execa");
3806
+ const res = await execa("git", ["remote", "get-url", "origin"], { cwd: root });
3807
+ return res.stdout.trim() || null;
3808
+ } catch {
3809
+ return null;
1869
3810
  }
1870
- return n;
1871
3811
  }
1872
- async function migrateComponent(dirArg, options = {}) {
1873
- const projectRoot = findProjectRoot();
1874
- if (!projectRoot) {
3812
+ async function ueCheck() {
3813
+ const root = findProjectRoot();
3814
+ if (!root) {
1875
3815
  logger.error("Not inside an EDS project (no EDS project markers found).");
1876
3816
  process.exitCode = 1;
1877
3817
  return;
1878
3818
  }
1879
- await logger.logoOnceAnimated("Migrate component");
1880
- const { input, confirm } = await import("@inquirer/prompts");
1881
- const interactive = !options.yes;
1882
- let componentDir = dirArg;
1883
- if (!componentDir && interactive) {
1884
- componentDir = await input({
1885
- message: "Path to the classic component folder",
1886
- validate: (v) => existsSync8(v.trim()) || "Folder not found"
1887
- });
1888
- }
1889
- if (!componentDir) {
1890
- logger.error("Provide the path to the classic component folder.");
1891
- process.exitCode = 1;
1892
- return;
1893
- }
1894
- componentDir = path10.resolve(componentDir.trim());
1895
- if (!findDialogFile(componentDir)) {
1896
- logger.error(`No Touch UI dialog (_cq_dialog/.content.xml) found in ${componentDir}.`);
1897
- process.exitCode = 1;
1898
- return;
1899
- }
1900
- const parsed = await readDialogFields(componentDir);
1901
- if (!parsed || parsed.fields.length === 0) {
1902
- logger.warn("Dialog found but no mappable fields were detected.");
1903
- if (parsed?.unmapped.length)
1904
- logger.info(`Unmapped resourceTypes: ${parsed.unmapped.join(", ")}`);
1905
- process.exitCode = 1;
1906
- return;
3819
+ logger.logoOnce(logo("Universal Editor - readiness"));
3820
+ const configPresent = {};
3821
+ for (const f2 of UE_CONFIG_FILES) configPresent[f2] = existsSync18(path21.join(root, f2));
3822
+ const componentModels = await readJson2(path21.join(root, "component-models.json"));
3823
+ const blocks = await scanBlocks(root);
3824
+ const { isUeProject, blocks: blockReadiness } = assessReadiness({
3825
+ configPresent,
3826
+ blocks,
3827
+ modelIds: modelIds(componentModels)
3828
+ });
3829
+ logger.info(heading("Universal Editor config"));
3830
+ const names = [...UE_CONFIG_FILES];
3831
+ const w = columnWidth(names);
3832
+ for (const f2 of names) {
3833
+ logger.info(statusLine(configPresent[f2] ? "pass" : "fail", f2, void 0, w));
1907
3834
  }
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) {
3835
+ if (!isUeProject) {
1913
3836
  logger.info("");
1914
- logger.warn(`Skipped (unrecognised): ${parsed.unmapped.join(", ")}`);
1915
- }
1916
- logger.info("");
1917
- if (interactive) {
1918
- const go = await confirm({ message: `Generate block "${blockName}"?`, default: true });
1919
- if (!go) {
1920
- logger.info("Cancelled.");
1921
- return;
1922
- }
1923
- }
1924
- const blockDir = path10.join(projectRoot, "blocks", blockName);
1925
- if (existsSync8(blockDir)) {
1926
- logger.error(
1927
- `Block "${blockName}" already exists at blocks/${blockName}. Use --name to pick another.`
3837
+ logger.warn(
3838
+ "This doesn't look like a Universal Editor (crosswalk) project - run `eds scaffold ue` first."
1928
3839
  );
1929
3840
  process.exitCode = 1;
1930
3841
  return;
1931
3842
  }
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;
3843
+ const headFile = path21.join(root, "head.html");
3844
+ if (existsSync18(headFile)) {
3845
+ const connected = hasAemConnection(await readFile13(headFile, "utf-8"));
3846
+ logger.info(
3847
+ statusLine(
3848
+ connected ? "pass" : "info",
3849
+ "head.html",
3850
+ connected ? "aemconnection present" : "no aemconnection (fine for cloud author)"
3851
+ )
3852
+ );
1943
3853
  }
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(", ")}`);
3854
+ logger.info(heading("Blocks", `${blockReadiness.length} found`));
3855
+ if (blockReadiness.length === 0) {
3856
+ logger.info(chalk8.dim(" No blocks yet."));
3857
+ } else {
3858
+ const bw = columnWidth(blockReadiness.map((b) => b.name));
3859
+ for (const b of blockReadiness) {
3860
+ logger.info(statusLine(b.editable ? "pass" : "warn", b.name, b.reason, bw));
3861
+ }
1963
3862
  }
3863
+ const editable = blockReadiness.filter((b) => b.editable).length;
3864
+ const notEditable = blockReadiness.length - editable;
1964
3865
  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, {});
3866
+ logger.info(summary({ pass: editable, warn: notEditable }));
3867
+ logger.info(chalk8.dim(" Open a page in the editor with `eds ue open <path>`."));
3868
+ }
3869
+ async function ueOpen(pagePath, options = {}) {
3870
+ const root = findProjectRoot() ?? process.cwd();
3871
+ const pPath = pagePath ?? options.path ?? "/";
3872
+ let host = options.url;
3873
+ if (!host) {
3874
+ const remote = await gitRemote(root);
3875
+ const parsed = remote ? parseRemote(remote) : null;
3876
+ if (!parsed) {
3877
+ logger.error("Couldn't determine the preview host from git.");
3878
+ logger.info(
3879
+ chalk8.dim(
3880
+ " Pass it explicitly: `eds ue open <path> --url main--<repo>--<owner>.aem.page`"
3881
+ )
3882
+ );
3883
+ process.exitCode = 1;
3884
+ return;
1973
3885
  }
3886
+ host = previewHost(parsed.owner, parsed.repo, options.ref ?? "main");
1974
3887
  }
1975
- logger.info(
1976
- box([`"${blockName}" migrated. Next: add CSS, then \`eds block preview ${blockName}\``])
1977
- );
3888
+ const link2 = editorLink({ host, path: pPath, org: options.org });
3889
+ logger.info(heading("Universal Editor"));
3890
+ logger.info(accentLine("page", `${host}${pPath.startsWith("/") ? "" : "/"}${pPath}`));
3891
+ logger.info(accentLine("open", link2));
3892
+ if (options.open !== false) await openUrl2(link2);
3893
+ else logger.info(chalk8.dim(" (use without --no-open to launch the browser)"));
1978
3894
  }
1979
- var slug2;
1980
- var init_migrate = __esm({
1981
- "src/commands/migrate.ts"() {
3895
+ var init_ue3 = __esm({
3896
+ "src/commands/ue.ts"() {
1982
3897
  "use strict";
1983
3898
  init_esm_shims();
1984
3899
  init_logger();
1985
- init_clientlib();
1986
- init_component();
1987
3900
  init_project();
3901
+ init_check();
3902
+ init_link();
1988
3903
  init_ui();
1989
- slug2 = (s) => s.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1990
3904
  }
1991
3905
  });
1992
3906
 
@@ -1996,16 +3910,16 @@ __export(create_exports, {
1996
3910
  createBlock: () => createBlock,
1997
3911
  ueModelFor: () => ueModelFor
1998
3912
  });
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";
3913
+ import { existsSync as existsSync19 } from "fs";
3914
+ import { mkdir as mkdir7, writeFile as writeFile14 } from "fs/promises";
3915
+ import path22 from "path";
3916
+ import chalk9 from "chalk";
2003
3917
  function titleCase(name) {
2004
3918
  return name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2005
3919
  }
2006
3920
  function ueModelFor(name) {
2007
3921
  const title = titleCase(name);
2008
- const model = {
3922
+ const model2 = {
2009
3923
  definitions: [
2010
3924
  {
2011
3925
  title,
@@ -2058,7 +3972,7 @@ function ueModelFor(name) {
2058
3972
  ],
2059
3973
  filters: []
2060
3974
  };
2061
- return `${JSON.stringify(model, null, 2)}
3975
+ return `${JSON.stringify(model2, null, 2)}
2062
3976
  `;
2063
3977
  }
2064
3978
  async function createBlock(name, options = {}) {
@@ -2077,13 +3991,13 @@ async function createBlock(name, options = {}) {
2077
3991
  process.exitCode = 1;
2078
3992
  return;
2079
3993
  }
2080
- const blocksDir = path11.join(projectRoot, "blocks", name);
2081
- if (existsSync9(blocksDir)) {
3994
+ const blocksDir = path22.join(projectRoot, "blocks", name);
3995
+ if (existsSync19(blocksDir)) {
2082
3996
  logger.error(`Block "${name}" already exists at ${blocksDir}`);
2083
3997
  process.exitCode = 1;
2084
3998
  return;
2085
3999
  }
2086
- await mkdir4(blocksDir, { recursive: true });
4000
+ await mkdir7(blocksDir, { recursive: true });
2087
4001
  const jsContent = `export default function decorate(block) {
2088
4002
  // TODO: implement ${name} block
2089
4003
  }
@@ -2092,14 +4006,14 @@ async function createBlock(name, options = {}) {
2092
4006
  /* TODO: style ${name} block */
2093
4007
  }
2094
4008
  `;
2095
- await writeFile7(path11.join(blocksDir, `${name}.js`), jsContent);
2096
- await writeFile7(path11.join(blocksDir, `${name}.css`), cssContent);
4009
+ await writeFile14(path22.join(blocksDir, `${name}.js`), jsContent);
4010
+ await writeFile14(path22.join(blocksDir, `${name}.css`), cssContent);
2097
4011
  const withUe = options.ueModel !== false;
2098
4012
  if (withUe) {
2099
- await writeFile7(path11.join(blocksDir, `_${name}.json`), ueModelFor(name));
4013
+ await writeFile14(path22.join(blocksDir, `_${name}.json`), ueModelFor(name));
2100
4014
  }
2101
4015
  logger.success(
2102
- `Block "${name}" created at ${chalk5.cyan(path11.relative(projectRoot, blocksDir))}${withUe ? chalk5.dim(" (+ Universal Editor model)") : ""}`
4016
+ `Block "${name}" created at ${chalk9.cyan(path22.relative(projectRoot, blocksDir))}${withUe ? chalk9.dim(" (+ Universal Editor model)") : ""}`
2103
4017
  );
2104
4018
  }
2105
4019
  var init_create = __esm({
@@ -2113,10 +4027,10 @@ var init_create = __esm({
2113
4027
  });
2114
4028
 
2115
4029
  // src/lib/agents/detect.ts
2116
- import { existsSync as existsSync10 } from "fs";
2117
- import { readFile as readFile6 } from "fs/promises";
4030
+ import { existsSync as existsSync20 } from "fs";
4031
+ import { readFile as readFile14 } from "fs/promises";
2118
4032
  import { homedir } from "os";
2119
- import path12 from "path";
4033
+ import path23 from "path";
2120
4034
  async function whichBinary(name) {
2121
4035
  const { execaCommand } = await import("execa");
2122
4036
  try {
@@ -2144,8 +4058,8 @@ function findFigmaServerKey(node) {
2144
4058
  async function findFigmaMcpServer(configPaths) {
2145
4059
  for (const configPath of configPaths) {
2146
4060
  try {
2147
- if (!existsSync10(configPath)) continue;
2148
- const content = await readFile6(configPath, "utf-8");
4061
+ if (!existsSync20(configPath)) continue;
4062
+ const content = await readFile14(configPath, "utf-8");
2149
4063
  try {
2150
4064
  const key = findFigmaServerKey(JSON.parse(content));
2151
4065
  if (key) return key;
@@ -2167,7 +4081,7 @@ async function detectAgent(preferred) {
2167
4081
  return {
2168
4082
  type: def.type,
2169
4083
  path: binPath,
2170
- configDir: existsSync10(def.configDir) ? def.configDir : void 0,
4084
+ configDir: existsSync20(def.configDir) ? def.configDir : void 0,
2171
4085
  hasFigmaMcp: !!figmaMcpServerName,
2172
4086
  figmaMcpServerName
2173
4087
  };
@@ -2182,7 +4096,7 @@ async function detectAgent(preferred) {
2182
4096
  return {
2183
4097
  type: def.type,
2184
4098
  path: binPath,
2185
- configDir: existsSync10(def.configDir) ? def.configDir : void 0,
4099
+ configDir: existsSync20(def.configDir) ? def.configDir : void 0,
2186
4100
  hasFigmaMcp: !!figmaMcpServerName,
2187
4101
  figmaMcpServerName
2188
4102
  };
@@ -2199,7 +4113,7 @@ async function detectAllAgents() {
2199
4113
  agents.push({
2200
4114
  type: def.type,
2201
4115
  path: binPath,
2202
- configDir: existsSync10(def.configDir) ? def.configDir : void 0,
4116
+ configDir: existsSync20(def.configDir) ? def.configDir : void 0,
2203
4117
  hasFigmaMcp: !!figmaMcpServerName,
2204
4118
  figmaMcpServerName
2205
4119
  });
@@ -2217,22 +4131,22 @@ var init_detect = __esm({
2217
4131
  {
2218
4132
  type: "claude",
2219
4133
  binary: "claude",
2220
- configDir: path12.join(homedir(), ".claude"),
4134
+ configDir: path23.join(homedir(), ".claude"),
2221
4135
  mcpConfigPaths: [
2222
- path12.join(homedir(), ".claude", "plugins.json"),
2223
- path12.join(homedir(), ".claude.json")
4136
+ path23.join(homedir(), ".claude", "plugins.json"),
4137
+ path23.join(homedir(), ".claude.json")
2224
4138
  ]
2225
4139
  },
2226
4140
  {
2227
4141
  type: "cursor",
2228
4142
  binary: "cursor",
2229
- configDir: path12.join(homedir(), ".cursor"),
2230
- mcpConfigPaths: [path12.join(homedir(), ".cursor", "mcp.json")]
4143
+ configDir: path23.join(homedir(), ".cursor"),
4144
+ mcpConfigPaths: [path23.join(homedir(), ".cursor", "mcp.json")]
2231
4145
  },
2232
4146
  {
2233
4147
  type: "codex",
2234
4148
  binary: "codex",
2235
- configDir: path12.join(homedir(), ".codex"),
4149
+ configDir: path23.join(homedir(), ".codex"),
2236
4150
  mcpConfigPaths: []
2237
4151
  }
2238
4152
  ];
@@ -2240,27 +4154,27 @@ var init_detect = __esm({
2240
4154
  });
2241
4155
 
2242
4156
  // src/lib/agents/invoke.ts
2243
- import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
4157
+ import { mkdir as mkdir8, readFile as readFile15, writeFile as writeFile15 } from "fs/promises";
2244
4158
  import { tmpdir } from "os";
2245
- import path13 from "path";
4159
+ import path24 from "path";
2246
4160
  import readline from "readline";
2247
- import chalk6 from "chalk";
4161
+ import chalk10 from "chalk";
2248
4162
  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 });
4163
+ const base = projectRoot ? path24.join(projectRoot, "tmp") : tmpdir();
4164
+ const dir = path24.join(base, "eds-cli");
4165
+ await mkdir8(dir, { recursive: true });
2252
4166
  const filename = `from-design-${Date.now()}.md`;
2253
- const filePath = path13.join(dir, filename);
2254
- await writeFile8(filePath, prompt, "utf-8");
4167
+ const filePath = path24.join(dir, filename);
4168
+ await writeFile15(filePath, prompt, "utf-8");
2255
4169
  return filePath;
2256
4170
  }
2257
4171
  async function invokeAgent(agent, promptFile, cwd) {
2258
4172
  const { execa } = await import("execa");
2259
- const prompt = await readFile7(promptFile, "utf-8");
4173
+ const prompt = await readFile15(promptFile, "utf-8");
2260
4174
  try {
2261
4175
  switch (agent.type) {
2262
4176
  case "claude": {
2263
- logger.info(`Running Claude Code ${chalk6.dim("- live log:")}`);
4177
+ logger.info(`Running Claude Code ${chalk10.dim("- live log:")}`);
2264
4178
  const figmaServers = [
2265
4179
  ...agent.figmaMcpServerName ? [agent.figmaMcpServerName] : [],
2266
4180
  "figma-sse",
@@ -2323,10 +4237,10 @@ async function renderClaudeStream(sub, cwd) {
2323
4237
  const ora4 = (await import("ora")).default;
2324
4238
  const start = Date.now();
2325
4239
  const elapsed = () => `${Math.round((Date.now() - start) / 1e3)}s`;
2326
- const spinner = ora4({ text: `thinking ${chalk6.dim(elapsed())}`, color: "yellow" }).start();
4240
+ const spinner = ora4({ text: `thinking ${chalk10.dim(elapsed())}`, color: "yellow" }).start();
2327
4241
  let activity = "thinking";
2328
4242
  const timer = setInterval(() => {
2329
- spinner.text = `${activity} ${chalk6.dim(elapsed())}`;
4243
+ spinner.text = `${activity} ${chalk10.dim(elapsed())}`;
2330
4244
  }, 300);
2331
4245
  const pending = /* @__PURE__ */ new Map();
2332
4246
  const emit = (line) => {
@@ -2352,7 +4266,7 @@ async function renderClaudeStream(sub, cwd) {
2352
4266
  for (const block2 of content) {
2353
4267
  if (block2.type === "text" && "text" in block2) {
2354
4268
  const first = block2.text.trim().split("\n")[0].slice(0, 100);
2355
- if (first) emit(chalk6.dim(first));
4269
+ if (first) emit(chalk10.dim(first));
2356
4270
  } else if (block2.type === "tool_use" && "name" in block2) {
2357
4271
  const id = "id" in block2 && block2.id ? block2.id : `${Math.random()}`;
2358
4272
  pending.set(id, {
@@ -2368,8 +4282,8 @@ async function renderClaudeStream(sub, cwd) {
2368
4282
  const p = pending.get(block2.tool_use_id);
2369
4283
  if (p) {
2370
4284
  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}`);
4285
+ const failed = "is_error" in block2 && block2.is_error ? ` ${chalk10.red("failed")}` : "";
4286
+ emit(`${p.label} ${chalk10.dim(dur)}${failed}`);
2373
4287
  pending.delete(block2.tool_use_id);
2374
4288
  }
2375
4289
  }
@@ -2381,9 +4295,9 @@ async function renderClaudeStream(sub, cwd) {
2381
4295
  }
2382
4296
  }
2383
4297
  }
2384
- for (const p of pending.values()) emit(`${p.label} ${chalk6.dim("\u2026")}`);
4298
+ for (const p of pending.values()) emit(`${p.label} ${chalk10.dim("\u2026")}`);
2385
4299
  clearInterval(timer);
2386
- if (result) spinner.succeed(`${brand.accent("agent finished")} ${chalk6.dim(result)}`);
4300
+ if (result) spinner.succeed(`${brand.accent("agent finished")} ${chalk10.dim(result)}`);
2387
4301
  else spinner.stop();
2388
4302
  } catch (err) {
2389
4303
  clearInterval(timer);
@@ -2396,22 +4310,22 @@ function toolLabel(name, input, cwd) {
2396
4310
  const rel = (key) => {
2397
4311
  const p = input?.[key];
2398
4312
  if (typeof p !== "string") return "";
2399
- return p.startsWith(cwd) ? path13.relative(cwd, p) : path13.basename(p);
4313
+ return p.startsWith(cwd) ? path24.relative(cwd, p) : path24.basename(p);
2400
4314
  };
2401
4315
  if (name.includes("figma")) {
2402
4316
  const short = name.split("__").pop() ?? name;
2403
- return `${icon.figma} ${chalk6.bold(short)} ${chalk6.dim("Figma MCP")}`;
4317
+ return `${icon.figma} ${chalk10.bold(short)} ${chalk10.dim("Figma MCP")}`;
2404
4318
  }
2405
4319
  switch (name) {
2406
4320
  case "Write":
2407
4321
  case "Edit":
2408
4322
  case "NotebookEdit":
2409
- return `${brand.accent("\u270E")} ${chalk6.cyan(rel("file_path"))}`;
4323
+ return `${brand.accent("\u270E")} ${chalk10.cyan(rel("file_path"))}`;
2410
4324
  case "Read":
2411
- return chalk6.dim(`\xB7 read ${rel("file_path")}`);
4325
+ return chalk10.dim(`\xB7 read ${rel("file_path")}`);
2412
4326
  case "Bash": {
2413
4327
  const cmd = typeof input?.command === "string" ? input.command.split("\n")[0].slice(0, 70) : "";
2414
- return chalk6.dim(`$ ${cmd}`);
4328
+ return chalk10.dim(`$ ${cmd}`);
2415
4329
  }
2416
4330
  default:
2417
4331
  return `${brand.blue("\u2192")} ${name}`;
@@ -2427,13 +4341,13 @@ var init_invoke = __esm({
2427
4341
  });
2428
4342
 
2429
4343
  // src/lib/config.ts
2430
- import { existsSync as existsSync11 } from "fs";
2431
- import { readFile as readFile8 } from "fs/promises";
4344
+ import { existsSync as existsSync21 } from "fs";
4345
+ import { readFile as readFile16 } from "fs/promises";
2432
4346
  import { homedir as homedir2 } from "os";
2433
- import path14 from "path";
4347
+ import path25 from "path";
2434
4348
  async function loadJsonFile(filePath) {
2435
4349
  try {
2436
- const content = await readFile8(filePath, "utf-8");
4350
+ const content = await readFile16(filePath, "utf-8");
2437
4351
  return JSON.parse(content);
2438
4352
  } catch {
2439
4353
  return void 0;
@@ -2441,7 +4355,7 @@ async function loadJsonFile(filePath) {
2441
4355
  }
2442
4356
  async function loadConfig(projectRoot) {
2443
4357
  const globalRaw = await loadJsonFile(GLOBAL_CONFIG_PATH);
2444
- const projectRaw = projectRoot ? await loadJsonFile(path14.join(projectRoot, PROJECT_CONFIG_NAME)) : void 0;
4358
+ const projectRaw = projectRoot ? await loadJsonFile(path25.join(projectRoot, PROJECT_CONFIG_NAME)) : void 0;
2445
4359
  const merged = {
2446
4360
  ...typeof globalRaw === "object" && globalRaw !== null ? globalRaw : {},
2447
4361
  ...typeof projectRaw === "object" && projectRaw !== null ? projectRaw : {}
@@ -2460,7 +4374,7 @@ function globalConfigPath() {
2460
4374
  return GLOBAL_CONFIG_PATH;
2461
4375
  }
2462
4376
  function globalConfigExists() {
2463
- return existsSync11(GLOBAL_CONFIG_PATH);
4377
+ return existsSync21(GLOBAL_CONFIG_PATH);
2464
4378
  }
2465
4379
  var GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_PATH, PROJECT_CONFIG_NAME;
2466
4380
  var init_config = __esm({
@@ -2469,8 +4383,8 @@ var init_config = __esm({
2469
4383
  init_esm_shims();
2470
4384
  init_logger();
2471
4385
  init_schemas();
2472
- GLOBAL_CONFIG_DIR = path14.join(homedir2(), ".eds");
2473
- GLOBAL_CONFIG_PATH = path14.join(GLOBAL_CONFIG_DIR, "config.json");
4386
+ GLOBAL_CONFIG_DIR = path25.join(homedir2(), ".eds");
4387
+ GLOBAL_CONFIG_PATH = path25.join(GLOBAL_CONFIG_DIR, "config.json");
2474
4388
  PROJECT_CONFIG_NAME = ".edsrc.json";
2475
4389
  }
2476
4390
  });
@@ -2520,9 +4434,9 @@ var init_node_id = __esm({
2520
4434
  });
2521
4435
 
2522
4436
  // 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";
4437
+ import { existsSync as existsSync22 } from "fs";
4438
+ import { readFile as readFile17 } from "fs/promises";
4439
+ import path26 from "path";
2526
4440
  function contentSourceGuidance(source, cfHint, blockName) {
2527
4441
  if (!source || source === "document" || source === "ue") return "";
2528
4442
  const q = cfHint ? ` (model / persisted query: \`${cfHint}\`)` : "";
@@ -2651,6 +4565,9 @@ Rules:
2651
4565
  - **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
4566
 
2653
4567
  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\`.
4568
+
4569
+ **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:
4570
+ ${PARTIALS.map((p) => `- \`${p.id}\` \u2014 ${p.label}`).join("\n")}
2654
4571
  ` : "";
2655
4572
  return `# Pixel-Perfect EDS Block from ${providerName}
2656
4573
 
@@ -2734,10 +4651,10 @@ ${tokensHint}
2734
4651
  `;
2735
4652
  }
2736
4653
  async function getTokensHint(projectRoot) {
2737
- const stylesPath = path15.join(projectRoot, "styles", "styles.css");
2738
- if (!existsSync12(stylesPath)) return "";
4654
+ const stylesPath = path26.join(projectRoot, "styles", "styles.css");
4655
+ if (!existsSync22(stylesPath)) return "";
2739
4656
  try {
2740
- const css = await readFile9(stylesPath, "utf-8");
4657
+ const css = await readFile17(stylesPath, "utf-8");
2741
4658
  const rootMatch = css.match(/:root\s*\{([^}]+)\}/);
2742
4659
  if (!rootMatch) return "";
2743
4660
  const props = rootMatch[1].split("\n").filter((line) => line.trim().startsWith("--")).map((line) => line.trim()).join("\n ");
@@ -2762,6 +4679,7 @@ var init_prompt_builder = __esm({
2762
4679
  "src/lib/figma/prompt-builder.ts"() {
2763
4680
  "use strict";
2764
4681
  init_esm_shims();
4682
+ init_partials();
2765
4683
  PROMPT_VERSION = "0.5.0";
2766
4684
  PROVIDER_NAMES = {
2767
4685
  figma: "Figma",
@@ -2878,11 +4796,11 @@ var init_harness = __esm({
2878
4796
  });
2879
4797
 
2880
4798
  // src/lib/preview/server.ts
2881
- import { createReadStream, existsSync as existsSync13, statSync } from "fs";
4799
+ import { createReadStream, existsSync as existsSync23, statSync } from "fs";
2882
4800
  import http from "http";
2883
- import path16 from "path";
4801
+ import path27 from "path";
2884
4802
  function startServer(root, routes, port, host = "127.0.0.1") {
2885
- const resolvedRoot = path16.resolve(root);
4803
+ const resolvedRoot = path27.resolve(root);
2886
4804
  const clients = /* @__PURE__ */ new Set();
2887
4805
  const server = http.createServer((req, res) => {
2888
4806
  const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
@@ -2908,19 +4826,19 @@ function startServer(root, routes, port, host = "127.0.0.1") {
2908
4826
  res.end(route.body);
2909
4827
  return;
2910
4828
  }
2911
- const filePath = path16.join(resolvedRoot, urlPath);
4829
+ const filePath = path27.join(resolvedRoot, urlPath);
2912
4830
  if (!filePath.startsWith(resolvedRoot)) {
2913
4831
  res.writeHead(403);
2914
4832
  res.end("Forbidden");
2915
4833
  return;
2916
4834
  }
2917
- if (!existsSync13(filePath) || !statSync(filePath).isFile()) {
4835
+ if (!existsSync23(filePath) || !statSync(filePath).isFile()) {
2918
4836
  res.writeHead(404, { "content-type": "text/plain" });
2919
4837
  res.end(`Not found: ${urlPath}`);
2920
4838
  return;
2921
4839
  }
2922
4840
  res.writeHead(200, {
2923
- "content-type": MIME[path16.extname(filePath).toLowerCase()] ?? "application/octet-stream"
4841
+ "content-type": MIME[path27.extname(filePath).toLowerCase()] ?? "application/octet-stream"
2924
4842
  });
2925
4843
  createReadStream(filePath).pipe(res);
2926
4844
  });
@@ -2984,10 +4902,10 @@ var preview_exports = {};
2984
4902
  __export(preview_exports, {
2985
4903
  previewBlock: () => previewBlock
2986
4904
  });
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";
4905
+ import { existsSync as existsSync24, watch } from "fs";
4906
+ import { readFile as readFile18 } from "fs/promises";
4907
+ import path28 from "path";
4908
+ import chalk11 from "chalk";
2991
4909
  function labelFor(width) {
2992
4910
  if (width <= 480) return `${width} mobile`;
2993
4911
  if (width <= 1024) return `${width} tablet`;
@@ -3039,30 +4957,30 @@ async function previewBlock(name, options = {}) {
3039
4957
  process.exitCode = 1;
3040
4958
  return;
3041
4959
  }
3042
- const blockDir = path17.join(projectRoot, "blocks", name);
3043
- if (!existsSync14(blockDir)) {
4960
+ const blockDir = path28.join(projectRoot, "blocks", name);
4961
+ if (!existsSync24(blockDir)) {
3044
4962
  logger.error(`Block "${name}" not found at blocks/${name}/.`);
3045
4963
  process.exitCode = 1;
3046
4964
  return;
3047
4965
  }
3048
- if (!existsSync14(path17.join(blockDir, `${name}.css`)) && !existsSync14(path17.join(blockDir, `${name}.js`))) {
4966
+ if (!existsSync24(path28.join(blockDir, `${name}.css`)) && !existsSync24(path28.join(blockDir, `${name}.js`))) {
3049
4967
  logger.error(`Block "${name}" has no ${name}.css / ${name}.js to preview.`);
3050
4968
  process.exitCode = 1;
3051
4969
  return;
3052
4970
  }
3053
4971
  let meta = null;
3054
- const metaPath = path17.join(blockDir, ".eds-meta.json");
3055
- if (existsSync14(metaPath)) {
4972
+ const metaPath = path28.join(blockDir, ".eds-meta.json");
4973
+ if (existsSync24(metaPath)) {
3056
4974
  try {
3057
- meta = JSON.parse(await readFile10(metaPath, "utf-8"));
4975
+ meta = JSON.parse(await readFile18(metaPath, "utf-8"));
3058
4976
  } catch {
3059
4977
  }
3060
4978
  }
3061
- const samplePath = path17.join(blockDir, `_${name}.preview.html`);
4979
+ const samplePath = path28.join(blockDir, `_${name}.preview.html`);
3062
4980
  let sampleContent;
3063
4981
  let placeholder = false;
3064
- if (existsSync14(samplePath)) {
3065
- sampleContent = await readFile10(samplePath, "utf-8");
4982
+ if (existsSync24(samplePath)) {
4983
+ sampleContent = await readFile18(samplePath, "utf-8");
3066
4984
  } else {
3067
4985
  sampleContent = defaultSample(name);
3068
4986
  placeholder = true;
@@ -3112,9 +5030,9 @@ async function previewBlock(name, options = {}) {
3112
5030
  reloadTimer = setTimeout(() => server.reload(), 150);
3113
5031
  });
3114
5032
  logger.info("");
3115
- logger.info(` ${chalk7.bold("\u279C")} ${chalk7.cyan(server.url)}`);
5033
+ logger.info(` ${chalk11.bold("\u279C")} ${chalk11.cyan(server.url)}`);
3116
5034
  logger.info(statusLine("pass", "live reload", "edits to the block refresh the browser"));
3117
- logger.info(chalk7.dim(" Press Ctrl+C to stop.\n"));
5035
+ logger.info(chalk11.dim(" Press Ctrl+C to stop.\n"));
3118
5036
  if (options.open !== false) await openInBrowser(server.url);
3119
5037
  const stop = async () => {
3120
5038
  watcher.close();
@@ -3143,10 +5061,10 @@ var from_design_exports = {};
3143
5061
  __export(from_design_exports, {
3144
5062
  blockFromDesign: () => blockFromDesign
3145
5063
  });
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";
5064
+ import { existsSync as existsSync25 } from "fs";
5065
+ import { readFile as readFile19, writeFile as writeFile16 } from "fs/promises";
5066
+ import path29 from "path";
5067
+ import chalk12 from "chalk";
3150
5068
  import ora from "ora";
3151
5069
  async function previewRunning(port = 8777) {
3152
5070
  try {
@@ -3296,7 +5214,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3296
5214
  try {
3297
5215
  figma2 = parseFigmaUrl(designUrl);
3298
5216
  spinner.succeed(
3299
- `Parsed Figma URL ${chalk8.dim(figma2.nodeId ? `node ${figma2.nodeId}` : `file ${figma2.fileKey}`)}`
5217
+ `Parsed Figma URL ${chalk12.dim(figma2.nodeId ? `node ${figma2.nodeId}` : `file ${figma2.fileKey}`)}`
3300
5218
  );
3301
5219
  } catch (err) {
3302
5220
  spinner.fail(err.message);
@@ -3304,7 +5222,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3304
5222
  return;
3305
5223
  }
3306
5224
  } else {
3307
- logger.info(` ${icon.pass} ${providerName} design ${chalk8.dim(designRef)}`);
5225
+ logger.info(` ${icon.pass} ${providerName} design ${chalk12.dim(designRef)}`);
3308
5226
  }
3309
5227
  const blockName = options.name || (figma2 ? inferBlockName(figma2.fileName) : "");
3310
5228
  if (!kebabCaseRegex.test(blockName)) {
@@ -3327,20 +5245,20 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3327
5245
  screenshot: options.screenshot !== false
3328
5246
  });
3329
5247
  const promptFile = await savePromptToFile(prompt, projectRoot);
3330
- const hasTokens = existsSync15(path18.join(projectRoot, "styles", "styles.css"));
5248
+ const hasTokens = existsSync25(path29.join(projectRoot, "styles", "styles.css"));
3331
5249
  promptSpinner.succeed(
3332
- hasTokens ? `Loaded design tokens ${chalk8.dim("styles/styles.css")}` : `Built prompt ${chalk8.dim("no project tokens found")}`
5250
+ hasTokens ? `Loaded design tokens ${chalk12.dim("styles/styles.css")}` : `Built prompt ${chalk12.dim("no project tokens found")}`
3333
5251
  );
3334
- logger.info(chalk8.dim(` prompt \u2192 ${promptFile}`));
5252
+ logger.info(chalk12.dim(` prompt \u2192 ${promptFile}`));
3335
5253
  if (options.dryRun) {
3336
5254
  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)}`);
5255
+ logger.info(chalk12.yellow("Dry run mode \u2014 prompt generated but agent not invoked."));
5256
+ logger.info(`Prompt file: ${chalk12.cyan(promptFile)}`);
5257
+ logger.info(`Block name: ${chalk12.cyan(blockName)}`);
3340
5258
  logger.info(`Prompt version: ${PROMPT_VERSION}`);
3341
5259
  logger.info("");
3342
5260
  logger.info("Preview:");
3343
- logger.info(chalk8.dim("\u2500".repeat(60)));
5261
+ logger.info(chalk12.dim("\u2500".repeat(60)));
3344
5262
  console.log(prompt);
3345
5263
  return;
3346
5264
  }
@@ -3348,22 +5266,22 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3348
5266
  const preferredAgent = options.agent || config.agent;
3349
5267
  if (options.agent === "none") {
3350
5268
  logger.info("Agent skipped (--agent none). Use the prompt file manually:");
3351
- logger.info(` ${chalk8.cyan(promptFile)}`);
5269
+ logger.info(` ${chalk12.cyan(promptFile)}`);
3352
5270
  return;
3353
5271
  }
3354
5272
  const agentSpinner = ora("Detecting AI agent...").start();
3355
5273
  const agent = await detectAgent(preferredAgent);
3356
5274
  if (!agent) {
3357
5275
  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)}`);
5276
+ logger.info(`Install Claude Code: ${chalk12.cyan("npm install -g @anthropic-ai/claude-code")}`);
5277
+ logger.info(`Or use the prompt file manually: ${chalk12.cyan(promptFile)}`);
3360
5278
  process.exitCode = 1;
3361
5279
  return;
3362
5280
  }
3363
- agentSpinner.succeed(`Agent ready ${chalk8.cyan(agent.type)} ${chalk8.dim(agent.path)}`);
5281
+ agentSpinner.succeed(`Agent ready ${chalk12.cyan(agent.type)} ${chalk12.dim(agent.path)}`);
3364
5282
  if (provider === "figma" && !agent.hasFigmaMcp) {
3365
5283
  logger.warn(`Figma MCP may not be configured for ${agent.type}.`);
3366
- logger.info(`Run ${chalk8.cyan("eds figma setup")} to configure it.`);
5284
+ logger.info(`Run ${chalk12.cyan("eds figma setup")} to configure it.`);
3367
5285
  }
3368
5286
  if (!options.yes) {
3369
5287
  const { confirm } = await import("@inquirer/prompts");
@@ -3372,24 +5290,24 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3372
5290
  default: true
3373
5291
  });
3374
5292
  if (!proceed) {
3375
- logger.info(`Prompt saved to: ${chalk8.cyan(promptFile)}`);
5293
+ logger.info(`Prompt saved to: ${chalk12.cyan(promptFile)}`);
3376
5294
  return;
3377
5295
  }
3378
5296
  }
3379
5297
  logger.info(
3380
- ` ${chalk8.dim("\u2192")} Handing off to ${chalk8.cyan(agent.type)} ${chalk8.dim(`via ${providerName} MCP`)}`
5298
+ ` ${chalk12.dim("\u2192")} Handing off to ${chalk12.cyan(agent.type)} ${chalk12.dim(`via ${providerName} MCP`)}`
3381
5299
  );
3382
5300
  if (provider === "figma") {
3383
- logger.info(chalk8.dim(" get_design_context \xB7 get_variable_defs \xB7 get_screenshot"));
5301
+ logger.info(chalk12.dim(" get_design_context \xB7 get_variable_defs \xB7 get_screenshot"));
3384
5302
  }
3385
5303
  const success = await invokeAgent(agent, promptFile, projectRoot);
3386
5304
  if (!success) {
3387
5305
  process.exitCode = 1;
3388
5306
  return;
3389
5307
  }
3390
- const blockDir = path18.join(projectRoot, "blocks", blockName);
5308
+ const blockDir = path29.join(projectRoot, "blocks", blockName);
3391
5309
  const ok = await verifyGeneratedBlock(blockDir, blockName, options.ueModel !== false);
3392
- if (existsSync15(blockDir)) {
5310
+ if (existsSync25(blockDir)) {
3393
5311
  const meta = {
3394
5312
  provider,
3395
5313
  designRef,
@@ -3399,7 +5317,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3399
5317
  promptVersion: PROMPT_VERSION,
3400
5318
  agentUsed: agent.type
3401
5319
  };
3402
- await writeFile9(path18.join(blockDir, ".eds-meta.json"), `${JSON.stringify(meta, null, 2)}
5320
+ await writeFile16(path29.join(blockDir, ".eds-meta.json"), `${JSON.stringify(meta, null, 2)}
3403
5321
  `);
3404
5322
  }
3405
5323
  if (!ok) {
@@ -3424,7 +5342,7 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3424
5342
  if (options.serve !== false) {
3425
5343
  if (await previewRunning()) {
3426
5344
  logger.info(
3427
- chalk8.dim(
5345
+ chalk12.dim(
3428
5346
  " A preview server is already running at http://127.0.0.1:8777 \u2014 your change will refresh there."
3429
5347
  )
3430
5348
  );
@@ -3434,15 +5352,15 @@ async function blockFromDesign(figmaUrlArg, optionsArg) {
3434
5352
  await previewBlock2(blockName, {});
3435
5353
  }
3436
5354
  } else {
3437
- logger.info(` ${chalk8.dim("\u2192")} Next: ${chalk8.cyan(`eds block preview ${blockName}`)}`);
5355
+ logger.info(` ${chalk12.dim("\u2192")} Next: ${chalk12.cyan(`eds block preview ${blockName}`)}`);
3438
5356
  }
3439
5357
  }
3440
5358
  async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3441
5359
  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);
5360
+ const jsPath = path29.join(blockDir, `${blockName}.js`);
5361
+ const cssPath = path29.join(blockDir, `${blockName}.css`);
5362
+ const hasJs = existsSync25(jsPath);
5363
+ const hasCss = existsSync25(cssPath);
3446
5364
  checks.push({
3447
5365
  status: hasJs ? "pass" : "fail",
3448
5366
  name: `${blockName}.js`,
@@ -3454,7 +5372,7 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3454
5372
  message: hasCss ? "created" : "missing \u2014 agent did not write the CSS file"
3455
5373
  });
3456
5374
  if (hasJs) {
3457
- const js = await readFile11(jsPath, "utf-8");
5375
+ const js = await readFile19(jsPath, "utf-8");
3458
5376
  checks.push({
3459
5377
  status: /export\s+default/.test(js) ? "pass" : "fail",
3460
5378
  name: "decorate export",
@@ -3469,7 +5387,7 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3469
5387
  }
3470
5388
  }
3471
5389
  if (hasCss) {
3472
- const css = await readFile11(cssPath, "utf-8");
5390
+ const css = await readFile19(cssPath, "utf-8");
3473
5391
  checks.push({
3474
5392
  status: css.includes(`.${blockName}`) ? "pass" : "warn",
3475
5393
  name: "scoped CSS",
@@ -3477,12 +5395,12 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3477
5395
  });
3478
5396
  }
3479
5397
  if (withUeModel) {
3480
- const hasPerBlock = existsSync15(path18.join(blockDir, `_${blockName}.json`));
3481
- const modelsFile = path18.join(blockDir, "..", "..", "component-models.json");
5398
+ const hasPerBlock = existsSync25(path29.join(blockDir, `_${blockName}.json`));
5399
+ const modelsFile = path29.join(blockDir, "..", "..", "component-models.json");
3482
5400
  let hasAggregated = false;
3483
- if (!hasPerBlock && existsSync15(modelsFile)) {
5401
+ if (!hasPerBlock && existsSync25(modelsFile)) {
3484
5402
  try {
3485
- hasAggregated = (await readFile11(modelsFile, "utf-8")).includes(`"${blockName}"`);
5403
+ hasAggregated = (await readFile19(modelsFile, "utf-8")).includes(`"${blockName}"`);
3486
5404
  } catch {
3487
5405
  }
3488
5406
  }
@@ -3494,8 +5412,8 @@ async function verifyGeneratedBlock(blockDir, blockName, withUeModel) {
3494
5412
  });
3495
5413
  if (hasPerBlock && hasJs) {
3496
5414
  try {
3497
- const modelRaw = await readFile11(path18.join(blockDir, `_${blockName}.json`), "utf-8");
3498
- const js = await readFile11(jsPath, "utf-8");
5415
+ const modelRaw = await readFile19(path29.join(blockDir, `_${blockName}.json`), "utf-8");
5416
+ const js = await readFile19(jsPath, "utf-8");
3499
5417
  const isRepeatable = /"resourceType"\s*:\s*"[^"]*\/item"/.test(modelRaw) || /"filter"\s*:/.test(modelRaw) || /"multi"\s*:\s*true/.test(modelRaw);
3500
5418
  const iterates = /\.children/.test(js) || /\bfor\s*\(/.test(js) || /\.forEach\s*\(/.test(js);
3501
5419
  if (isRepeatable) {
@@ -3538,10 +5456,10 @@ var list_exports = {};
3538
5456
  __export(list_exports, {
3539
5457
  listBlocks: () => listBlocks
3540
5458
  });
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";
5459
+ import { existsSync as existsSync26 } from "fs";
5460
+ import { readFile as readFile20, readdir as readdir4 } from "fs/promises";
5461
+ import path30 from "path";
5462
+ import chalk13 from "chalk";
3545
5463
  async function listBlocks(options = {}) {
3546
5464
  const projectRoot = findProjectRoot();
3547
5465
  if (!projectRoot) {
@@ -3549,27 +5467,27 @@ async function listBlocks(options = {}) {
3549
5467
  process.exitCode = 1;
3550
5468
  return;
3551
5469
  }
3552
- const blocksDir = path19.join(projectRoot, "blocks");
3553
- if (!existsSync16(blocksDir)) {
5470
+ const blocksDir = path30.join(projectRoot, "blocks");
5471
+ if (!existsSync26(blocksDir)) {
3554
5472
  logger.info("No blocks/ directory found.");
3555
5473
  return;
3556
5474
  }
3557
- const entries = await readdir2(blocksDir, { withFileTypes: true });
5475
+ const entries = await readdir4(blocksDir, { withFileTypes: true });
3558
5476
  const blocks = [];
3559
5477
  for (const entry of entries) {
3560
5478
  if (!entry.isDirectory()) continue;
3561
5479
  const name = entry.name;
3562
- const blockDir = path19.join(blocksDir, name);
5480
+ const blockDir = path30.join(blocksDir, name);
3563
5481
  const info = {
3564
5482
  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`))
5483
+ hasJs: existsSync26(path30.join(blockDir, `${name}.js`)),
5484
+ hasCss: existsSync26(path30.join(blockDir, `${name}.css`)),
5485
+ hasUeModel: existsSync26(path30.join(blockDir, `_${name}.json`))
3568
5486
  };
3569
- const metaFile = path19.join(blockDir, ".eds-meta.json");
3570
- if (existsSync16(metaFile)) {
5487
+ const metaFile = path30.join(blockDir, ".eds-meta.json");
5488
+ if (existsSync26(metaFile)) {
3571
5489
  try {
3572
- const meta = JSON.parse(await readFile12(metaFile, "utf-8"));
5490
+ const meta = JSON.parse(await readFile20(metaFile, "utf-8"));
3573
5491
  info.figmaSource = {
3574
5492
  fileKey: meta.figmaFileKey,
3575
5493
  nodeId: meta.figmaNodeId,
@@ -3592,17 +5510,17 @@ async function listBlocks(options = {}) {
3592
5510
  const nameWidth = columnWidth(blocks.map((b) => b.name));
3593
5511
  for (const b of blocks) {
3594
5512
  const badges = [
3595
- b.hasJs ? brand.ok("js") : chalk9.dim.strikethrough("js"),
3596
- b.hasCss ? brand.ok("css") : chalk9.dim.strikethrough("css")
5513
+ b.hasJs ? brand.ok("js") : chalk13.dim.strikethrough("js"),
5514
+ b.hasCss ? brand.ok("css") : chalk13.dim.strikethrough("css")
3597
5515
  ];
3598
5516
  if (b.hasUeModel) badges.push(brand.blue("ue-model"));
3599
5517
  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("]")}`);
5518
+ const name = padEndVisible(chalk13.bold(b.name), nameWidth);
5519
+ logger.info(` ${name} ${chalk13.dim("[")}${badges.join(" ")}${chalk13.dim("]")}`);
3602
5520
  if (b.figmaSource) {
3603
5521
  const synced = new Date(b.figmaSource.lastSyncedAt).toLocaleDateString();
3604
5522
  logger.info(
3605
- ` ${" ".repeat(nameWidth)} ${icon.arrow} ${chalk9.dim(
5523
+ ` ${" ".repeat(nameWidth)} ${icon.arrow} ${chalk13.dim(
3606
5524
  `figma:${b.figmaSource.fileKey} \xB7 synced ${synced}`
3607
5525
  )}`
3608
5526
  );
@@ -3782,10 +5700,10 @@ var integrate_exports = {};
3782
5700
  __export(integrate_exports, {
3783
5701
  integrate: () => integrate
3784
5702
  });
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";
5703
+ import { existsSync as existsSync27 } from "fs";
5704
+ import { mkdir as mkdir9, readFile as readFile21, writeFile as writeFile17 } from "fs/promises";
5705
+ import path31 from "path";
5706
+ import chalk14 from "chalk";
3789
5707
  async function integrate(type) {
3790
5708
  const projectRoot = findProjectRoot();
3791
5709
  if (!projectRoot) {
@@ -3808,16 +5726,16 @@ async function integrate(type) {
3808
5726
  return;
3809
5727
  }
3810
5728
  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();
5729
+ for (const f2 of integration.fields) {
5730
+ const answer = await input({ message: ` ${f2.message}`, default: f2.placeholder });
5731
+ values[f2.name] = answer.trim();
3814
5732
  }
3815
5733
  const modulePaths = [];
3816
5734
  if (integration.module) {
3817
5735
  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);
5736
+ const abs = path31.join(projectRoot, mod.file);
5737
+ await mkdir9(path31.dirname(abs), { recursive: true });
5738
+ await writeFile17(abs, mod.content);
3821
5739
  modulePaths.push(mod.file);
3822
5740
  }
3823
5741
  }
@@ -3825,26 +5743,26 @@ async function integrate(type) {
3825
5743
  // ${integration.name} (eds integrate)
3826
5744
  ${integration.code(values)}
3827
5745
  ` : 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*$/, "")}
5746
+ const delayedPath = path31.join(projectRoot, "scripts", "delayed.js");
5747
+ await mkdir9(path31.dirname(delayedPath), { recursive: true });
5748
+ const existing = existsSync27(delayedPath) ? await readFile21(delayedPath, "utf-8") : DELAYED_HEADER;
5749
+ await writeFile17(delayedPath, `${existing.replace(/\s*$/, "")}
3832
5750
  ${block2}`);
3833
5751
  logger.info(heading("Added"));
3834
5752
  for (const p of modulePaths) logger.info(accentLine(integration.name, `wrote ${p}`));
3835
5753
  logger.info(accentLine(integration.name, "wired into scripts/delayed.js"));
3836
5754
  logger.info("");
3837
5755
  logger.info(
3838
- chalk10.dim(" Loads after LCP (delayed phase) - zero impact on your Core Web Vitals.")
5756
+ chalk14.dim(" Loads after LCP (delayed phase) - zero impact on your Core Web Vitals.")
3839
5757
  );
3840
5758
  if (integration.id === "gtm" || integration.id === "ga4") {
3841
5759
  logger.info(
3842
- chalk10.dim(" Tip: gate analytics behind user consent (add the Cookiebot integration).")
5760
+ chalk14.dim(" Tip: gate analytics behind user consent (add the Cookiebot integration).")
3843
5761
  );
3844
5762
  }
3845
5763
  if (integration.id === "chat") {
3846
5764
  logger.info(
3847
- chalk10.dim(" The widget loads only when the visitor clicks the button (facade pattern).")
5765
+ chalk14.dim(" The widget loads only when the visitor clicks the button (facade pattern).")
3848
5766
  );
3849
5767
  }
3850
5768
  logger.info("");
@@ -3870,9 +5788,9 @@ var setup_exports = {};
3870
5788
  __export(setup_exports, {
3871
5789
  figmaSetup: () => figmaSetup
3872
5790
  });
3873
- import { existsSync as existsSync18 } from "fs";
3874
- import { mkdir as mkdir7, writeFile as writeFile11 } from "fs/promises";
3875
- import chalk11 from "chalk";
5791
+ import { existsSync as existsSync28 } from "fs";
5792
+ import { mkdir as mkdir10, writeFile as writeFile18 } from "fs/promises";
5793
+ import chalk15 from "chalk";
3876
5794
  async function figmaSetup() {
3877
5795
  const { select, confirm } = await import("@inquirer/prompts");
3878
5796
  logger.logoOnce(logo());
@@ -3882,15 +5800,15 @@ async function figmaSetup() {
3882
5800
  let selectedAgent;
3883
5801
  if (agents.length === 0) {
3884
5802
  logger.warn("No AI agents found (claude, cursor, codex).");
3885
- logger.info(`Install Claude Code: ${chalk11.cyan("npm install -g @anthropic-ai/claude-code")}`);
5803
+ logger.info(`Install Claude Code: ${chalk15.cyan("npm install -g @anthropic-ai/claude-code")}`);
3886
5804
  selectedAgent = "none";
3887
5805
  } else {
3888
- logger.success(`Found: ${agents.map((a) => chalk11.cyan(a.type)).join(", ")}`);
5806
+ logger.success(`Found: ${agents.map((a) => chalk15.cyan(a.type)).join(", ")}`);
3889
5807
  selectedAgent = await select({
3890
5808
  message: "Which agent do you want to use with eds-cli?",
3891
5809
  choices: [
3892
5810
  ...agents.map((a) => ({
3893
- name: `${a.type}${a.hasFigmaMcp ? chalk11.green(" (Figma MCP detected)") : ""}`,
5811
+ name: `${a.type}${a.hasFigmaMcp ? chalk15.green(" (Figma MCP detected)") : ""}`,
3894
5812
  value: a.type
3895
5813
  })),
3896
5814
  { name: "None (manual mode)", value: "none" }
@@ -3906,17 +5824,17 @@ async function figmaSetup() {
3906
5824
  switch (selectedAgent) {
3907
5825
  case "claude":
3908
5826
  logger.info(
3909
- ` ${chalk11.cyan("claude mcp add --transport sse figma-sse https://mcp.figma.com/sse")}`
5827
+ ` ${chalk15.cyan("claude mcp add --transport sse figma-sse https://mcp.figma.com/sse")}`
3910
5828
  );
3911
5829
  logger.info(" or for Figma Desktop app:");
3912
5830
  logger.info(
3913
- ` ${chalk11.cyan("claude mcp add --transport http figma-desktop http://127.0.0.1:3845/mcp")}`
5831
+ ` ${chalk15.cyan("claude mcp add --transport http figma-desktop http://127.0.0.1:3845/mcp")}`
3914
5832
  );
3915
5833
  break;
3916
5834
  case "cursor":
3917
- logger.info(` Add to ${chalk11.cyan("~/.cursor/mcp.json")}:`);
5835
+ logger.info(` Add to ${chalk15.cyan("~/.cursor/mcp.json")}:`);
3918
5836
  logger.info(
3919
- chalk11.dim(
5837
+ chalk15.dim(
3920
5838
  JSON.stringify(
3921
5839
  {
3922
5840
  mcpServers: {
@@ -3941,7 +5859,7 @@ async function figmaSetup() {
3941
5859
  }
3942
5860
  }
3943
5861
  logger.info(
3944
- chalk11.dim(
5862
+ chalk15.dim(
3945
5863
  "\nNote: Figma MCP requires a Figma seat (Dev or Full on paid plans for desktop server)."
3946
5864
  )
3947
5865
  );
@@ -3951,22 +5869,22 @@ async function figmaSetup() {
3951
5869
  });
3952
5870
  if (shouldSave) {
3953
5871
  const configDir = globalConfigDir();
3954
- if (!existsSync18(configDir)) {
3955
- await mkdir7(configDir, { recursive: true });
5872
+ if (!existsSync28(configDir)) {
5873
+ await mkdir10(configDir, { recursive: true });
3956
5874
  }
3957
5875
  const config = {};
3958
5876
  if (selectedAgent !== "none") {
3959
5877
  config.agent = selectedAgent;
3960
5878
  }
3961
- await writeFile11(globalConfigPath(), `${JSON.stringify(config, null, 2)}
5879
+ await writeFile18(globalConfigPath(), `${JSON.stringify(config, null, 2)}
3962
5880
  `, {
3963
5881
  mode: 384
3964
5882
  });
3965
- logger.success(`Config saved to ${chalk11.cyan(globalConfigPath())}`);
5883
+ logger.success(`Config saved to ${chalk15.cyan(globalConfigPath())}`);
3966
5884
  }
3967
- logger.info(chalk11.bold("\nSetup complete!"));
5885
+ logger.info(chalk15.bold("\nSetup complete!"));
3968
5886
  if (selectedAgent !== "none") {
3969
- logger.info(`Try: ${chalk11.cyan('eds block from-design "<figma-url>" --dry-run')}`);
5887
+ logger.info(`Try: ${chalk15.cyan('eds block from-design "<figma-url>" --dry-run')}`);
3970
5888
  }
3971
5889
  }
3972
5890
  var init_setup = __esm({
@@ -3985,10 +5903,10 @@ var doctor_exports = {};
3985
5903
  __export(doctor_exports, {
3986
5904
  doctor: () => doctor
3987
5905
  });
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";
5906
+ import { existsSync as existsSync29 } from "fs";
5907
+ import { readFile as readFile22, readdir as readdir5 } from "fs/promises";
5908
+ import path32 from "path";
5909
+ import { parse as parseYaml2 } from "yaml";
3992
5910
  async function doctor(options = {}) {
3993
5911
  const projectRoot = findProjectRoot();
3994
5912
  if (!projectRoot) {
@@ -4009,8 +5927,8 @@ async function doctor(options = {}) {
4009
5927
  });
4010
5928
  if (project.hasFstab) {
4011
5929
  try {
4012
- const content = await readFile14(path21.join(projectRoot, "fstab.yaml"), "utf-8");
4013
- parseYaml(content);
5930
+ const content = await readFile22(path32.join(projectRoot, "fstab.yaml"), "utf-8");
5931
+ parseYaml2(content);
4014
5932
  results.push({ name: "fstab.yaml valid YAML", status: "pass", message: "Valid" });
4015
5933
  } catch {
4016
5934
  results.push({
@@ -4031,28 +5949,28 @@ async function doctor(options = {}) {
4031
5949
  message: project.hasBlocks ? "Found" : "Missing - no blocks yet"
4032
5950
  });
4033
5951
  if (project.hasBlocks) {
4034
- const blocksDir = path21.join(projectRoot, "blocks");
4035
- const entries = await readdir3(blocksDir, { withFileTypes: true });
5952
+ const blocksDir = path32.join(projectRoot, "blocks");
5953
+ const entries = await readdir5(blocksDir, { withFileTypes: true });
4036
5954
  let aggregatedModels = "";
4037
- const aggregatedPath = path21.join(projectRoot, "component-models.json");
4038
- if (existsSync19(aggregatedPath)) {
5955
+ const aggregatedPath = path32.join(projectRoot, "component-models.json");
5956
+ if (existsSync29(aggregatedPath)) {
4039
5957
  try {
4040
- aggregatedModels = await readFile14(aggregatedPath, "utf-8");
5958
+ aggregatedModels = await readFile22(aggregatedPath, "utf-8");
4041
5959
  } catch {
4042
5960
  }
4043
5961
  }
4044
5962
  for (const entry of entries) {
4045
5963
  if (!entry.isDirectory()) continue;
4046
5964
  const blockName = entry.name;
4047
- const jsFile = path21.join(blocksDir, blockName, `${blockName}.js`);
4048
- const hasJs = existsSync19(jsFile);
5965
+ const jsFile = path32.join(blocksDir, blockName, `${blockName}.js`);
5966
+ const hasJs = existsSync29(jsFile);
4049
5967
  results.push({
4050
5968
  name: `blocks/${blockName} has JS`,
4051
5969
  status: hasJs ? "pass" : "warn",
4052
5970
  message: hasJs ? "Found" : `Missing ${blockName}.js`
4053
5971
  });
4054
5972
  if (hasJs) {
4055
- const jsContent = await readFile14(jsFile, "utf-8");
5973
+ const jsContent = await readFile22(jsFile, "utf-8");
4056
5974
  if (jsContent.includes("console.log")) {
4057
5975
  results.push({
4058
5976
  name: `blocks/${blockName} no console.log`,
@@ -4061,17 +5979,17 @@ async function doctor(options = {}) {
4061
5979
  });
4062
5980
  }
4063
5981
  }
4064
- const hasPerBlockModel = existsSync19(path21.join(blocksDir, blockName, `_${blockName}.json`));
5982
+ const hasPerBlockModel = existsSync29(path32.join(blocksDir, blockName, `_${blockName}.json`));
4065
5983
  const hasAggregatedModel = aggregatedModels.includes(`"${blockName}"`);
4066
5984
  results.push({
4067
5985
  name: `blocks/${blockName} UE model`,
4068
5986
  status: hasPerBlockModel || hasAggregatedModel ? "pass" : "warn",
4069
5987
  message: hasPerBlockModel || hasAggregatedModel ? hasPerBlockModel ? `_${blockName}.json` : "in component-models.json" : "no Universal Editor model - authors can't edit this block in UE"
4070
5988
  });
4071
- const metaFile = path21.join(blocksDir, blockName, ".eds-meta.json");
4072
- if (existsSync19(metaFile)) {
5989
+ const metaFile = path32.join(blocksDir, blockName, ".eds-meta.json");
5990
+ if (existsSync29(metaFile)) {
4073
5991
  try {
4074
- const meta = JSON.parse(await readFile14(metaFile, "utf-8"));
5992
+ const meta = JSON.parse(await readFile22(metaFile, "utf-8"));
4075
5993
  const syncDate = new Date(meta.lastSyncedAt);
4076
5994
  const daysSinceSync = Math.floor(
4077
5995
  (Date.now() - syncDate.getTime()) / (1e3 * 60 * 60 * 24)
@@ -4091,11 +6009,11 @@ async function doctor(options = {}) {
4091
6009
  }
4092
6010
  }
4093
6011
  }
4094
- const queryFile = path21.join(projectRoot, "helix-query.yaml");
4095
- if (existsSync19(queryFile)) {
6012
+ const queryFile = path32.join(projectRoot, "helix-query.yaml");
6013
+ if (existsSync29(queryFile)) {
4096
6014
  try {
4097
- const content = await readFile14(queryFile, "utf-8");
4098
- parseYaml(content);
6015
+ const content = await readFile22(queryFile, "utf-8");
6016
+ parseYaml2(content);
4099
6017
  results.push({ name: "helix-query.yaml valid", status: "pass", message: "Valid YAML" });
4100
6018
  } catch {
4101
6019
  results.push({
@@ -4151,10 +6069,10 @@ var audit_loading_exports = {};
4151
6069
  __export(audit_loading_exports, {
4152
6070
  auditLoading: () => auditLoading
4153
6071
  });
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";
6072
+ import { existsSync as existsSync30 } from "fs";
6073
+ import { readFile as readFile23, readdir as readdir6, stat as stat3 } from "fs/promises";
6074
+ import path33 from "path";
6075
+ import chalk16 from "chalk";
4158
6076
  async function auditLoading(options = {}) {
4159
6077
  const projectRoot = findProjectRoot();
4160
6078
  if (!projectRoot) {
@@ -4187,8 +6105,8 @@ async function auditLoading(options = {}) {
4187
6105
  printResults(results);
4188
6106
  }
4189
6107
  async function checkHeadHtml(root, results) {
4190
- const filePath = path22.join(root, "head.html");
4191
- if (!existsSync20(filePath)) {
6108
+ const filePath = path33.join(root, "head.html");
6109
+ if (!existsSync30(filePath)) {
4192
6110
  results.push({
4193
6111
  rule: "head.html exists",
4194
6112
  status: "warn",
@@ -4196,7 +6114,7 @@ async function checkHeadHtml(root, results) {
4196
6114
  });
4197
6115
  return;
4198
6116
  }
4199
- const content = await readFile15(filePath, "utf-8");
6117
+ const content = await readFile23(filePath, "utf-8");
4200
6118
  const lines = content.split("\n");
4201
6119
  for (let i = 0; i < lines.length; i++) {
4202
6120
  const line = lines[i];
@@ -4268,8 +6186,8 @@ async function checkHeadHtml(root, results) {
4268
6186
  }
4269
6187
  }
4270
6188
  async function checkScriptsJs(root, results) {
4271
- const filePath = path22.join(root, "scripts", "scripts.js");
4272
- if (!existsSync20(filePath)) {
6189
+ const filePath = path33.join(root, "scripts", "scripts.js");
6190
+ if (!existsSync30(filePath)) {
4273
6191
  results.push({
4274
6192
  rule: "scripts/scripts.js exists",
4275
6193
  status: "warn",
@@ -4277,7 +6195,7 @@ async function checkScriptsJs(root, results) {
4277
6195
  });
4278
6196
  return;
4279
6197
  }
4280
- const content = await readFile15(filePath, "utf-8");
6198
+ const content = await readFile23(filePath, "utf-8");
4281
6199
  const lines = content.split("\n");
4282
6200
  const hasLoadEager = /function\s+loadEager|loadEager\s*=/i.test(content);
4283
6201
  const hasLoadLazy = /function\s+loadLazy|loadLazy\s*=/i.test(content);
@@ -4355,8 +6273,8 @@ async function checkScriptsJs(root, results) {
4355
6273
  }
4356
6274
  }
4357
6275
  async function checkStylesCss(root, results) {
4358
- const filePath = path22.join(root, "styles", "styles.css");
4359
- if (!existsSync20(filePath)) return;
6276
+ const filePath = path33.join(root, "styles", "styles.css");
6277
+ if (!existsSync30(filePath)) return;
4360
6278
  const stats = await stat3(filePath);
4361
6279
  const sizeKb = stats.size / 1024;
4362
6280
  results.push({
@@ -4365,7 +6283,7 @@ async function checkStylesCss(root, results) {
4365
6283
  message: `${sizeKb.toFixed(1)} KB${sizeKb > 40 ? " \u2014 large CSS competes with 100KB LCP budget" : ""}`,
4366
6284
  file: "styles/styles.css"
4367
6285
  });
4368
- const content = await readFile15(filePath, "utf-8");
6286
+ const content = await readFile23(filePath, "utf-8");
4369
6287
  if (/@import\s/.test(content)) {
4370
6288
  results.push({
4371
6289
  rule: "No @import in styles.css",
@@ -4376,8 +6294,8 @@ async function checkStylesCss(root, results) {
4376
6294
  }
4377
6295
  }
4378
6296
  async function checkDelayedJs(root, results) {
4379
- const filePath = path22.join(root, "scripts", "delayed.js");
4380
- if (!existsSync20(filePath)) {
6297
+ const filePath = path33.join(root, "scripts", "delayed.js");
6298
+ if (!existsSync30(filePath)) {
4381
6299
  results.push({
4382
6300
  rule: "scripts/delayed.js exists",
4383
6301
  status: "info",
@@ -4385,7 +6303,7 @@ async function checkDelayedJs(root, results) {
4385
6303
  });
4386
6304
  return;
4387
6305
  }
4388
- const content = await readFile15(filePath, "utf-8");
6306
+ const content = await readFile23(filePath, "utf-8");
4389
6307
  const stripped = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").trim();
4390
6308
  if (!stripped) {
4391
6309
  results.push({
@@ -4397,15 +6315,15 @@ async function checkDelayedJs(root, results) {
4397
6315
  }
4398
6316
  }
4399
6317
  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 });
6318
+ const blocksDir = path33.join(root, "blocks");
6319
+ if (!existsSync30(blocksDir)) return;
6320
+ const entries = await readdir6(blocksDir, { withFileTypes: true });
4403
6321
  for (const entry of entries) {
4404
6322
  if (!entry.isDirectory()) continue;
4405
6323
  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");
6324
+ const jsFile = path33.join(blocksDir, blockName, `${blockName}.js`);
6325
+ if (!existsSync30(jsFile)) continue;
6326
+ const content = await readFile23(jsFile, "utf-8");
4409
6327
  const lines = content.split("\n");
4410
6328
  const heavyLibs = [
4411
6329
  "jquery",
@@ -4455,9 +6373,9 @@ async function checkBlocks(root, results) {
4455
6373
  for (const entry of entries) {
4456
6374
  if (!entry.isDirectory()) continue;
4457
6375
  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");
6376
+ const cssFile = path33.join(blocksDir, blockName, `${blockName}.css`);
6377
+ if (!existsSync30(cssFile)) continue;
6378
+ const content = await readFile23(cssFile, "utf-8");
4461
6379
  const selectorPattern = /^([a-z][a-z0-9-]*)\s*\{/gm;
4462
6380
  for (const selectorMatch of content.matchAll(selectorPattern)) {
4463
6381
  const selector = selectorMatch[1];
@@ -4473,8 +6391,8 @@ async function checkBlocks(root, results) {
4473
6391
  }
4474
6392
  }
4475
6393
  async function checkAemJs(root, results) {
4476
- const filePath = path22.join(root, "scripts", "aem.js");
4477
- if (!existsSync20(filePath)) {
6394
+ const filePath = path33.join(root, "scripts", "aem.js");
6395
+ if (!existsSync30(filePath)) {
4478
6396
  results.push({
4479
6397
  rule: "scripts/aem.js exists",
4480
6398
  status: "warn",
@@ -4512,12 +6430,12 @@ function printResults(results) {
4512
6430
  grouped.set(key, bucket);
4513
6431
  }
4514
6432
  for (const [file, fileResults] of grouped) {
4515
- logger.info(` ${icon.bullet} ${chalk12.underline(file)}`);
6433
+ logger.info(` ${icon.bullet} ${chalk16.underline(file)}`);
4516
6434
  for (const r of fileResults) {
4517
- const loc = r.line ? chalk12.dim(`:${r.line}`) : "";
6435
+ const loc = r.line ? chalk16.dim(`:${r.line}`) : "";
4518
6436
  logger.info(` ${statusIcon(r.status)} ${r.rule}${loc}`);
4519
6437
  if (r.status !== "pass") {
4520
- logger.info(chalk12.dim(` ${r.message}`));
6438
+ logger.info(chalk16.dim(` ${r.message}`));
4521
6439
  }
4522
6440
  }
4523
6441
  logger.info("");
@@ -4546,14 +6464,14 @@ var audit_security_exports = {};
4546
6464
  __export(audit_security_exports, {
4547
6465
  auditSecurity: () => auditSecurity
4548
6466
  });
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";
6467
+ import { existsSync as existsSync31 } from "fs";
6468
+ import { readFile as readFile24, readdir as readdir7 } from "fs/promises";
6469
+ import path34 from "path";
6470
+ import chalk17 from "chalk";
4553
6471
  async function* walk2(dir) {
4554
- const entries = await readdir5(dir, { withFileTypes: true });
6472
+ const entries = await readdir7(dir, { withFileTypes: true });
4555
6473
  for (const entry of entries) {
4556
- const full = path23.join(dir, entry.name);
6474
+ const full = path34.join(dir, entry.name);
4557
6475
  if (entry.isDirectory()) {
4558
6476
  if (IGNORE_DIRS.has(entry.name)) continue;
4559
6477
  yield* walk2(full);
@@ -4587,11 +6505,11 @@ function scanContent(content, relFile, ext) {
4587
6505
  return findings;
4588
6506
  }
4589
6507
  async function auditDependencies(root) {
4590
- if (!existsSync21(path23.join(root, "package.json"))) {
6508
+ if (!existsSync31(path34.join(root, "package.json"))) {
4591
6509
  return { status: "skipped", note: "no package.json" };
4592
6510
  }
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"))) {
6511
+ const hasLock = existsSync31(path34.join(root, "package-lock.json")) || existsSync31(path34.join(root, "npm-shrinkwrap.json"));
6512
+ if (!hasLock && !existsSync31(path34.join(root, "node_modules"))) {
4595
6513
  return { status: "skipped", note: "no package-lock.json / node_modules \u2014 run npm install" };
4596
6514
  }
4597
6515
  try {
@@ -4623,15 +6541,15 @@ async function auditSecurity(options = {}) {
4623
6541
  }
4624
6542
  const findings = [];
4625
6543
  for await (const file of walk2(projectRoot)) {
4626
- const ext = path23.extname(file).slice(1).toLowerCase();
6544
+ const ext = path34.extname(file).slice(1).toLowerCase();
4627
6545
  if (!SCAN_EXT.has(ext)) continue;
4628
- const rel = path23.relative(projectRoot, file);
6546
+ const rel = path34.relative(projectRoot, file);
4629
6547
  if (rel.endsWith("aem.js") || rel.endsWith("lib-franklin.js")) continue;
4630
- const content = await readFile16(file, "utf-8");
6548
+ const content = await readFile24(file, "utf-8");
4631
6549
  findings.push(...scanContent(content, rel, ext));
4632
6550
  }
4633
6551
  const deps = await auditDependencies(projectRoot);
4634
- const bySeverity = (s) => findings.filter((f) => f.severity === s);
6552
+ const bySeverity = (s) => findings.filter((f2) => f2.severity === s);
4635
6553
  const codeCounts = {
4636
6554
  critical: bySeverity("critical").length,
4637
6555
  high: bySeverity("high").length,
@@ -4673,11 +6591,11 @@ async function auditSecurity(options = {}) {
4673
6591
  const sorted = [...findings].sort(
4674
6592
  (a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity)
4675
6593
  );
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}`));
6594
+ for (const f2 of sorted) {
6595
+ const meta = SEVERITY_META[f2.severity];
6596
+ const loc = chalk17.dim(`${f2.file}:${f2.line}`);
6597
+ logger.info(` ${statusIcon(meta.status)} ${chalk17.bold(meta.label)} ${f2.message} ${loc}`);
6598
+ logger.info(chalk17.dim(` ${f2.snippet}`));
4681
6599
  }
4682
6600
  }
4683
6601
  const summaryLine = summary({
@@ -4884,7 +6802,7 @@ __export(preview_exports2, {
4884
6802
  preview: () => preview,
4885
6803
  resolveAdminConfig: () => resolveAdminConfig
4886
6804
  });
4887
- import chalk14 from "chalk";
6805
+ import chalk18 from "chalk";
4888
6806
  import ora2 from "ora";
4889
6807
  async function preview(paths, options) {
4890
6808
  const projectRoot = findProjectRoot();
@@ -4897,7 +6815,7 @@ async function preview(paths, options) {
4897
6815
  let hasErrors = false;
4898
6816
  for (const r of results) {
4899
6817
  if (r.ok) {
4900
- logger.success(`${r.path} \u2192 ${chalk14.cyan(r.previewUrl || "preview updated")}`);
6818
+ logger.success(`${r.path} \u2192 ${chalk18.cyan(r.previewUrl || "preview updated")}`);
4901
6819
  } else {
4902
6820
  logger.error(`${r.path} \u2014 ${r.message}`);
4903
6821
  hasErrors = true;
@@ -4911,7 +6829,7 @@ function resolveAdminConfig(config, options) {
4911
6829
  const ref = options.ref || config.admin?.ref || "main";
4912
6830
  if (!org || !site) {
4913
6831
  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" } }'));
6832
+ logger.info(chalk18.dim(' { "admin": { "org": "my-org", "site": "my-site" } }'));
4915
6833
  process.exitCode = 1;
4916
6834
  return void 0;
4917
6835
  }
@@ -4933,7 +6851,7 @@ var publish_exports = {};
4933
6851
  __export(publish_exports, {
4934
6852
  publish: () => publish
4935
6853
  });
4936
- import chalk15 from "chalk";
6854
+ import chalk19 from "chalk";
4937
6855
  import ora3 from "ora";
4938
6856
  async function publish(paths, options) {
4939
6857
  const projectRoot = findProjectRoot();
@@ -4946,7 +6864,7 @@ async function publish(paths, options) {
4946
6864
  let hasErrors = false;
4947
6865
  for (const r of results) {
4948
6866
  if (r.ok) {
4949
- logger.success(`${r.path} \u2192 ${chalk15.cyan(r.liveUrl || "published")}`);
6867
+ logger.success(`${r.path} \u2192 ${chalk19.cyan(r.liveUrl || "published")}`);
4950
6868
  } else {
4951
6869
  logger.error(`${r.path} \u2014 ${r.message}`);
4952
6870
  hasErrors = true;
@@ -5045,13 +6963,13 @@ var menu_exports = {};
5045
6963
  __export(menu_exports, {
5046
6964
  mainMenu: () => mainMenu
5047
6965
  });
5048
- import { existsSync as existsSync22 } from "fs";
5049
- import { readdir as readdir6 } from "fs/promises";
5050
- import path24 from "path";
6966
+ import { existsSync as existsSync32 } from "fs";
6967
+ import { readdir as readdir8 } from "fs/promises";
6968
+ import path35 from "path";
5051
6969
  async function listBlockNames(projectRoot) {
5052
- const dir = path24.join(projectRoot, "blocks");
5053
- if (!existsSync22(dir)) return [];
5054
- const entries = await readdir6(dir, { withFileTypes: true });
6970
+ const dir = path35.join(projectRoot, "blocks");
6971
+ if (!existsSync32(dir)) return [];
6972
+ const entries = await readdir8(dir, { withFileTypes: true });
5055
6973
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
5056
6974
  }
5057
6975
  async function mainMenu() {
@@ -5061,6 +6979,8 @@ async function mainMenu() {
5061
6979
  message: "Choose an action",
5062
6980
  choices: [
5063
6981
  { name: "Set up this project (authoring model, CI, naming)", value: "init" },
6982
+ { name: "Generate a page template (initial content)", value: "template" },
6983
+ { name: "Create a disposable sandbox repo on GitHub", value: "sandbox" },
5064
6984
  {
5065
6985
  name: "Generate a block from a design (Figma/Stitch/Canva/Sketch/Framer)",
5066
6986
  value: "from-design"
@@ -5069,7 +6989,10 @@ async function mainMenu() {
5069
6989
  { name: "Scaffold Universal Editor components", value: "scaffold" },
5070
6990
  { name: "Create an empty block", value: "create" },
5071
6991
  { name: "Add a third-party integration (GTM, chat, consent\u2026)", value: "integrate" },
6992
+ { name: "Add fields to a block model (partials)", value: "model" },
5072
6993
  { name: "Instrument a block for analytics (dataLayer)", value: "track" },
6994
+ { name: "Add JSON-LD structured data to a block", value: "schema" },
6995
+ { name: "Check Universal Editor readiness / open the editor", value: "ue" },
5073
6996
  { name: "Preview a block in the browser", value: "preview-block" },
5074
6997
  { name: "Preview / publish pages (Admin API)", value: "admin" },
5075
6998
  { name: "Audit the project", value: "audit" },
@@ -5082,6 +7005,24 @@ async function mainMenu() {
5082
7005
  await initProject2({});
5083
7006
  return;
5084
7007
  }
7008
+ case "template": {
7009
+ const name = await input({
7010
+ message: "Template name",
7011
+ validate: (v) => !!v.trim() || "Required"
7012
+ });
7013
+ const { templateNew: templateNew2 } = await Promise.resolve().then(() => (init_template(), template_exports));
7014
+ await templateNew2(name.trim(), {});
7015
+ return;
7016
+ }
7017
+ case "sandbox": {
7018
+ const name = await input({
7019
+ message: "Sandbox repo name",
7020
+ validate: (v) => /^[a-z0-9][a-z0-9._-]*$/i.test(v.trim()) || "Use a valid repo name"
7021
+ });
7022
+ const { sandboxNew: sandboxNew2 } = await Promise.resolve().then(() => (init_sandbox(), sandbox_exports));
7023
+ await sandboxNew2(name.trim(), {});
7024
+ return;
7025
+ }
5085
7026
  case "from-design": {
5086
7027
  const { blockFromDesign: blockFromDesign2 } = await Promise.resolve().then(() => (init_from_design(), from_design_exports));
5087
7028
  await blockFromDesign2(void 0, {});
@@ -5130,6 +7071,11 @@ async function mainMenu() {
5130
7071
  await previewBlock2(name, {});
5131
7072
  return;
5132
7073
  }
7074
+ case "model": {
7075
+ const { modelAdd: modelAdd2 } = await Promise.resolve().then(() => (init_model(), model_exports));
7076
+ await modelAdd2(void 0, [], {});
7077
+ return;
7078
+ }
5133
7079
  case "track": {
5134
7080
  const projectRoot = findProjectRoot();
5135
7081
  if (!projectRoot) {
@@ -5149,6 +7095,43 @@ async function mainMenu() {
5149
7095
  await trackBlock2(name, {});
5150
7096
  return;
5151
7097
  }
7098
+ case "schema": {
7099
+ const projectRoot = findProjectRoot();
7100
+ if (!projectRoot) {
7101
+ logger.error("Not inside an EDS project (no EDS project markers found).");
7102
+ return;
7103
+ }
7104
+ const blocks = await listBlockNames(projectRoot);
7105
+ if (blocks.length === 0) {
7106
+ logger.warn("No blocks found in blocks/.");
7107
+ return;
7108
+ }
7109
+ const name = await select({
7110
+ message: "Which block?",
7111
+ choices: blocks.map((b) => ({ name: b, value: b }))
7112
+ });
7113
+ const { schemaBlock: schemaBlock2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
7114
+ await schemaBlock2(name, {});
7115
+ return;
7116
+ }
7117
+ case "ue": {
7118
+ const which = await select({
7119
+ message: "Universal Editor:",
7120
+ choices: [
7121
+ { name: "Check readiness (which blocks are editable)", value: "check" },
7122
+ { name: "Open a page in the editor", value: "open" }
7123
+ ]
7124
+ });
7125
+ if (which === "check") {
7126
+ const { ueCheck: ueCheck2 } = await Promise.resolve().then(() => (init_ue3(), ue_exports));
7127
+ await ueCheck2();
7128
+ } else {
7129
+ const pagePath = await input({ message: "Page path", default: "/" });
7130
+ const { ueOpen: ueOpen2 } = await Promise.resolve().then(() => (init_ue3(), ue_exports));
7131
+ await ueOpen2(pagePath.trim() || "/", {});
7132
+ }
7133
+ return;
7134
+ }
5152
7135
  case "admin": {
5153
7136
  const { adminWizard: adminWizard2 } = await Promise.resolve().then(() => (init_admin(), admin_exports));
5154
7137
  await adminWizard2();
@@ -5194,13 +7177,13 @@ var init_menu = __esm({
5194
7177
 
5195
7178
  // src/bin/eds.ts
5196
7179
  init_esm_shims();
5197
- import chalk16 from "chalk";
7180
+ import chalk20 from "chalk";
5198
7181
  import { Command } from "commander";
5199
7182
 
5200
7183
  // package.json
5201
7184
  var package_default = {
5202
7185
  name: "aem-eds-kit",
5203
- version: "0.4.12",
7186
+ version: "0.4.22",
5204
7187
  description: "CLI for AEM Edge Delivery Services with Figma MCP integration",
5205
7188
  type: "module",
5206
7189
  engines: {
@@ -5276,10 +7259,40 @@ program.name("eds").description("CLI for AEM Edge Delivery Services with Figma i
5276
7259
  const opts = thisCommand.opts();
5277
7260
  if (opts.verbose) logger.setLevel("debug");
5278
7261
  if (opts.quiet) logger.setQuiet(true);
5279
- if (opts.color === false) chalk16.level = 0;
7262
+ if (opts.color === false) chalk20.level = 0;
5280
7263
  });
5281
7264
  program.addHelpText("beforeAll", () => logo());
5282
- program.command("init").description("Set up a freshly cloned EDS boilerplate for your authoring model (interactive)").option("--name <name>", "Project name (kebab-case)").option("--authoring <model>", "Authoring model: ue | da | gdrive | sharepoint").option("--mountpoint <url>", "Content source URL (document models)").option("--org <org>", "DA org (authoring=da)").option("--site <site>", "DA site (authoring=da)").option("--no-ci", "Skip the GitHub Actions CI workflow").option("--yes", "Skip prompts; apply with flags/defaults").action(async (options) => {
7265
+ var sandbox = program.command("sandbox").description("Disposable EDS sandbox repos on GitHub");
7266
+ sandbox.command("new <name>").description("Create a disposable EDS repo from a boilerplate (GitHub auth, no gh required)").option("--org <org>", "Create under a GitHub org (else your account)").option("--boilerplate <repo>", "Template repo (default adobe/aem-boilerplate)").option("--ue", "Use the crosswalk / Universal Editor boilerplate").option("--private", "Create a private repo").option("--yes", "Skip the guided prompts (use flags/defaults)").action(async (name, options) => {
7267
+ const { sandboxNew: sandboxNew2 } = await Promise.resolve().then(() => (init_sandbox(), sandbox_exports));
7268
+ await sandboxNew2(name, options);
7269
+ });
7270
+ sandbox.command("login").description("Authorize GitHub (OAuth device flow or a token) - no gh needed").action(async () => {
7271
+ const { sandboxLogin: sandboxLogin2 } = await Promise.resolve().then(() => (init_sandbox(), sandbox_exports));
7272
+ await sandboxLogin2();
7273
+ });
7274
+ sandbox.command("logout").description("Remove the stored GitHub token").action(async () => {
7275
+ const { sandboxLogout: sandboxLogout2 } = await Promise.resolve().then(() => (init_sandbox(), sandbox_exports));
7276
+ await sandboxLogout2();
7277
+ });
7278
+ sandbox.command("list").description("List your EDS sandboxes (repos tagged eds-sandbox)").action(async () => {
7279
+ const { sandboxList: sandboxList2 } = await Promise.resolve().then(() => (init_sandbox(), sandbox_exports));
7280
+ await sandboxList2();
7281
+ });
7282
+ sandbox.command("rm <name>").description("Delete a sandbox repo (destructive)").option("--org <org>", "Owner org (else your account)").option("--yes", "Skip the confirmation").action(async (name, options) => {
7283
+ const { sandboxRm: sandboxRm2 } = await Promise.resolve().then(() => (init_sandbox(), sandbox_exports));
7284
+ await sandboxRm2(name, options);
7285
+ });
7286
+ var template = program.command("template").description("Generate page templates / initial content");
7287
+ 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(
7288
+ "--blocks <list>",
7289
+ "Comma-separated block ids to seed",
7290
+ (v) => v.split(",").map((b) => b.trim())
7291
+ ).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) => {
7292
+ const { templateNew: templateNew2 } = await Promise.resolve().then(() => (init_template(), template_exports));
7293
+ await templateNew2(name, options);
7294
+ });
7295
+ 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("--no-env", "Skip the local-dev .env (aem up config)").option("--no-xwalk", "Skip xwalk.json (UE multi-field)").option("--yes", "Skip prompts; apply with flags/defaults").action(async (options) => {
5283
7296
  const { initProject: initProject2 } = await Promise.resolve().then(() => (init_init(), init_exports));
5284
7297
  await initProject2(options);
5285
7298
  });
@@ -5288,11 +7301,30 @@ migrate.command("component [dir]").description("Migrate a classic AEM component'
5288
7301
  const { migrateComponent: migrateComponent2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
5289
7302
  await migrateComponent2(dir, options);
5290
7303
  });
7304
+ var model = program.command("model").description("Compose Universal Editor models");
7305
+ 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) => {
7306
+ const { modelAdd: modelAdd2 } = await Promise.resolve().then(() => (init_model(), model_exports));
7307
+ await modelAdd2(blockName, partials, options);
7308
+ });
7309
+ var schema = program.command("schema").description("Structured data (JSON-LD) for blocks");
7310
+ 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) => {
7311
+ const { schemaBlock: schemaBlock2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
7312
+ await schemaBlock2(name, options);
7313
+ });
5291
7314
  var track = program.command("track").description("Instrument blocks for analytics");
5292
7315
  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
7316
  const { trackBlock: trackBlock2 } = await Promise.resolve().then(() => (init_track2(), track_exports));
5294
7317
  await trackBlock2(name, options);
5295
7318
  });
7319
+ var ue = program.command("ue").description("Universal Editor - readiness check + editor deep link");
7320
+ ue.command("check").description("Check whether blocks are Universal-Editor-ready (offline)").action(async () => {
7321
+ const { ueCheck: ueCheck2 } = await Promise.resolve().then(() => (init_ue3(), ue_exports));
7322
+ await ueCheck2();
7323
+ });
7324
+ ue.command("open [path]").description("Open a page in the hosted Universal Editor (preview host from git)").option("--org <slug>", "IMS org slug (skips the org picker)").option("--ref <branch>", "Branch/ref for the preview host (default: main)").option("--url <host>", "Override the delivery host (e.g. main--repo--owner.aem.page)").option("--no-open", "Print the link without opening the browser").action(async (pagePath, options) => {
7325
+ const { ueOpen: ueOpen2 } = await Promise.resolve().then(() => (init_ue3(), ue_exports));
7326
+ await ueOpen2(pagePath, options);
7327
+ });
5296
7328
  var block = program.command("block").description("Manage EDS blocks");
5297
7329
  block.command("create <name>").description("Scaffold a new EDS block (with Universal Editor model by default)").option("--no-ue-model", "Skip the Universal Editor model (generated by default)").action(async (name, options) => {
5298
7330
  const { createBlock: createBlock2 } = await Promise.resolve().then(() => (init_create(), create_exports));
@@ -5328,6 +7360,18 @@ scaffold.command("ci").description("Scaffold a GitHub Actions workflow (doctor +
5328
7360
  const { scaffoldCi: scaffoldCi2 } = await Promise.resolve().then(() => (init_scaffold(), scaffold_exports));
5329
7361
  await scaffoldCi2();
5330
7362
  });
7363
+ scaffold.command("helpers [names...]").description("Scaffold scripts/utils.js with chosen EDS helpers (interactive picker)").action(async (names) => {
7364
+ const { scaffoldHelpers: scaffoldHelpers2 } = await Promise.resolve().then(() => (init_scaffold(), scaffold_exports));
7365
+ await scaffoldHelpers2(names);
7366
+ });
7367
+ scaffold.command("env").description("Write a local-dev .env for `aem up` (AEM_OPEN / AEM_PORT / AEM_PAGES_URL)").option("--open <path>", "AEM_OPEN - path opened on start (default: /)").option("--port <port>", "AEM_PORT - proxy port (default: 3007)").option("--pages-url <url>", "AEM_PAGES_URL - content origin (default: from git remote)").option("--yes", "Skip prompts; use flags/defaults").action(async (options) => {
7368
+ const { scaffoldEnv: scaffoldEnv2 } = await Promise.resolve().then(() => (init_scaffold(), scaffold_exports));
7369
+ await scaffoldEnv2(options);
7370
+ });
7371
+ scaffold.command("xwalk").description("Write xwalk.json enabling Universal Editor multi-field support").action(async () => {
7372
+ const { scaffoldXwalk: scaffoldXwalk2 } = await Promise.resolve().then(() => (init_scaffold(), scaffold_exports));
7373
+ await scaffoldXwalk2();
7374
+ });
5331
7375
  program.command("integrate [type]").description("Add a third-party service (GTM, GA4, chat, consent) to scripts/delayed.js").action(async (type) => {
5332
7376
  const { integrate: integrate2 } = await Promise.resolve().then(() => (init_integrate(), integrate_exports));
5333
7377
  await integrate2(type);