@deneb-ui/cli 2.0.57 → 2.0.59

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
@@ -468,6 +468,11 @@ function getComponentRegistry(importPkg) {
468
468
  component: 'EditableProductGrid',
469
469
  code: `'use client';\n\nimport { EditableProductGrid, ProductGrid, type EditableProductGridProps, type ProductGridItem } from '${importPkg}';\n\nexport { EditableProductGrid, ProductGrid, type EditableProductGridProps, type ProductGridItem };\n`,
470
470
  },
471
+ 'product-showcase': {
472
+ file: 'ProductShowcase.tsx',
473
+ component: 'EditableProductShowcase',
474
+ code: `'use client';\n\nimport { EditableProductShowcase, ProductShowcase, type EditableProductShowcaseProps, type ProductShowcaseItem, type ProductShowcaseColor } from '${importPkg}';\n\nexport { EditableProductShowcase, ProductShowcase, type EditableProductShowcaseProps, type ProductShowcaseItem, type ProductShowcaseColor };\n`,
475
+ },
471
476
  'product-detail': {
472
477
  file: 'ProductDetail.tsx',
473
478
  component: 'EditableProductDetail',
@@ -528,6 +533,11 @@ function getComponentRegistry(importPkg) {
528
533
  component: 'WhatsAppButton',
529
534
  code: `'use client';\n\nimport { WhatsAppButton, type WhatsAppButtonProps } from '${importPkg}';\n\nexport { WhatsAppButton, type WhatsAppButtonProps };\n`,
530
535
  },
536
+ 'whatsapp-order-button': {
537
+ file: 'WhatsAppOrderButton.tsx',
538
+ component: 'WhatsAppOrderButton',
539
+ code: `'use client';\n\nimport React from 'react';\nimport { useSiteData } from '${importPkg}';\nimport { MessageCircle } from 'lucide-react';\n\nexport interface WhatsAppOrderButtonProps {\n product: { name: string; price?: string | number; brand?: string; condition?: string; [key: string]: any };\n selectedColor?: string;\n selectedStorage?: string;\n className?: string;\n showText?: boolean;\n}\n\nexport function WhatsAppOrderButton({\n product,\n selectedColor,\n selectedStorage,\n className = '',\n showText = true,\n}: WhatsAppOrderButtonProps) {\n const siteData = useSiteData();\n const rawTarget =\n siteData?.content?.home?.whatsappOrderUrl ||\n siteData?.content?.home?.whatsappNumber ||\n siteData?.content?.common?.business?.whatsapp ||\n 'https://wa.me/15550192834';\n const orderLabel = siteData?.content?.home?.whatsappOrderLabel ?? 'Order via WhatsApp';\n\n const handleWhatsAppClick = (e: React.MouseEvent) => {\n e.stopPropagation();\n const color = selectedColor || product.colors?.[0]?.name || 'Default';\n const storage = selectedStorage || product.storageOptions?.[0] || 'Default';\n const message = \`Hi, I would like to order the following product:\\n\\n*\${product.name}*\\nBrand: \${product.brand || 'Store'}\\nCondition: \${product.condition || 'New'}\\nColor: \${color}\\nStorage: \${storage}\\nPrice: Rs \${product.price}\\n\\nIs it available?\`;\n\n let targetUrl = (rawTarget || '').trim();\n if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {\n const cleanNum = targetUrl.replace(/[^0-9]/g, '');\n targetUrl = \`https://wa.me/\${cleanNum || '15550192834'}\`;\n }\n const sep = targetUrl.includes('?') ? '&' : '?';\n const url = \`\${targetUrl}\${sep}text=\${encodeURIComponent(message)}\`;\n window.open(url, '_blank', 'noopener,noreferrer');\n };\n\n return (\n <button type=\"button\" onClick={handleWhatsAppClick} className={className}>\n <MessageCircle className=\"h-4 w-4\" />\n {showText && (\n <span data-preview-field-path=\"home.whatsappOrderLabel\" data-preview-style-target=\"home.whatsappOrderLabel\" data-preview-style-type=\"text\">\n {orderLabel}\n </span>\n )}\n </button>\n );\n}\n`,
540
+ },
531
541
  'phone-button': {
532
542
  file: 'PhoneButton.tsx',
533
543
  component: 'PhoneButton',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.57",
3
+ "version": "2.0.59",
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.57",
52
+ "@deneb-ui/core": "^2.0.59",
53
53
  "@octokit/rest": "^22.0.1",
54
54
  "adm-zip": "^0.6.0",
55
55
  "dotenv": "^17.4.2",
@@ -150,6 +150,9 @@ export function isForbiddenTemplatePackagePath(entryName: string) {
150
150
  const hasGeneratedDirectory = segments.some((segment) =>
151
151
  FORBIDDEN_GENERATED_DIRECTORIES.has(segment),
152
152
  );
153
+ const hasDenebInternal = segments.some((segment) =>
154
+ segment === '.deneb' || segment.startsWith('.deneb-backup'),
155
+ );
153
156
  const isEnvironmentFile = fileName === '.env' || fileName.startsWith('.env.');
154
157
  const isNestedArchive = fileName.endsWith('.zip');
155
158
  const isLogFile = fileName.endsWith('.log');
@@ -160,6 +163,7 @@ export function isForbiddenTemplatePackagePath(entryName: string) {
160
163
  hasForbiddenDirectory ||
161
164
  hasYarnCache ||
162
165
  hasGeneratedDirectory ||
166
+ hasDenebInternal ||
163
167
  isEnvironmentFile ||
164
168
  isNestedArchive ||
165
169
  isLogFile ||
@@ -1053,6 +1053,16 @@
1053
1053
  "key": "verifiedService",
1054
1054
  "type": "text",
1055
1055
  "label": "Verified Service"
1056
+ },
1057
+ {
1058
+ "key": "rating",
1059
+ "type": "number",
1060
+ "label": "Rating"
1061
+ },
1062
+ {
1063
+ "key": "device",
1064
+ "type": "text",
1065
+ "label": "Device"
1056
1066
  }
1057
1067
  ]
1058
1068
  },
@@ -1306,11 +1316,6 @@
1306
1316
  "type": "text",
1307
1317
  "label": "Rs Label3"
1308
1318
  },
1309
- {
1310
- "key": "rsLabel4",
1311
- "type": "text",
1312
- "label": "Rs Label4"
1313
- },
1314
1319
  {
1315
1320
  "key": "inStockTestedLabel",
1316
1321
  "type": "text",
@@ -1538,126 +1543,11 @@
1538
1543
  "type": "text",
1539
1544
  "label": "Click Inspect Hardware Label"
1540
1545
  },
1541
- {
1542
- "key": "instantValueCalculatorLabel",
1543
- "type": "text",
1544
- "label": "Instant Value Calculator Label"
1545
- },
1546
- {
1547
- "key": "heading10",
1548
- "type": "text",
1549
- "label": "Heading10"
1550
- },
1551
- {
1552
- "key": "newPossibilitiesLabel",
1553
- "type": "text",
1554
- "label": "New Possibilities Label"
1555
- },
1556
- {
1557
- "key": "description10",
1558
- "type": "textarea",
1559
- "label": "Description10"
1560
- },
1561
- {
1562
- "key": "selectCurrentDeviceBrand",
1563
- "type": "text",
1564
- "label": "Select Current Device Brand"
1565
- },
1566
- {
1567
- "key": "exactModel",
1568
- "type": "text",
1569
- "label": "Exact Model"
1570
- },
1571
- {
1572
- "key": "physicalCondition",
1573
- "type": "text",
1574
- "label": "Physical Condition"
1575
- },
1576
- {
1577
- "key": "targetUpgradeGoal",
1578
- "type": "text",
1579
- "label": "Target Upgrade Goal"
1580
- },
1581
- {
1582
- "key": "msrpRs",
1583
- "type": "text",
1584
- "label": "Msrp Rs"
1585
- },
1586
- {
1587
- "key": "freeDataTransferSecureMilitary",
1588
- "type": "text",
1589
- "label": "Free Data Transfer Secure Military"
1590
- },
1591
- {
1592
- "key": "instantValuationLabel",
1593
- "type": "text",
1594
- "label": "Instant Valuation Label"
1595
- },
1596
- {
1597
- "key": "guaranteed14DaysLabel",
1598
- "type": "text",
1599
- "label": "Guaranteed14 Days Label"
1600
- },
1601
- {
1602
- "key": "estimatedTradeCreditLabel",
1603
- "type": "text",
1604
- "label": "Estimated Trade Credit Label"
1605
- },
1606
1546
  {
1607
1547
  "key": "subtitle7",
1608
1548
  "type": "text",
1609
1549
  "label": "Subtitle7"
1610
1550
  },
1611
- {
1612
- "key": "targetUpgradeLabel",
1613
- "type": "text",
1614
- "label": "Target Upgrade Label"
1615
- },
1616
- {
1617
- "key": "retailPriceLabel",
1618
- "type": "text",
1619
- "label": "Retail Price Label"
1620
- },
1621
- {
1622
- "key": "rsLabel5",
1623
- "type": "text",
1624
- "label": "Rs Label5"
1625
- },
1626
- {
1627
- "key": "lessTradeCreditLabel",
1628
- "type": "text",
1629
- "label": "Less Trade Credit Label"
1630
- },
1631
- {
1632
- "key": "rsLabel6",
1633
- "type": "text",
1634
- "label": "Rs Label6"
1635
- },
1636
- {
1637
- "key": "yourNetUpgradeLabel",
1638
- "type": "text",
1639
- "label": "Your Net Upgrade Label"
1640
- },
1641
- {
1642
- "key": "rsLabel7",
1643
- "type": "text",
1644
- "label": "Rs Label7"
1645
- },
1646
- {
1647
- "key": "estimateLockedPasscodeTrLabel",
1648
- "type": "text",
1649
- "label": "Estimate Locked Passcode Tr Label"
1650
- },
1651
- {
1652
- "key": "lockEstimateUpgradeLabel",
1653
- "type": "text",
1654
- "label": "Lock Estimate Upgrade Label"
1655
- },
1656
- {
1657
- "key": "description11",
1658
- "type": "textarea",
1659
- "label": "Description11"
1660
- },
1661
1551
  {
1662
1552
  "key": "shopPhonesUrl",
1663
1553
  "type": "text",
@@ -1736,6 +1626,31 @@
1736
1626
  "key": "price",
1737
1627
  "type": "number",
1738
1628
  "label": "Price"
1629
+ },
1630
+ {
1631
+ "key": "originalPrice",
1632
+ "type": "number",
1633
+ "label": "Original Price"
1634
+ },
1635
+ {
1636
+ "key": "colors",
1637
+ "type": "list",
1638
+ "label": "Colors",
1639
+ "itemLabel": "Color",
1640
+ "minItems": 1,
1641
+ "maxItems": 6,
1642
+ "fields": [
1643
+ {
1644
+ "key": "name",
1645
+ "type": "text",
1646
+ "label": "Color Name"
1647
+ },
1648
+ {
1649
+ "key": "hex",
1650
+ "type": "text",
1651
+ "label": "Color Hex"
1652
+ }
1653
+ ]
1739
1654
  }
1740
1655
  ]
1741
1656
  },
@@ -1938,11 +1853,6 @@
1938
1853
  "label": "Product Image"
1939
1854
  }
1940
1855
  ]
1941
- },
1942
- {
1943
- "key": "forYourDeviceLabel",
1944
- "type": "text",
1945
- "label": "For Your Device Label"
1946
1856
  }
1947
1857
  ],
1948
1858
  "pageKey": "home"
@@ -674,6 +674,183 @@ function runDoctor(targetDirInput = '.', options = {}) {
674
674
  addCheck(suite5, 'warn', 'Empty-State Singleton Persistence', `${unmountedSingletonCount} singleton label(s) unmount when list is empty. Provide an empty fallback container.`, { code: 'DNB-EMP-002' });
675
675
  }
676
676
 
677
+ // Check: WhatsApp Action Target Synchronization (DNB-WHA-008)
678
+ let disconnectedWhatsAppCount = 0;
679
+ for (const file of sourceFiles) {
680
+ let c = fs.readFileSync(file, 'utf-8');
681
+ if (c.includes('whatsappNumber') && (c.includes('1234567890') || c.includes('siteData?.content?.home?.whatsappNumber')) && !c.includes('whatsappOrderUrl')) {
682
+ disconnectedWhatsAppCount++;
683
+ if (shouldFix) {
684
+ c = c.replace(
685
+ /const\s+whatsappNumber\s*=\s*siteData\?\.content\?\.home\?\.whatsappNumber\s*\?\?\s*["'][^"']+["'];?/g,
686
+ 'const rawTarget = siteData?.content?.home?.whatsappOrderUrl || siteData?.content?.home?.whatsappNumber || siteData?.content?.common?.business?.whatsapp || "https://wa.me/15550192834";'
687
+ );
688
+ c = c.replace(
689
+ /const\s+url\s*=\s*`https:\/\/wa\.me\/\$\{\s*whatsappNumber\.replace\([^)]+\)\s*\}\?text=\$\{([^}]+)\}`;/g,
690
+ 'let targetUrl = (rawTarget || "").trim(); if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) { const cleanNum = targetUrl.replace(/[^0-9]/g, ""); targetUrl = `https://wa.me/${cleanNum || "15550192834"}`; } const sep = targetUrl.includes("?") ? "&" : "?"; const url = `${targetUrl}${sep}text=${$1}`;'
691
+ );
692
+ fs.writeFileSync(file, c, 'utf8');
693
+ }
694
+ }
695
+ }
696
+ if (disconnectedWhatsAppCount === 0) {
697
+ addCheck(suite5, 'pass', 'WhatsApp Action Live Sync', 'All WhatsApp order triggers dynamically resolve configured merchant order URLs', { code: 'DNB-WHA-008' });
698
+ } else if (shouldFix) {
699
+ addCheck(suite5, 'fixed', 'WhatsApp Action Live Sync', `Repaired ${disconnectedWhatsAppCount} WhatsApp action trigger(s) to dynamically resolve whatsappOrderUrl`, { code: 'DNB-WHA-008' });
700
+ } else {
701
+ addCheck(suite5, 'warn', 'WhatsApp Action Live Sync', `${disconnectedWhatsAppCount} WhatsApp trigger(s) using disconnected fallback instead of whatsappOrderUrl. Run with --fix to repair.`, { code: 'DNB-WHA-008' });
702
+ }
703
+
704
+ // Check: Next.js Image Empty String Guard (DNB-IMG-002)
705
+ let emptyImageSrcCount = 0;
706
+ for (const file of sourceFiles) {
707
+ let c = fs.readFileSync(file, 'utf-8');
708
+ if (c.includes('<Image') || c.includes('<img')) {
709
+ const unguardedImageMatches = c.matchAll(/<(?:Image|img)\s+[^>]*\bsrc=\{([a-zA-Z0-9_$.?]+)\}[^>]*>/g);
710
+ let fileModified = false;
711
+ for (const m of unguardedImageMatches) {
712
+ const expr = m[1];
713
+ if (!expr.includes('||') && !expr.includes('?') && !expr.includes('fallback') && !expr.includes('placeholder')) {
714
+ emptyImageSrcCount++;
715
+ if (shouldFix) {
716
+ const safeExpr = `(${expr} && typeof ${expr} === 'string' && ${expr}.trim() !== '') ? ${expr} : '/images/showcase-phone.jpg'`;
717
+ c = c.replace(m[0], m[0].replace(`src={${expr}}`, `src={${safeExpr}}`));
718
+ fileModified = true;
719
+ }
720
+ }
721
+ }
722
+ if (shouldFix && fileModified) {
723
+ fs.writeFileSync(file, c, 'utf8');
724
+ }
725
+ }
726
+ }
727
+ if (emptyImageSrcCount === 0) {
728
+ addCheck(suite5, 'pass', 'Next.js Image Empty-String Guard', 'All Image src attributes are protected against empty string ("") network triggers', { code: 'DNB-IMG-002' });
729
+ } else if (shouldFix) {
730
+ addCheck(suite5, 'fixed', 'Next.js Image Empty-String Guard', `Protected ${emptyImageSrcCount} Image src attribute(s) with safe non-empty fallback guards`, { code: 'DNB-IMG-002' });
731
+ } else {
732
+ addCheck(suite5, 'warn', 'Next.js Image Empty-String Guard', `${emptyImageSrcCount} Image src attribute(s) pass unguarded expressions that may trigger Next.js empty string download errors. Run with --fix to guard.`, { code: 'DNB-IMG-002' });
733
+ }
734
+
735
+ // Check: Product Card Color Swatch & Strikethrough Price Editability (DNB-COL-009)
736
+ let staticSwatchCount = 0;
737
+ let missingColorSchemaCount = 0;
738
+ for (const file of sourceFiles) {
739
+ let c = fs.readFileSync(file, 'utf-8');
740
+ if (c.includes('colors') || c.includes('hex') || c.includes('line-through')) {
741
+ const staticRegex = /data-preview-static="(?:color-swatch|color-name|active-color-name|swatch)"/g;
742
+ if (staticRegex.test(c)) {
743
+ staticSwatchCount++;
744
+ if (shouldFix) {
745
+ c = c.replace(/data-preview-static="(?:color-swatch|swatch)"/g, 'data-preview-field-type="color"');
746
+ c = c.replace(/data-preview-static="(?:color-name|active-color-name)"/g, 'data-preview-style-type="text"');
747
+ fs.writeFileSync(file, c, 'utf8');
748
+ }
749
+ }
750
+ }
751
+ }
752
+
753
+ if (manifestData?.editorSchema?.sections) {
754
+ for (const sec of manifestData.editorSchema.sections) {
755
+ for (const field of sec.fields || []) {
756
+ if (field.type === 'list' && (field.key.toLowerCase().includes('phone') || field.key.toLowerCase().includes('product'))) {
757
+ const hasColorField = (field.fields || []).some((sf) => sf.key === 'colors' || sf.key === 'color');
758
+ const hasPriceField = (field.fields || []).some((sf) => sf.key === 'price');
759
+ const hasOriginalPriceField = (field.fields || []).some((sf) => sf.key === 'originalPrice');
760
+
761
+ let changed = false;
762
+ if (!hasColorField) {
763
+ missingColorSchemaCount++;
764
+ if (shouldFix) {
765
+ field.fields = field.fields || [];
766
+ field.fields.push({
767
+ key: 'colors',
768
+ type: 'list',
769
+ label: 'Colors',
770
+ itemLabel: 'Color',
771
+ minItems: 1,
772
+ maxItems: 6,
773
+ fields: [
774
+ { key: 'name', type: 'text', label: 'Color Name' },
775
+ { key: 'hex', type: 'text', label: 'Color Hex' },
776
+ ],
777
+ });
778
+ changed = true;
779
+ }
780
+ }
781
+ if (hasPriceField && !hasOriginalPriceField) {
782
+ missingColorSchemaCount++;
783
+ if (shouldFix) {
784
+ field.fields = field.fields || [];
785
+ field.fields.push({
786
+ key: 'originalPrice',
787
+ type: 'number',
788
+ label: 'Original Price',
789
+ });
790
+ changed = true;
791
+ }
792
+ }
793
+ if (changed) {
794
+ fs.writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2) + '\n', 'utf8');
795
+ }
796
+ }
797
+ }
798
+ }
799
+ }
800
+
801
+ if (staticSwatchCount === 0 && missingColorSchemaCount === 0) {
802
+ addCheck(suite5, 'pass', 'Product Color Swatch Editability', 'Product color swatches & original prices are fully editable and registered in editorSchema', { code: 'DNB-COL-009' });
803
+ } else if (shouldFix) {
804
+ addCheck(suite5, 'fixed', 'Product Color Swatch Editability', `Made ${staticSwatchCount} static elements editable and registered colors/originalPrice in ${missingColorSchemaCount} schema collection(s)`, { code: 'DNB-COL-009' });
805
+ } else {
806
+ addCheck(suite5, 'warn', 'Product Color Swatch Editability', `${staticSwatchCount} static swatch/label element(s) or ${missingColorSchemaCount} missing schema colors/originalPrice definition(s). Run with --fix to make editable.`, { code: 'DNB-COL-009' });
807
+ }
808
+
809
+ // Check: Review & Testimonial Star Rating Editability (DNB-REV-010)
810
+ let unannotatedStarCount = 0;
811
+ let missingRatingSchemaCount = 0;
812
+
813
+ for (const file of sourceFiles) {
814
+ let c = fs.readFileSync(file, 'utf-8');
815
+ if ((c.includes('<Star') || c.includes('starIdx')) && (c.includes('.rating') || c.includes('ratingNum'))) {
816
+ // Must have data-preview-field-path associated with rating
817
+ const hasRatingFieldPath = /data-preview-field-path=[^>]*\.rating/i.test(c);
818
+ if (!hasRatingFieldPath) {
819
+ unannotatedStarCount++;
820
+ }
821
+ }
822
+ }
823
+
824
+ if (manifestData?.editorSchema?.sections) {
825
+ for (const sec of manifestData.editorSchema.sections) {
826
+ for (const field of sec.fields || []) {
827
+ if (field.type === 'list' && (field.key.toLowerCase().includes('review') || field.key.toLowerCase().includes('testimonial'))) {
828
+ const hasRatingField = (field.fields || []).some((sf) => sf.key === 'rating');
829
+ if (!hasRatingField) {
830
+ missingRatingSchemaCount++;
831
+ if (shouldFix) {
832
+ field.fields = field.fields || [];
833
+ field.fields.push({
834
+ key: 'rating',
835
+ type: 'number',
836
+ label: 'Rating',
837
+ });
838
+ fs.writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2) + '\n', 'utf8');
839
+ }
840
+ }
841
+ }
842
+ }
843
+ }
844
+ }
845
+
846
+ if (unannotatedStarCount === 0 && missingRatingSchemaCount === 0) {
847
+ addCheck(suite5, 'pass', 'Review Star Rating Editability', 'Review star ratings are clickable, editable, and declared in editorSchema', { code: 'DNB-REV-010' });
848
+ } else if (shouldFix) {
849
+ addCheck(suite5, 'fixed', 'Review Star Rating Editability', `Registered rating in ${missingRatingSchemaCount} review schema collection(s) and validated star editability`, { code: 'DNB-REV-010' });
850
+ } else {
851
+ addCheck(suite5, 'warn', 'Review Star Rating Editability', `${unannotatedStarCount} component(s) with unannotated star icons or ${missingRatingSchemaCount} review schema(s) missing rating field. Run with --fix to register.`, { code: 'DNB-REV-010' });
852
+ }
853
+
677
854
  if (!isJson) console.log('\n\x1b[1m[6/6] Multi-Niche Architecture & Asset Security:\x1b[0m');
678
855
  const suite6 = 'Niche Architecture & Security';
679
856