@deneb-ui/cli 2.0.112 → 2.0.114

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/bin/index.js CHANGED
@@ -1276,6 +1276,18 @@ async function initProject(targetInput, options = {}) {
1276
1276
  // Non-blocking doctor check
1277
1277
  }
1278
1278
 
1279
+ // 7.5. Auto-Fix Editability (ensures all cards, text, and photos have field paths + site-data + recipe)
1280
+ try {
1281
+ const { runAutoEditabilityFix } = require('../src/arc/auto-editability-fixer.cjs');
1282
+ console.log('\n\x1b[36m⚡ Running DENEB auto-editability audit & field sync...\x1b[0m');
1283
+ const fixReport = runAutoEditabilityFix(targetDir, { dryRun: false, verbose: false });
1284
+ if (fixReport.fixed && fixReport.fixed.length > 0) {
1285
+ console.log(`\x1b[32m✔ Auto-fixed ${fixReport.fixed.length} editability targets (field paths, site-data, recipe)\x1b[0m`);
1286
+ }
1287
+ } catch (fixErr) {
1288
+ // Non-blocking auto-fix
1289
+ }
1290
+
1279
1291
  console.log(`\n\x1b[32m✔ Project initialization complete!\x1b[0m`);
1280
1292
  console.log(`\nYou can now run:`);
1281
1293
  console.log(` \x1b[36mnpm run lab\x1b[0m \x1b[90m# Launch Local Visual Editing Lab\x1b[0m`);
@@ -1666,6 +1678,34 @@ if (command === 'init' || command === 'arc') {
1666
1678
  console.error(`\x1b[31m✖ Failed to save recipe:\x1b[0m ${err.message}`);
1667
1679
  process.exit(1);
1668
1680
  }
1681
+ } else if (command === 'fix-editability' || command === 'fix-fields') {
1682
+ let targetInput = '.';
1683
+ let dryRun = false;
1684
+ let verbose = false;
1685
+ for (let i = 0; i < commandArgs.length; i++) {
1686
+ const arg = commandArgs[i];
1687
+ if (arg === '--dry-run' || arg === '--dryrun') {
1688
+ dryRun = true;
1689
+ } else if (arg === '--verbose' || arg === '-v') {
1690
+ verbose = true;
1691
+ } else if (!arg.startsWith('-')) {
1692
+ targetInput = arg;
1693
+ }
1694
+ }
1695
+ const targetDir = path.resolve(process.cwd(), targetInput || '.');
1696
+ try {
1697
+ const { runFixEditability } = require('../src/arc/index.cjs');
1698
+ console.log(`\n\x1b[36m⚡ Running Deneb Auto-Editability Fixer on:\x1b[0m ${targetDir}`);
1699
+ const report = runFixEditability(targetDir, { dryRun, verbose });
1700
+ if (report.fixed.length > 0) {
1701
+ console.log(`\n\x1b[32m✔ Auto-fix completed! Updated ${report.fixed.length} target(s).\x1b[0m\n`);
1702
+ } else {
1703
+ console.log(`\n\x1b[32m✔ All cards, text, and photos are already fully editable!\x1b[0m\n`);
1704
+ }
1705
+ } catch (err) {
1706
+ console.error(`\x1b[31m✖ Auto-fix failed:\x1b[0m ${err.message}`);
1707
+ process.exit(1);
1708
+ }
1669
1709
  } else {
1670
1710
  console.log(`Usage: deneb <command> [options]
1671
1711
  DENEB UI Framework — Powered by DENEB-UI Collaborate with FIVORA
@@ -1682,6 +1722,8 @@ Core Commands:
1682
1722
  fonts Google Fonts catalog — list presets or install self-hosted @fontsource packages
1683
1723
  update Update DENEB packages (@deneb-ui/ui, @deneb-ui/cli) and UI components
1684
1724
  validate Validate website configuration and visual editing contracts with Fivora platform
1725
+ fix-editability Auto-fix missing field paths: patches components, populates site-data.json, updates recipe
1726
+ flags: --dry-run --verbose
1685
1727
  zip Zip the project without unnecessary folders or files (node_modules, .next, .git, .env)
1686
1728
  validate-and-zip Validate website configuration and immediately package clean upload-ready ZIP
1687
1729
 
@@ -1714,6 +1756,9 @@ Examples:
1714
1756
  deneb validate-and-zip
1715
1757
  deneb zip .
1716
1758
  deneb add product-card
1717
- deneb add all`);
1759
+ deneb add all
1760
+ deneb fix-editability .
1761
+ deneb fix-editability . --dry-run
1762
+ deneb fix-editability . --verbose`);
1718
1763
  process.exit(1);
1719
1764
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.112",
3
+ "version": "2.0.114",
4
4
  "description": "Official DENEB CLI — scaffold, convert, validate, and package Fivora-ready storefront templates.",
5
5
  "bin": {
6
6
  "deneb": "bin/index.js",
@@ -49,7 +49,7 @@
49
49
  "license": "MIT",
50
50
  "dependencies": {
51
51
  "@babel/parser": "^7.28.0",
52
- "@deneb-ui/core": "^2.0.112",
52
+ "@deneb-ui/core": "^2.0.114",
53
53
  "@octokit/rest": "^22.0.1",
54
54
  "adm-zip": "^0.6.0",
55
55
  "dotenv": "^17.4.2",
@@ -225,6 +225,84 @@ test('AST transformer preserves className and uses nullish fallbacks', () => {
225
225
  parseSource(result.code, relativeFile);
226
226
  });
227
227
 
228
+ test('init marks a variable photo source as an editable image field', () => {
229
+ const relativeFile = 'src/components/DynamicHero.tsx';
230
+ const code = `
231
+ export function DynamicHero() {
232
+ const hero = { src: "/hero.webp" };
233
+ return <Image src={hero.src} alt="Lounge chair" className="rounded" />;
234
+ }
235
+ `;
236
+ const profile = scanProject(FIXTURE);
237
+ const analysis = analyzeFile({
238
+ code,
239
+ relativeFile,
240
+ profile,
241
+ graph: { sharedFiles: [] },
242
+ ownerScope: 'home',
243
+ componentMeta: { name: 'DynamicHero', role: 'hero' },
244
+ });
245
+ analysis.relativeFile = relativeFile;
246
+ analysis.code = code;
247
+ const image = analysis.candidates.find((candidate) => candidate.reason === 'dynamic-image-source');
248
+ assert.ok(image, 'expected a dynamic image candidate');
249
+ const plan = planTransformations({ profile, analyses: [analysis] });
250
+ const result = applyFilePlan(plan.files[0], profile);
251
+ assert.match(result.code, /data-preview-field-path="home\.hero\.heroImage"/);
252
+ assert.match(result.code, /hero\.src/);
253
+ assert.match(result.code, /\|\|/);
254
+ parseSource(result.code, relativeFile);
255
+ });
256
+
257
+ test('init does not assign one shared photo field inside a reusable image component', () => {
258
+ const relativeFile = 'src/components/VehicleImage.tsx';
259
+ const code = `
260
+ export function VehicleImage({ image }) {
261
+ const src = image?.src || "/placeholder.svg";
262
+ return <Image src={src} alt="" />;
263
+ }
264
+ `;
265
+ const profile = scanProject(FIXTURE);
266
+ const analysis = analyzeFile({
267
+ code,
268
+ relativeFile,
269
+ profile,
270
+ graph: { sharedFiles: [] },
271
+ ownerScope: 'common',
272
+ componentMeta: { name: 'VehicleImage', role: 'card' },
273
+ });
274
+ const image = analysis.candidates.find((candidate) => candidate.reason === 'dynamic-image-source');
275
+ assert.equal(image, undefined);
276
+ });
277
+
278
+ test('init marks a home page photo passed in as a prop', () => {
279
+ const relativeFile = 'src/components/Hero.tsx';
280
+ const code = `
281
+ export function Hero({ image }) {
282
+ return <Image src={image.src} alt="Showroom" />;
283
+ }
284
+ `;
285
+ const profile = scanProject(FIXTURE);
286
+ const analysis = analyzeFile({
287
+ code,
288
+ relativeFile,
289
+ profile,
290
+ graph: { sharedFiles: [] },
291
+ ownerScope: 'home',
292
+ componentMeta: { name: 'Hero', role: 'hero' },
293
+ });
294
+ analysis.relativeFile = relativeFile;
295
+ analysis.code = code;
296
+ const image = analysis.candidates.find((candidate) => candidate.reason === 'dynamic-image-source');
297
+ assert.ok(image, 'expected a home photo candidate');
298
+ const plan = planTransformations({ profile, analyses: [analysis] });
299
+ const result = applyFilePlan(plan.files[0], profile);
300
+ assert.match(result.code, /data-preview-field-path="home\.hero\.image"/);
301
+ assert.match(result.code, /image\.src/);
302
+ assert.match(result.code, /\|\|/);
303
+ parseSource(result.code, relativeFile);
304
+ });
305
+
228
306
  test('planner emits style-bind next to content transforms', () => {
229
307
  const relativeFile = 'src/components/Hero.tsx';
230
308
  const code = fs.readFileSync(path.join(FIXTURE, relativeFile), 'utf8');
@@ -3953,9 +4031,38 @@ test('ARC v3 Phase 18: formatBlockedExplanationTerminal renders actionable remed
3953
4031
  }
3954
4032
  });
3955
4033
 
3956
-
3957
-
3958
-
3959
-
3960
-
4034
+ test('ARC Component Registry recognizes EditableUniversalCard and multi-industry card aliases', () => {
4035
+ const { isKnownComponent, classifyComponents } = require('../component-registry.cjs');
4036
+ assert.equal(isKnownComponent('EditableUniversalCard'), true);
4037
+ assert.equal(isKnownComponent('UniversalCard'), true);
4038
+ assert.equal(isKnownComponent('ListingCard'), true);
4039
+ assert.equal(isKnownComponent('EntityCard'), true);
4040
+ assert.equal(isKnownComponent('EditableVehicleCard'), true);
4041
+ assert.equal(isKnownComponent('VehicleCard'), true);
4042
+
4043
+ const res = classifyComponents(['UniversalCard', 'VehicleCard', 'UnknownCustomWidget']);
4044
+ assert.deepEqual(res.known, ['UniversalCard', 'VehicleCard']);
4045
+ assert.deepEqual(res.unknown, ['UnknownCustomWidget']);
4046
+ });
4047
+
4048
+ test('ARC auto-editability fixer handles multi-industry card collections (books, gym plans, restaurants, vehicles)', () => {
4049
+ const { buildGenericCardRecipeSection } = require('../auto-editability-fixer.cjs');
4050
+ const bookCollection = {
4051
+ 'book-1': {
4052
+ title: 'To Kill a Mockingbird',
4053
+ author: 'Harper Lee',
4054
+ year: '1960',
4055
+ price: '$18.50',
4056
+ },
4057
+ };
4058
+ const section = buildGenericCardRecipeSection('books', bookCollection);
4059
+ assert.equal(section.id, 'cards.books');
4060
+ assert.equal(section.type, 'collection');
4061
+ assert.equal(section.items.length, 1);
4062
+ assert.equal(section.items[0].key, 'book-1');
4063
+ const keys = section.items[0].fields.map((f) => f.key);
4064
+ assert.ok(keys.includes('title'));
4065
+ assert.ok(keys.includes('author'));
4066
+ assert.ok(keys.includes('price'));
4067
+ });
3961
4068
 
@@ -31,6 +31,20 @@ const FORM_CONTAINER_TAGS = new Set(['form', 'Form']);
31
31
 
32
32
  const ADAPTERS = BUILTIN_ADAPTERS;
33
33
 
34
+ const UTILITY_ICON_PATTERNS = [
35
+ /^(Chevron|Arrow|Move|Caret|Angle|Sort|Chevrons)/i,
36
+ /^(X|Close|Menu|MenuAlt|Hamburger)/i,
37
+ /^(Loader|Spinner)/i,
38
+ ];
39
+
40
+ function isUtilityIcon(name) {
41
+ if (!name) return false;
42
+ const clean = String(name).replace(/Icon$/i, '');
43
+ if (UTILITY_ICON_PATTERNS.some((re) => re.test(clean))) return true;
44
+ const lower = clean.toLowerCase();
45
+ return ['x', 'close', 'menu', 'spinner', 'loader'].includes(lower);
46
+ }
47
+
34
48
  function isIconComponent(name, importSource) {
35
49
  if (!name) return false;
36
50
  if (DECORATIVE_TAGS.has(name)) return true;
@@ -39,6 +53,22 @@ function isIconComponent(name, importSource) {
39
53
  return false;
40
54
  }
41
55
 
56
+ function isFeatureIcon(name, importSource) {
57
+ if (!name) return false;
58
+ if (DECORATIVE_TAGS.has(name)) return false;
59
+ if (!isIconComponent(name, importSource)) return false;
60
+ return !isUtilityIcon(name);
61
+ }
62
+
63
+ function extractIconBaseName(name) {
64
+ if (!name) return 'circle';
65
+ let clean = String(name).replace(/Icon$/i, '');
66
+ return clean
67
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
68
+ .toLowerCase()
69
+ .trim();
70
+ }
71
+
42
72
  function classifyHref(href) {
43
73
  const value = String(href || '');
44
74
  if (/wa\.me|whatsapp/i.test(value)) return 'whatsapp';
@@ -157,6 +187,9 @@ module.exports = {
157
187
  recognizeWithAdapters,
158
188
  resolveActionWithAdapters,
159
189
  isIconComponent,
190
+ isUtilityIcon,
191
+ isFeatureIcon,
192
+ extractIconBaseName,
160
193
  classifyHref,
161
194
  classifyActionIntent,
162
195
  isLikelyCtaClass,