@deneb-ui/cli 2.0.84 → 2.0.85

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
@@ -112,19 +112,47 @@ function isForbiddenFile(filename) {
112
112
  );
113
113
  }
114
114
 
115
- function stripAnsi(str) {
116
- return str.replace(/\x1b\[[0-9;]*m/g, '');
115
+ function getVisualWidth(str) {
116
+ if (!str) return 0;
117
+ const stripped = String(str).replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
118
+ let width = 0;
119
+ for (const char of stripped) {
120
+ const code = char.codePointAt(0);
121
+ if (
122
+ (code >= 0x1100 && code <= 0x115F) ||
123
+ (code >= 0x2E80 && code <= 0xA4CF) ||
124
+ (code >= 0xAC00 && code <= 0xD7A3) ||
125
+ (code >= 0xF900 && code <= 0xFAFF) ||
126
+ (code >= 0xFE10 && code <= 0xFE19) ||
127
+ (code >= 0xFE30 && code <= 0xFE6F) ||
128
+ (code >= 0xFF00 && code <= 0xFF60) ||
129
+ (code >= 0xFFE0 && code <= 0xFFE6) ||
130
+ (code >= 0x1F000 && code <= 0x1FAFF) ||
131
+ (code >= 0x2600 && code <= 0x27BF)
132
+ ) {
133
+ width += 2;
134
+ } else if (code > 0xFFFF) {
135
+ width += 2;
136
+ } else {
137
+ width += 1;
138
+ }
139
+ }
140
+ return width;
117
141
  }
118
142
 
119
- function createBox(lines, width = 55) {
143
+ function createBox(lines, preferredWidth = 58) {
120
144
  const cyan = '\x1b[36m';
121
145
  const reset = '\x1b[0m';
122
- const top = ` ${cyan}╔${'═'.repeat(width)}╗${reset}`;
123
- const bottom = ` ${cyan}╚${'═'.repeat(width)}╝${reset}`;
146
+ const visualLengths = lines.map((l) => getVisualWidth(l));
147
+ const maxLineVisual = Math.max(...visualLengths, 0);
148
+ const boxWidth = Math.max(preferredWidth, maxLineVisual + 4);
124
149
 
125
- const rows = lines.map((line) => {
126
- const rawLen = stripAnsi(line).length;
127
- const padTotal = Math.max(0, width - rawLen);
150
+ const top = ` ${cyan}╔${'═'.repeat(boxWidth)}╗${reset}`;
151
+ const bottom = ` ${cyan}╚${'═'.repeat(boxWidth)}╝${reset}`;
152
+
153
+ const rows = lines.map((line, idx) => {
154
+ const rawLen = visualLengths[idx];
155
+ const padTotal = Math.max(0, boxWidth - rawLen);
128
156
  const padLeft = Math.floor(padTotal / 2);
129
157
  const padRight = padTotal - padLeft;
130
158
  return ` ${cyan}║${reset}${' '.repeat(padLeft)}${line}${' '.repeat(padRight)}${cyan}║${reset}`;
@@ -1210,11 +1238,16 @@ async function initProject(targetInput, options = {}) {
1210
1238
  }
1211
1239
  }
1212
1240
 
1213
- if (!skipInstall) {
1241
+ if (!skipInstall && !process.env.DENEB_SKIP_FONTS) {
1214
1242
  try {
1215
1243
  const { runFontsInstall } = require('../src/tools/deneb-fonts.cjs');
1244
+ const plannedFontArgs = [];
1245
+ const fontList = conversionRes && Array.isArray(conversionRes.fontIds) ? conversionRes.fontIds : [];
1246
+ for (const id of fontList) {
1247
+ plannedFontArgs.push('--font', id);
1248
+ }
1216
1249
  console.log(`\n\x1b[36mAa Downloading and configuring DENEB Google Fonts...\x1b[0m`);
1217
- runFontsInstall(targetDir, []);
1250
+ runFontsInstall(targetDir, plannedFontArgs);
1218
1251
  } catch (err) {
1219
1252
  console.log(`\n\x1b[33m! Fonts were not installed automatically: ${err.message}. Run \x1b[1mdeneb fonts install .\x1b[0m\x1b[0m`);
1220
1253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.84",
3
+ "version": "2.0.85",
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.84",
52
+ "@deneb-ui/core": "^2.0.85",
53
53
  "@octokit/rest": "^22.0.1",
54
54
  "adm-zip": "^0.6.0",
55
55
  "dotenv": "^17.4.2",
@@ -6,6 +6,8 @@ const fs = require('fs');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
8
 
9
+ process.env.DENEB_HOME = path.join(os.tmpdir(), 'deneb-test-home-' + Date.now());
10
+
9
11
  const { scanProject, buildDependencyGraph } = require('../scanner.cjs');
10
12
  const { analyzeFile } = require('../semantic.cjs');
11
13
  const { planTransformations } = require('../planner.cjs');
@@ -1708,8 +1710,112 @@ test('manifest buildSiteDataAndManifest creates default dual-mode palette with d
1708
1710
  assert.equal(bundle.siteData.theme.dark.textColor, '#f9fafb');
1709
1711
  });
1710
1712
 
1713
+ test('healEmptyStateConditionals preserves modal, null-check, and state-dependent conditional guards', () => {
1714
+ const { parseSource, printSource } = require('../ast.cjs');
1715
+ const { healEmptyStateConditionals } = require('../transformer.cjs');
1716
+ const code = `
1717
+ export function Showcase() {
1718
+ const [lightboxIndex, setLightboxIndex] = useState(null);
1719
+ const [selectedDish, setSelectedDish] = useState(null);
1720
+ return (
1721
+ <div>
1722
+ <AnimatePresence>
1723
+ {lightboxIndex !== null && filteredItems[lightboxIndex] && (
1724
+ <div className="fixed inset-0 z-50">
1725
+ <span data-preview-field-path="home.ofLabel">Caption</span>
1726
+ </div>
1727
+ )}
1728
+ </AnimatePresence>
1729
+ <AnimatePresence>
1730
+ {selectedDish && (
1731
+ <div className="fixed inset-0 z-50">
1732
+ <h3 data-preview-field-path="home.dishLabel">{selectedDish.name}</h3>
1733
+ </div>
1734
+ )}
1735
+ </AnimatePresence>
1736
+ </div>
1737
+ );
1738
+ }
1739
+ `;
1740
+ const ast = parseSource(code, 'src/components/Showcase.tsx');
1741
+ const healed = healEmptyStateConditionals(ast);
1742
+ const result = printSource(ast, code);
1743
+ assert.equal(healed, 0, 'No modal guards should be stripped');
1744
+ assert.match(result, /lightboxIndex !== null/);
1745
+ assert.match(result, /selectedDish &&/);
1746
+ });
1747
+
1748
+ test('M1: auditFivoraContract flags uncovered visible text as a contract violation', () => {
1749
+ const { auditFivoraContract } = require('../index.cjs');
1750
+ const profile = { routes: [{ id: 'home', file: 'src/app/page.tsx' }] };
1751
+ const siteData = { content: { home: { heroTitle: 'Welcome' } } };
1752
+ const manifest = {
1753
+ pages: [{ id: 'home', route: '/' }],
1754
+ editorSchema: {
1755
+ sections: [
1756
+ {
1757
+ id: 'home',
1758
+ path: 'home',
1759
+ type: 'object',
1760
+ fields: [{ key: 'heroTitle', type: 'text' }],
1761
+ },
1762
+ ],
1763
+ },
1764
+ };
1765
+ const inventory = {
1766
+ sources: [
1767
+ {
1768
+ rel: 'src/app/page.tsx',
1769
+ code: 'export default function Page() { return <div><h1><span data-preview-field-path="home.heroTitle">Welcome</span></h1><p>Uncovered paragraph copy here</p></div>; }',
1770
+ },
1771
+ ],
1772
+ pageKeysByFile: { 'src/app/page.tsx': ['home'] },
1773
+ };
1774
+
1775
+ const audit = auditFivoraContract({ profile, siteData, manifest, inventory });
1776
+ assert.equal(audit.passed, false, 'Audit must fail when uncovered visible text exists');
1777
+ assert.ok(audit.uncoveredVisibleText.length > 0, 'Uncovered visible text must be recorded');
1778
+ assert.match(audit.uncoveredVisibleText[0].text, /Uncovered paragraph copy here/);
1779
+ });
1780
+
1781
+ test('M2: ensureStyleAttrs is idempotent and does not duplicate style attributes', () => {
1782
+ const { parseSource, printSource, ensureStyleAttrs } = require('../ast.cjs');
1783
+ const code = '<span data-preview-field-path="home.title">Hello</span>';
1784
+ const ast = parseSource(code, 'test.tsx');
1785
+ const node = ast.program.body[0].expression;
1711
1786
 
1787
+ ensureStyleAttrs(node, 'home.title', 'text');
1788
+ const firstPass = printSource(ast, code);
1789
+ assert.match(firstPass, /data-preview-style-target="home\.title"/);
1790
+ assert.match(firstPass, /data-preview-style-type="text"/);
1712
1791
 
1792
+ // Second pass: must not duplicate attributes
1793
+ ensureStyleAttrs(node, 'home.title', 'text');
1794
+ const secondPass = printSource(ast, code);
1795
+ assert.equal(secondPass, firstPass, 'Second call must be strictly idempotent');
1796
+ });
1713
1797
 
1798
+ test('M3: collectFontIdsFromSiteData resolves planned fonts and theme accurately', () => {
1799
+ const { collectFontIdsFromSiteData, applyFontTheme } = require('../font-plan.cjs');
1800
+ const siteData = {
1801
+ theme: {
1802
+ headingFont: 'Playfair Display',
1803
+ bodyFont: 'Plus Jakarta Sans',
1804
+ },
1805
+ styles: {
1806
+ 'home.accent': { fontFamily: 'Space Grotesk' },
1807
+ },
1808
+ };
1714
1809
 
1810
+ const fontIds = collectFontIdsFromSiteData(siteData);
1811
+ assert.ok(Array.isArray(fontIds));
1812
+ assert.ok(fontIds.includes('playfair-display'));
1813
+ assert.ok(fontIds.includes('plus-jakarta-sans'));
1814
+ assert.ok(fontIds.includes('space-grotesk'));
1815
+
1816
+ const siteDataEmpty = {};
1817
+ applyFontTheme(siteDataEmpty, ['inter', 'playfair-display']);
1818
+ assert.equal(siteDataEmpty.theme.headingFont, 'Playfair Display');
1819
+ assert.equal(siteDataEmpty.theme.bodyFont, 'Inter');
1820
+ });
1715
1821
 
@@ -119,6 +119,9 @@ function rel(projectDir, filePath) {
119
119
  }
120
120
 
121
121
  function homeDenebDir() {
122
+ if (process.env.DENEB_HOME) {
123
+ return path.resolve(process.env.DENEB_HOME);
124
+ }
122
125
  return path.join(os.homedir(), '.deneb');
123
126
  }
124
127
 
package/src/arc/index.cjs CHANGED
@@ -21,6 +21,7 @@ const { applyFilePlan, instrumentLayoutSource, instrumentPageKey, resolveSiteDat
21
21
  const { applyResidualPass } = require('./residual.cjs');
22
22
  const { parseSource } = require('./ast.cjs');
23
23
  const { buildSiteDataAndManifest, writeDataBank, loadExistingData, countSchemaFields } = require('./manifest.cjs');
24
+ const { collectFontIdsFromSiteData } = require('./font-plan.cjs');
24
25
  const { validateAstFiles, validateContracts, designPreservationScore, coverageMetrics } = require('./validator.cjs');
25
26
  const { recordExperience, registryArchitecture } = require('./learning.cjs');
26
27
  const { matchRecipeV2 } = require('./recipes-v2.cjs');
@@ -785,7 +786,11 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
785
786
  }
786
787
  }
787
788
  outcome = 'rolled-back';
788
- printer.printRollback('Strict Fivora contract failed');
789
+ const reasons = [];
790
+ if (!fivoraAudit.passed) reasons.push(`${fivoraAudit.errors.length} Fivora contract error(s)`);
791
+ if (fivoraAudit.uncoveredVisibleText.length > 0) reasons.push(`${fivoraAudit.uncoveredVisibleText.length} uncovered visible text node(s)`);
792
+ if (designFailure) reasons.push(`Design preservation score below threshold (${design.score} < 98)`);
793
+ printer.printRollback(`Strict Fivora contract failed: ${reasons.join(', ')}`);
789
794
  } else if (contractFailure) {
790
795
  outcome = 'contract-failed';
791
796
  } else if (designFailure) {
@@ -862,6 +867,7 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
862
867
  designPreservation: design.score,
863
868
  validation,
864
869
  outcome,
870
+ fontIds: collectFontIdsFromSiteData(dataBundle.siteData),
865
871
  };
866
872
 
867
873
  const report = buildReport({
@@ -954,6 +960,7 @@ module.exports = {
954
960
  runDenebArc,
955
961
  parseArcOptions,
956
962
  scanProject,
963
+ auditFivoraContract,
957
964
  ENGINE_ID,
958
965
  ARC_VERSION,
959
966
  };
@@ -1624,6 +1624,108 @@ function findFirstLeafChild(node) {
1624
1624
  return null;
1625
1625
  }
1626
1626
 
1627
+ function isUnsafeToStripCondition(leftExpr, rightExpr, pathNode) {
1628
+ if (!leftExpr || !rightExpr) return true;
1629
+
1630
+ // 1. Collect all identifiers used on the left-hand side
1631
+ const leftIdentifiers = new Set();
1632
+ recast.types.visit(leftExpr, {
1633
+ visitIdentifier(idPath) {
1634
+ leftIdentifiers.add(idPath.node.name);
1635
+ return false;
1636
+ },
1637
+ });
1638
+
1639
+ if (leftIdentifiers.size === 0) return true;
1640
+
1641
+ // 2. State & modal variable keywords (e.g., selectedDish, lightboxIndex, isOpen, activeTab)
1642
+ const stateKeywordRegex = /(?:open|active|selected|current|expanded|visible|show|modal|dialog|lightbox|drawer|loading|tab|filter|step|index)/i;
1643
+ for (const id of leftIdentifiers) {
1644
+ if (stateKeywordRegex.test(id)) {
1645
+ return true; // NEVER strip modal/state/selection guards
1646
+ }
1647
+ }
1648
+
1649
+ // 3. Null / undefined / equality / comparison checks
1650
+ let hasComparisonOrNullCheck = false;
1651
+ recast.types.visit(leftExpr, {
1652
+ visitBinaryExpression(bPath) {
1653
+ const op = bPath.node.operator;
1654
+ if (['!==', '!=', '===', '==', '>', '<', '>=', '<='].includes(op)) {
1655
+ hasComparisonOrNullCheck = true;
1656
+ }
1657
+ this.traverse(bPath);
1658
+ },
1659
+ visitUnaryExpression(uPath) {
1660
+ if (uPath.node.operator === '!') {
1661
+ hasComparisonOrNullCheck = true;
1662
+ }
1663
+ this.traverse(uPath);
1664
+ },
1665
+ });
1666
+ if (hasComparisonOrNullCheck) return true;
1667
+
1668
+ // 4. Check if ANY identifier in leftExpr is referenced inside rightExpr
1669
+ let rightReferencesLeftId = false;
1670
+ recast.types.visit(rightExpr, {
1671
+ visitIdentifier(idPath) {
1672
+ if (leftIdentifiers.has(idPath.node.name)) {
1673
+ rightReferencesLeftId = true;
1674
+ return false;
1675
+ }
1676
+ this.traverse(idPath);
1677
+ },
1678
+ });
1679
+ if (rightReferencesLeftId) return true;
1680
+
1681
+ // 5. Check if rightExpr or any of its ancestors is an interactive modal / dialog / overlay / AnimatePresence
1682
+ let curr = pathNode;
1683
+ while (curr && curr.parent) {
1684
+ const parentNode = curr.parent.node;
1685
+ if (parentNode && parentNode.type === 'JSXElement') {
1686
+ const parentTag = getJsxName(parentNode);
1687
+ if (/^(?:AnimatePresence|Dialog|Modal|Lightbox|Drawer|Popup|Sheet|Portal)$/i.test(parentTag)) {
1688
+ return true;
1689
+ }
1690
+ }
1691
+ curr = curr.parent;
1692
+ }
1693
+
1694
+ let isModalOrOverlay = false;
1695
+ recast.types.visit(rightExpr, {
1696
+ visitJSXElement(elPath) {
1697
+ const node = elPath.node;
1698
+ const tag = getJsxName(node);
1699
+ if (/^(?:AnimatePresence|Dialog|Modal|Lightbox|Drawer|Popup|Sheet|Portal)$/i.test(tag)) {
1700
+ isModalOrOverlay = true;
1701
+ return false;
1702
+ }
1703
+ const classAttr = findJsxAttribute(node, 'className') || findJsxAttribute(node, 'class');
1704
+ if (classAttr && classAttr.value) {
1705
+ const val = classAttr.value.value || '';
1706
+ if (typeof val === 'string' && (/fixed\s+inset-0/i.test(val) || /z-(?:50|\[50\]|40)/i.test(val) || /backdrop-blur/i.test(val))) {
1707
+ isModalOrOverlay = true;
1708
+ return false;
1709
+ }
1710
+ }
1711
+ const roleAttr = findJsxAttribute(node, 'role');
1712
+ if (roleAttr && roleAttr.value && /^(?:dialog|alertdialog)$/i.test(String(roleAttr.value.value || ''))) {
1713
+ isModalOrOverlay = true;
1714
+ return false;
1715
+ }
1716
+ this.traverse(elPath);
1717
+ },
1718
+ });
1719
+ if (isModalOrOverlay) return true;
1720
+
1721
+ // 6. Only allow stripping if leftExpr is purely a direct siteData content member chain
1722
+ const leftCode = recast.print(leftExpr).code;
1723
+ const isDirectContentCheck = /siteData(?:\?\.|\.)content(?:\?\.|\.)/i.test(leftCode);
1724
+ if (!isDirectContentCheck) return true;
1725
+
1726
+ return false;
1727
+ }
1728
+
1627
1729
  function healEmptyStateConditionals(ast) {
1628
1730
  let healed = 0;
1629
1731
 
@@ -1650,9 +1752,11 @@ function healEmptyStateConditionals(ast) {
1650
1752
  visitLogicalExpression(pathNode) {
1651
1753
  const expr = pathNode.node;
1652
1754
  if (expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
1653
- pathNode.replace(expr.right);
1654
- healed++;
1655
- return false;
1755
+ if (!isUnsafeToStripCondition(expr.left, expr.right, pathNode)) {
1756
+ pathNode.replace(expr.right);
1757
+ healed++;
1758
+ return false;
1759
+ }
1656
1760
  }
1657
1761
  this.traverse(pathNode);
1658
1762
  },
@@ -1663,20 +1767,24 @@ function healEmptyStateConditionals(ast) {
1663
1767
  return;
1664
1768
  }
1665
1769
  if (expr.type === 'LogicalExpression' && expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
1666
- pathNode.node.expression = expr.right;
1667
- healed++;
1668
- return false;
1770
+ if (!isUnsafeToStripCondition(expr.left, expr.right, pathNode)) {
1771
+ pathNode.node.expression = expr.right;
1772
+ healed++;
1773
+ return false;
1774
+ }
1669
1775
  }
1670
1776
  if (
1671
1777
  expr.type === 'ConditionalExpression' &&
1672
1778
  jsxHasPreviewMarker(expr.consequent) &&
1673
1779
  (isNullish(expr.alternate) || isNullish(expr.consequent))
1674
1780
  ) {
1675
- pathNode.node.expression = jsxHasPreviewMarker(expr.consequent) && !isNullish(expr.consequent)
1676
- ? expr.consequent
1677
- : expr.alternate;
1678
- healed++;
1679
- return false;
1781
+ if (!isUnsafeToStripCondition(expr.test, expr.consequent, pathNode)) {
1782
+ pathNode.node.expression = jsxHasPreviewMarker(expr.consequent) && !isNullish(expr.consequent)
1783
+ ? expr.consequent
1784
+ : expr.alternate;
1785
+ healed++;
1786
+ return false;
1787
+ }
1680
1788
  }
1681
1789
  this.traverse(pathNode);
1682
1790
  },
@@ -1928,6 +2036,77 @@ function healMissingSiteDataHooks(code, filePath = 'file.tsx') {
1928
2036
  return code;
1929
2037
  }
1930
2038
 
2039
+ function healUnguardedModalConditionals(ast) {
2040
+ let healed = 0;
2041
+ const stateVariables = [];
2042
+ recast.types.visit(ast, {
2043
+ visitVariableDeclarator(pathNode) {
2044
+ const node = pathNode.node;
2045
+ if (node.id && node.id.type === 'ArrayPattern') {
2046
+ const first = node.id.elements[0];
2047
+ if (first && first.type === 'Identifier') {
2048
+ stateVariables.push(first.name);
2049
+ }
2050
+ }
2051
+ this.traverse(pathNode);
2052
+ },
2053
+ });
2054
+
2055
+ if (stateVariables.length === 0) return 0;
2056
+
2057
+ recast.types.visit(ast, {
2058
+ visitJSXElement(pathNode) {
2059
+ const tag = getJsxName(pathNode.node);
2060
+ if (tag === 'AnimatePresence') {
2061
+ for (const child of pathNode.node.children || []) {
2062
+ if (child.type === 'JSXExpressionContainer') {
2063
+ let inner = child.expression;
2064
+ if (inner && inner.type === 'ParenthesizedExpression') {
2065
+ inner = inner.expression;
2066
+ }
2067
+ if (inner && (inner.type === 'JSXElement' || inner.type === 'JSXFragment')) {
2068
+ const innerIds = new Set();
2069
+ recast.types.visit(inner, {
2070
+ visitIdentifier(idPath) {
2071
+ innerIds.add(idPath.node.name);
2072
+ return false;
2073
+ },
2074
+ });
2075
+
2076
+ const matchingVar = stateVariables.find((sv) => innerIds.has(sv));
2077
+ if (matchingVar) {
2078
+ let guardExpr;
2079
+ if (/index/i.test(matchingVar)) {
2080
+ guardExpr = b.binaryExpression('!==', b.identifier(matchingVar), b.nullLiteral());
2081
+ } else {
2082
+ guardExpr = b.identifier(matchingVar);
2083
+ }
2084
+ child.expression = b.logicalExpression('&&', guardExpr, inner);
2085
+ healed++;
2086
+ }
2087
+ }
2088
+ }
2089
+ }
2090
+ }
2091
+ this.traverse(pathNode);
2092
+ },
2093
+ });
2094
+
2095
+ return healed;
2096
+ }
2097
+
2098
+ function healUnguardedModalConditionalsInSource(code, filePath = 'file.tsx') {
2099
+ if (!code.includes('AnimatePresence') && !code.includes('fixed inset-0')) return { code, updated: false };
2100
+ try {
2101
+ const ast = parseSource(code, filePath);
2102
+ const healed = healUnguardedModalConditionals(ast);
2103
+ if (healed > 0) {
2104
+ return { code: printSource(ast, code), updated: true, count: healed };
2105
+ }
2106
+ } catch {}
2107
+ return { code, updated: false };
2108
+ }
2109
+
1931
2110
  module.exports = {
1932
2111
  applyFilePlan,
1933
2112
  applyCollectionTransform,
@@ -1947,6 +2126,8 @@ module.exports = {
1947
2126
  healDecorativeOverlays,
1948
2127
  healLegacyProductDetailLinks,
1949
2128
  healMissingSiteDataHooks,
2129
+ healUnguardedModalConditionals,
2130
+ healUnguardedModalConditionalsInSource,
1950
2131
  injectSiteDataHook,
1951
2132
  };
1952
2133
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const ARC_NAME = 'Deneb ARC';
4
4
  const ARC_FULL_NAME = 'Deneb Adaptive Refactoring Compiler — AI-Augmented';
5
- const ARC_VERSION = '2.1.0';
5
+ const ARC_VERSION = '2.2.0';
6
6
  const SCHEMA_VERSION = 2;
7
7
  const ENGINE_ID = 'deneb-arc';
8
8
 
@@ -13,17 +13,49 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const { spawnSync } = require('child_process');
15
15
  const { matchRecipeForProject, loadAllRecipes } = require('./recipe-engine.cjs');
16
- const { healMissingSiteDataHooks, healBroadContainerMarkers, healSectionOverflowHidden } = require('../arc/transformer.cjs');
16
+ const { healMissingSiteDataHooks, healBroadContainerMarkers, healSectionOverflowHidden, healUnguardedModalConditionalsInSource } = require('../arc/transformer.cjs');
17
+
18
+ function getVisualWidth(str) {
19
+ if (!str) return 0;
20
+ const stripped = String(str).replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
21
+ let width = 0;
22
+ for (const char of stripped) {
23
+ const code = char.codePointAt(0);
24
+ if (
25
+ (code >= 0x1100 && code <= 0x115F) ||
26
+ (code >= 0x2E80 && code <= 0xA4CF) ||
27
+ (code >= 0xAC00 && code <= 0xD7A3) ||
28
+ (code >= 0xF900 && code <= 0xFAFF) ||
29
+ (code >= 0xFE10 && code <= 0xFE19) ||
30
+ (code >= 0xFE30 && code <= 0xFE6F) ||
31
+ (code >= 0xFF00 && code <= 0xFF60) ||
32
+ (code >= 0xFFE0 && code <= 0xFFE6) ||
33
+ (code >= 0x1F000 && code <= 0x1FAFF) ||
34
+ (code >= 0x2600 && code <= 0x27BF)
35
+ ) {
36
+ width += 2;
37
+ } else if (code > 0xFFFF) {
38
+ width += 2;
39
+ } else {
40
+ width += 1;
41
+ }
42
+ }
43
+ return width;
44
+ }
17
45
 
18
- function createBox(lines, width = 60) {
19
- const horizontal = '═'.repeat(width - 2);
46
+ function createBox(lines, preferredWidth = 66) {
47
+ const visualLengths = lines.map((l) => getVisualWidth(l));
48
+ const maxLineVisual = Math.max(...visualLengths, 0);
49
+ const boxWidth = Math.max(preferredWidth, maxLineVisual + 6);
50
+ const innerWidth = boxWidth - 4;
51
+
52
+ const horizontal = '═'.repeat(boxWidth - 2);
20
53
  const top = ` ╔${horizontal}╗`;
21
54
  const bottom = ` ╚${horizontal}╝`;
22
55
 
23
- const content = lines.map((line) => {
24
- // Strip ANSI colors for length calculation
25
- const stripped = line.replace(/\x1b\[[0-9;]*m/g, '');
26
- const padding = Math.max(0, width - 4 - stripped.length);
56
+ const content = lines.map((line, idx) => {
57
+ const vLen = visualLengths[idx];
58
+ const padding = Math.max(0, innerWidth - vLen);
27
59
  const leftPad = Math.floor(padding / 2);
28
60
  const rightPad = padding - leftPad;
29
61
  return ` ║ ${' '.repeat(leftPad)}${line}${' '.repeat(rightPad)} ║`;
@@ -289,17 +321,29 @@ function runDoctor(targetDirInput = '.', options = {}) {
289
321
  addCheck(suite2, 'err', 'Platform Engine Compatibility', 'Detected packages requiring Node >=22. Run "deneb doctor --fix" to inject Node 20 overrides.', { code: 'DNB-ENG-001' }) //, 'Detected packages requiring Node >=22. Run "deneb doctor --fix" to inject Node 20 overrides.');
290
322
  }
291
323
 
292
- // Pre-heal missing useSiteData hooks before tsc runs when --fix is enabled
324
+ // Pre-heal missing useSiteData hooks and unguarded modals before tsc runs when --fix is enabled
293
325
  if (shouldFix) {
294
326
  for (const file of sourceFiles) {
295
327
  if (!/\.(tsx|jsx|ts|js)$/.test(file)) continue;
296
328
  let c = fs.readFileSync(file, 'utf-8');
329
+ let modified = false;
297
330
  if (c.includes('siteData')) {
298
331
  const healed = healMissingSiteDataHooks(c, file);
299
332
  if (healed && healed !== c) {
300
- fs.writeFileSync(file, healed, 'utf8');
333
+ c = healed;
334
+ modified = true;
335
+ }
336
+ }
337
+ if (c.includes('AnimatePresence') || c.includes('fixed inset-0')) {
338
+ const modalHealed = healUnguardedModalConditionalsInSource(c, file);
339
+ if (modalHealed && modalHealed.updated) {
340
+ c = modalHealed.code;
341
+ modified = true;
301
342
  }
302
343
  }
344
+ if (modified) {
345
+ fs.writeFileSync(file, c, 'utf8');
346
+ }
303
347
  }
304
348
  }
305
349