@deneb-ui/cli 2.0.84 → 2.0.86

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.
@@ -1,14 +1,17 @@
1
1
  'use strict';
2
2
 
3
- const { getJsxName, getJsxAttributeLiteral, hasJsxAttribute, collectJsxText } = require('./ast.cjs');
4
-
5
- function createAdapter(id, impl) {
6
- return { id, ...impl };
7
- }
8
-
9
- function detectFromProfile(profile, id) {
10
- return (profile.componentLibraries || []).includes(id) || profile[id] === true;
11
- }
3
+ const { collectJsxText, getJsxAttributeLiteral } = require('./ast.cjs');
4
+ const {
5
+ AdapterRegistry,
6
+ defaultRegistry,
7
+ BUILTIN_ADAPTERS,
8
+ registerAdapter,
9
+ getAdapter,
10
+ getAllAdapters,
11
+ activeAdapters,
12
+ recognizeWithAdapters,
13
+ resolveActionWithAdapters,
14
+ } = require('./adapters/registry.cjs');
12
15
 
13
16
  const ICON_NAME_RE = /^(Icon|[A-Z][A-Za-z0-9]*(Icon|Logo)|Lucide[A-Z]|HiOutline|HiSolid|Fa[A-Z]|Md[A-Z]|Io[A-Z])/;
14
17
  const DECORATIVE_TAGS = new Set([
@@ -22,151 +25,11 @@ const SKIP_TAGS = new Set([
22
25
  const HEADING_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'Heading', 'Title', 'CardTitle', 'ModalHeader', 'DialogTitle']);
23
26
  const TEXT_TAGS = new Set(['p', 'span', 'li', 'blockquote', 'figcaption', 'label', 'CardDescription', 'Description', 'Subtitle', 'Typography', 'Text', 'Badge', 'badge']);
24
27
  const ACTION_TAGS = new Set(['a', 'Link', 'NavLink', 'Button', 'button', 'IconButton', 'NavbarBrand']);
25
- const IMAGE_TAGS = new Set(['img', 'Image', 'Img', 'BackgroundImage']);
28
+ const IMAGE_TAGS = new Set(['img', 'Image', 'Img', 'BackgroundImage', 'picture']);
26
29
  const FORM_INPUT_TAGS = new Set(['input', 'Input', 'textarea', 'Textarea', 'select', 'Select']);
27
30
  const FORM_CONTAINER_TAGS = new Set(['form', 'Form']);
28
31
 
29
- const shadcn = createAdapter('shadcn', {
30
- detect: (project) => detectFromProfile(project, 'shadcn') || project.shadcn,
31
- recognizeNode(node, ctx) {
32
- const name = getJsxName(node);
33
- if (name === 'Button') {
34
- return { library: 'shadcn', kind: 'button', asChild: hasJsxAttribute(node, 'asChild') };
35
- }
36
- if (name === 'CardTitle' || name === 'CardDescription' || name === 'Badge') {
37
- return { library: 'shadcn', kind: 'text', tag: name };
38
- }
39
- return null;
40
- },
41
- resolveAction(node) {
42
- const name = getJsxName(node);
43
- if (name === 'Button' && hasJsxAttribute(node, 'asChild')) {
44
- const child = (node.children || []).find((c) => c && c.type === 'JSXElement');
45
- if (child && ACTION_TAGS.has(getJsxName(child))) return child;
46
- }
47
- return null;
48
- },
49
- });
50
-
51
- const heroui = createAdapter('heroui', {
52
- detect: (project) => detectFromProfile(project, 'heroui') || project.heroui,
53
- recognizeNode(node) {
54
- const name = getJsxName(node);
55
- if (['Button', 'Link', 'Navbar', 'NavbarBrand', 'NavbarItem', 'NavbarContent'].includes(name)) {
56
- return { library: 'heroui', kind: name === 'Button' || name === 'Link' ? 'action' : 'structure', tag: name };
57
- }
58
- return null;
59
- },
60
- resolveAction(node) {
61
- const name = getJsxName(node);
62
- if (name === 'Button' && (hasJsxAttribute(node, 'href') || hasJsxAttribute(node, 'as'))) return node;
63
- if (name === 'Link') return node;
64
- return null;
65
- },
66
- });
67
-
68
- const nextjs = createAdapter('nextjs', {
69
- detect: (project) => project.framework === 'nextjs',
70
- recognizeNode(node) {
71
- const name = getJsxName(node);
72
- if (name === 'Link') return { library: 'nextjs', kind: 'action', tag: 'Link' };
73
- if (name === 'Image') return { library: 'nextjs', kind: 'image', tag: 'Image' };
74
- return null;
75
- },
76
- resolveAction(node) {
77
- return getJsxName(node) === 'Link' ? node : null;
78
- },
79
- });
80
-
81
- const framer = createAdapter('framer-motion', {
82
- detect: (project) => (project.animationLibraries || []).includes('framer-motion'),
83
- recognizeNode(node) {
84
- const name = getJsxName(node);
85
- if (name.startsWith('motion.')) {
86
- const tag = name.slice('motion.'.length);
87
- return { library: 'framer-motion', kind: HEADING_TAGS.has(tag) || TEXT_TAGS.has(tag) ? 'text' : 'structure', tag };
88
- }
89
- return null;
90
- },
91
- });
92
-
93
- const reactBits = createAdapter('react-bits', {
94
- detect: (project) => project.reactBits,
95
- recognizeNode(node) {
96
- const name = getJsxName(node);
97
- if (/SplitText|BlurText|GradientText|ShinyText|CountUp|RotatingText/.test(name)) {
98
- return { library: 'react-bits', kind: 'text', tag: name };
99
- }
100
- return null;
101
- },
102
- });
103
-
104
- const deneb = createAdapter('deneb', {
105
- detect: (project) =>
106
- (project.componentLibraries || []).includes('@deneb-ui/ui') ||
107
- project['@deneb-ui/ui'] === true ||
108
- Boolean(project.dependencies && project.dependencies['@deneb-ui/ui']),
109
- recognizeNode(node) {
110
- const name = getJsxName(node);
111
- if (['EditableContactForm', 'ContactForm'].includes(name)) {
112
- return { library: 'deneb', kind: 'form', tag: name };
113
- }
114
- if (['EditableGoogleFeedback', 'EditableCustomerReviews', 'CustomerReviews', 'GoogleFeedback'].includes(name)) {
115
- return { library: 'deneb', kind: 'feedback', tag: name };
116
- }
117
- if (['EditableTestimonialSection', 'EditableTestimonialCard', 'TestimonialSection', 'Testimonials'].includes(name)) {
118
- return { library: 'deneb', kind: 'testimonial', tag: name };
119
- }
120
- if (name === 'EditableMap' || name === 'Map') {
121
- return { library: 'deneb', kind: 'map', tag: name };
122
- }
123
- if (['EditableImage', 'EditableText', 'EditableCard', 'EditableProductCard', 'Image', 'Text', 'Card'].includes(name)) {
124
- return { library: 'deneb', kind: 'editable-primitive', tag: name };
125
- }
126
- return null;
127
- },
128
- });
129
-
130
- const radix = createAdapter('radix', {
131
- detect: (project) => (project.componentLibraries || []).includes('radix'),
132
- recognizeNode(node) {
133
- const name = getJsxName(node);
134
- if (name === 'Slot' || name.endsWith('.Root') || name.endsWith('.Trigger')) {
135
- return { library: 'radix', kind: 'structure', tag: name };
136
- }
137
- return null;
138
- },
139
- });
140
-
141
- const ADAPTERS = [nextjs, deneb, shadcn, heroui, framer, reactBits, radix];
142
-
143
- function activeAdapters(profile) {
144
- return ADAPTERS.filter((adapter) => {
145
- try {
146
- return adapter.detect(profile);
147
- } catch {
148
- return false;
149
- }
150
- });
151
- }
152
-
153
- function recognizeWithAdapters(node, ctx, adapters) {
154
- for (const adapter of adapters) {
155
- if (!adapter.recognizeNode) continue;
156
- const result = adapter.recognizeNode(node, ctx);
157
- if (result) return { ...result, adapterId: adapter.id };
158
- }
159
- return null;
160
- }
161
-
162
- function resolveActionWithAdapters(node, adapters) {
163
- for (const adapter of adapters) {
164
- if (!adapter.resolveAction) continue;
165
- const result = adapter.resolveAction(node);
166
- if (result) return result;
167
- }
168
- return null;
169
- }
32
+ const ADAPTERS = BUILTIN_ADAPTERS;
170
33
 
171
34
  function isIconComponent(name, importSource) {
172
35
  if (!name) return false;
@@ -283,6 +146,12 @@ function classifyActionIntent(text, href) {
283
146
  }
284
147
 
285
148
  module.exports = {
149
+ AdapterRegistry,
150
+ defaultRegistry,
151
+ BUILTIN_ADAPTERS,
152
+ registerAdapter,
153
+ getAdapter,
154
+ getAllAdapters,
286
155
  ADAPTERS,
287
156
  activeAdapters,
288
157
  recognizeWithAdapters,
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const recast = require('recast');
4
+ const b = recast.types.builders;
5
+
6
+ /**
7
+ * Canonical Path Abstraction for Deneb ARC.
8
+ * Prevents subtle path drift between runtime siteData getters and visual DOM markers.
9
+ */
10
+ class CanonicalFieldRef {
11
+ constructor(canonicalPath) {
12
+ if (!canonicalPath || typeof canonicalPath !== 'string') {
13
+ throw new Error(`Invalid canonical path: ${canonicalPath}`);
14
+ }
15
+ this.path = canonicalPath;
16
+ this.parts = canonicalPath.split('.').filter(Boolean);
17
+ this.scope = this.parts[0] || 'home';
18
+ this.section = this.parts.length > 2 ? this.parts[1] : null;
19
+ this.fieldName = this.parts[this.parts.length - 1];
20
+ }
21
+
22
+ /**
23
+ * Builds the optional chaining AST expression:
24
+ * siteData?.content?.home?.section?.field
25
+ */
26
+ toGetterAst() {
27
+ let expr = b.identifier('siteData');
28
+ const allParts = ['content', ...this.parts];
29
+ for (const part of allParts) {
30
+ expr = b.optionalMemberExpression(expr, b.identifier(part), false, true);
31
+ }
32
+ return expr;
33
+ }
34
+
35
+ /**
36
+ * Builds the fallback expression:
37
+ * siteData?.content?.home?.section?.field ?? fallback
38
+ */
39
+ toBindingAst(fallbackValue = '') {
40
+ const getter = this.toGetterAst();
41
+ let fallbackAst;
42
+ if (typeof fallbackValue === 'number') {
43
+ fallbackAst = b.literal(fallbackValue);
44
+ } else if (typeof fallbackValue === 'boolean') {
45
+ fallbackAst = b.literal(fallbackValue);
46
+ } else {
47
+ fallbackAst = b.stringLiteral(String(fallbackValue));
48
+ }
49
+ return b.logicalExpression('??', getter, fallbackAst);
50
+ }
51
+
52
+ /**
53
+ * Builds the JSX attribute AST:
54
+ * data-preview-field-path="home.section.field"
55
+ */
56
+ toPreviewAttrAst() {
57
+ return b.jsxAttribute(
58
+ b.jsxIdentifier('data-preview-field-path'),
59
+ b.stringLiteral(this.path)
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Generates a nested item path for sub-collections:
65
+ * e.g. parentPath="home.categories[0]", childProp="products", childIndex="2"
66
+ * -> "home.categories[0].products[2]"
67
+ */
68
+ static buildNestedItemPath(parentItemPath, childProp, childIndexExpr = 'index') {
69
+ return `${parentItemPath}.${childProp}[\${${childIndexExpr}}]`;
70
+ }
71
+ }
72
+
73
+ function isValidCanonicalPath(canonicalPath) {
74
+ if (typeof canonicalPath !== 'string' || !canonicalPath.trim()) return false;
75
+ return /^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+|\[\d+\]|\[\$\{[^}]+\}\])*$/.test(canonicalPath);
76
+ }
77
+
78
+ function validatePathAlignment(getterCode, previewAttrCode) {
79
+ const normalizedGetter = String(getterCode || '').replace(/\?\./g, '.');
80
+ const getterMatch = normalizedGetter.match(/siteData\.content\.([a-zA-Z0-9_.]+)/);
81
+ const attrMatch = String(previewAttrCode || '').match(/data-preview-field-path=["'`{]([a-zA-Z0-9_.]+)/);
82
+ if (!getterMatch || !attrMatch) return false;
83
+ return getterMatch[1] === attrMatch[1];
84
+ }
85
+
86
+ function fieldRef(canonicalPath) {
87
+ return new CanonicalFieldRef(canonicalPath);
88
+ }
89
+
90
+ module.exports = {
91
+ CanonicalFieldRef,
92
+ fieldRef,
93
+ isValidCanonicalPath,
94
+ validatePathAlignment,
95
+ };
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const { unwrapExpr } = require('./ast.cjs');
4
+
5
+ /**
6
+ * Array pipeline methods commonly used before .map()
7
+ */
8
+ const ARRAY_TRANSFORM_METHODS = new Set([
9
+ 'filter',
10
+ 'slice',
11
+ 'sort',
12
+ 'concat',
13
+ 'reverse',
14
+ 'flatMap',
15
+ 'take',
16
+ 'splice',
17
+ ]);
18
+
19
+ /**
20
+ * Unrolls a chained array expression to its root data source identifier.
21
+ * e.g.:
22
+ * products.slice(0, 4) -> 'products'
23
+ * products.filter(p => p.featured).slice(0, 4) -> 'products'
24
+ * Object.values(products) -> 'products'
25
+ * [...featured, ...newItems] -> ['featured', 'newItems']
26
+ */
27
+ function unrollCollectionSource(exprNode, aliasMap = new Map()) {
28
+ if (!exprNode) return null;
29
+ let curr = unwrapExpr(exprNode);
30
+
31
+ // Direct identifier: products
32
+ if (curr.type === 'Identifier') {
33
+ const name = curr.name;
34
+ if (aliasMap.has(name)) {
35
+ return aliasMap.get(name);
36
+ }
37
+ return { rootName: name, chain: [], kind: 'identifier' };
38
+ }
39
+
40
+ // Method call: products.slice(0, 4) or products.filter(...).slice(...)
41
+ if (curr.type === 'CallExpression') {
42
+ const callee = curr.callee;
43
+
44
+ // Object.values(products) or Object.entries(products)
45
+ if (
46
+ callee?.type === 'MemberExpression' &&
47
+ callee.object?.type === 'Identifier' &&
48
+ callee.object.name === 'Object' &&
49
+ callee.property?.type === 'Identifier' &&
50
+ (callee.property.name === 'values' || callee.property.name === 'entries')
51
+ ) {
52
+ const arg = curr.arguments?.[0];
53
+ if (arg?.type === 'Identifier') {
54
+ const root = unrollCollectionSource(arg, aliasMap);
55
+ return {
56
+ rootName: root?.rootName || arg.name,
57
+ chain: ['Object.' + callee.property.name, ...(root?.chain || [])],
58
+ kind: 'object-values',
59
+ };
60
+ }
61
+ }
62
+
63
+ // array.method(...)
64
+ if (callee?.type === 'MemberExpression' && !callee.computed && callee.property?.type === 'Identifier') {
65
+ const methodName = callee.property.name;
66
+ if (ARRAY_TRANSFORM_METHODS.has(methodName) || methodName === 'map') {
67
+ const sub = unrollCollectionSource(callee.object, aliasMap);
68
+ if (sub) {
69
+ return {
70
+ rootName: sub.rootName,
71
+ chain: [methodName, ...sub.chain],
72
+ kind: 'pipeline',
73
+ };
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ // Array spread literal: [...featured, ...newItems]
80
+ if (curr.type === 'ArrayExpression') {
81
+ const roots = [];
82
+ for (const el of curr.elements || []) {
83
+ if (el?.type === 'SpreadElement' && el.argument?.type === 'Identifier') {
84
+ const sub = unrollCollectionSource(el.argument, aliasMap);
85
+ roots.push(sub?.rootName || el.argument.name);
86
+ } else if (el?.type === 'Identifier') {
87
+ const sub = unrollCollectionSource(el, aliasMap);
88
+ roots.push(sub?.rootName || el.name);
89
+ }
90
+ }
91
+ if (roots.length > 0) {
92
+ return {
93
+ rootName: roots[0],
94
+ additionalRoots: roots.slice(1),
95
+ chain: ['spread-merge'],
96
+ kind: 'spread',
97
+ };
98
+ }
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Collects variable aliases inside an AST that point to arrays.
106
+ * e.g. `const featuredProducts = products.slice(0, 4);`
107
+ */
108
+ function collectVariableAliases(ast) {
109
+ const aliases = new Map();
110
+ if (!ast) return aliases;
111
+
112
+ const recast = require('recast');
113
+ recast.types.visit(ast, {
114
+ visitVariableDeclarator(pathNode) {
115
+ const node = pathNode.node;
116
+ if (node.id?.type === 'Identifier' && node.init) {
117
+ const varName = node.id.name;
118
+ const resolved = unrollCollectionSource(node.init, aliases);
119
+ if (resolved && (resolved.rootName || resolved.rootIdentifier)) {
120
+ aliases.set(varName, {
121
+ ...resolved,
122
+ rootIdentifier: resolved.rootIdentifier || resolved.rootName,
123
+ transformMethod: resolved.chain?.[0] || 'slice',
124
+ });
125
+ }
126
+ }
127
+ this.traverse(pathNode);
128
+ },
129
+ });
130
+
131
+ return aliases;
132
+ }
133
+
134
+ module.exports = {
135
+ ARRAY_TRANSFORM_METHODS,
136
+ unrollCollectionSource,
137
+ unrollCollectionPipelines: (ast) => {
138
+ const pipelines = [];
139
+ const recast = require('recast');
140
+ recast.types.visit(ast, {
141
+ visitCallExpression(pathNode) {
142
+ if (pathNode.node.callee?.property?.name === 'map') {
143
+ const res = unrollCollectionSource(pathNode.node.callee.object);
144
+ if (res && res.chain && res.chain.length > 0) {
145
+ pipelines.push({
146
+ rootIdentifier: res.rootName || res.rootIdentifier,
147
+ pipelineChain: [...res.chain].reverse(),
148
+ });
149
+ }
150
+ }
151
+ this.traverse(pathNode);
152
+ },
153
+ });
154
+ return pipelines;
155
+ },
156
+ collectVariableAliases,
157
+ resolveVariableAliases: collectVariableAliases,
158
+ };
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { scanProject } = require('./scanner.cjs');
6
+ const { buildProjectIR } = require('./ir-builder.cjs');
7
+ const { analyzeFile } = require('./semantic.cjs');
8
+ const { planTransformations } = require('./planner.cjs');
9
+ const { applyFilePlan } = require('./transformer.cjs');
10
+
11
+ /**
12
+ * Computes a unified diff string between original and transformed code.
13
+ */
14
+ function createUnifiedDiff(filename, oldCode, newCode) {
15
+ const oldLines = oldCode.split('\n');
16
+ const newLines = newCode.split('\n');
17
+ const diffLines = [];
18
+
19
+ diffLines.push(`--- a/${filename}`);
20
+ diffLines.push(`+++ b/${filename}`);
21
+
22
+ let additions = 0;
23
+ let deletions = 0;
24
+
25
+ // Simple chunk comparison for clear visual inspection
26
+ const max = Math.max(oldLines.length, newLines.length);
27
+ for (let i = 0; i < max; i++) {
28
+ const oldLine = oldLines[i];
29
+ const newLine = newLines[i];
30
+
31
+ if (oldLine === newLine) {
32
+ continue;
33
+ }
34
+
35
+ if (oldLine !== undefined && (newLine === undefined || oldLine.trim() !== newLine.trim())) {
36
+ diffLines.push(`- ${oldLine}`);
37
+ deletions++;
38
+ }
39
+ if (newLine !== undefined && (oldLine === undefined || oldLine.trim() !== newLine.trim())) {
40
+ diffLines.push(`+ ${newLine}`);
41
+ additions++;
42
+ }
43
+ }
44
+
45
+ return {
46
+ filename,
47
+ diff: diffLines.join('\n'),
48
+ additions,
49
+ deletions,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Simulates ARC conversion and generates unified diffs for all modified files.
55
+ * Zero files are written to disk.
56
+ *
57
+ * @param {string} targetDirInput - Target project directory.
58
+ * @param {object} options - Options including profile overrides.
59
+ * @returns {Array} Array of file diff objects.
60
+ */
61
+ function generateArcDiff(targetDirInput = '.', options = {}) {
62
+ const projectDir = path.resolve(targetDirInput);
63
+ const profile = scanProject(projectDir);
64
+ const ir = buildProjectIR(projectDir, profile);
65
+
66
+ const analyses = (profile.jsxFiles || []).map((relativeFile) => {
67
+ const absPath = path.join(projectDir, relativeFile);
68
+ if (!fs.existsSync(absPath)) return null;
69
+ const code = fs.readFileSync(absPath, 'utf8');
70
+ return analyzeFile({
71
+ code,
72
+ relativeFile,
73
+ profile,
74
+ ir,
75
+ });
76
+ }).filter(Boolean);
77
+
78
+ const plan = planTransformations({
79
+ profile,
80
+ analyses,
81
+ ir,
82
+ });
83
+
84
+ const fileDiffs = [];
85
+
86
+ for (const filePlan of plan.files || []) {
87
+ if (!filePlan.transformations?.length || filePlan.skippedFile) continue;
88
+ let result;
89
+ try {
90
+ result = applyFilePlan(filePlan, profile);
91
+ } catch {
92
+ continue;
93
+ }
94
+
95
+ if (result && result.changed && result.code !== filePlan.originalCode) {
96
+ const fileDiff = createUnifiedDiff(filePlan.file, filePlan.originalCode, result.code);
97
+ fileDiffs.push(fileDiff);
98
+ }
99
+ }
100
+
101
+ return fileDiffs;
102
+ }
103
+
104
+ /**
105
+ * Pretty-prints file diffs to stdout.
106
+ */
107
+ function printDiff(fileDiffs) {
108
+ console.log(`\n\x1b[1m\x1b[36mDENEB ARC DIFF\x1b[0m \x1b[90m— Simulated Transformation Diff\x1b[0m`);
109
+ if (!fileDiffs.length) {
110
+ console.log(`\x1b[32m✔ No changes planned or all files already editable.\x1b[0m\n`);
111
+ return;
112
+ }
113
+
114
+ for (const fd of fileDiffs) {
115
+ console.log(`\n\x1b[1m\x1b[34mDiff: ${fd.filename}\x1b[0m \x1b[90m(+${fd.additions} -${fd.deletions})\x1b[0m`);
116
+ const lines = fd.diff.split('\n');
117
+ for (const line of lines) {
118
+ if (line.startsWith('---') || line.startsWith('+++')) {
119
+ console.log(`\x1b[1m${line}\x1b[0m`);
120
+ } else if (line.startsWith('+')) {
121
+ console.log(`\x1b[32m${line}\x1b[0m`);
122
+ } else if (line.startsWith('-')) {
123
+ console.log(`\x1b[31m${line}\x1b[0m`);
124
+ } else {
125
+ console.log(`\x1b[90m${line}\x1b[0m`);
126
+ }
127
+ }
128
+ }
129
+ console.log('');
130
+ }
131
+
132
+ module.exports = {
133
+ createUnifiedDiff,
134
+ generateArcDiff,
135
+ printDiff,
136
+ };
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { analyzeFile } = require('./semantic.cjs');
6
+ const { scanProject } = require('./scanner.cjs');
7
+ const { buildProjectIR } = require('./ir-builder.cjs');
8
+
9
+ /**
10
+ * Explains how Deneb ARC analyzes and intends to refactor a specific component file.
11
+ *
12
+ * @param {string} fileInput - Path to the file to explain.
13
+ * @param {object} options - Options including root project directory and json format flag.
14
+ * @returns {object} Analysis explanation summary.
15
+ */
16
+ function explainFile(fileInput, options = {}) {
17
+ const absPath = path.resolve(options.root || '.', fileInput);
18
+ if (!fs.existsSync(absPath)) {
19
+ throw new Error(`File not found: ${fileInput} (resolved to ${absPath})`);
20
+ }
21
+
22
+ const code = fs.readFileSync(absPath, 'utf8');
23
+ const projectDir = options.root ? path.resolve(options.root) : path.dirname(absPath);
24
+ const relativeFile = path.relative(projectDir, absPath).replace(/\\/g, '/');
25
+
26
+ let profile = options.profile;
27
+ let ir = options.ir;
28
+ if (!profile) {
29
+ try {
30
+ profile = scanProject(projectDir);
31
+ ir = buildProjectIR(projectDir, profile);
32
+ } catch {
33
+ profile = { framework: 'nextjs', root: projectDir };
34
+ ir = null;
35
+ }
36
+ }
37
+
38
+ const analysis = analyzeFile({
39
+ code,
40
+ relativeFile,
41
+ profile,
42
+ ir,
43
+ });
44
+
45
+ const candidates = analysis.candidates || [];
46
+ const editable = candidates.filter((c) => !c.skip);
47
+ const skipped = candidates.filter((c) => c.skip);
48
+
49
+ const explanation = {
50
+ file: relativeFile,
51
+ absolutePath: absPath,
52
+ isTypeScript: /\.(tsx|ts)$/.test(relativeFile),
53
+ alreadyEditable: analysis.alreadyEditable,
54
+ importsCount: Object.keys(analysis.imports || {}).length,
55
+ imports: analysis.imports || {},
56
+ totalCandidates: candidates.length,
57
+ editableCount: editable.length,
58
+ skippedCount: skipped.length,
59
+ editableElements: editable.map((c) => ({
60
+ tag: c.tag,
61
+ kind: c.kind,
62
+ operation: c.operation,
63
+ loc: c.loc,
64
+ value: c.value,
65
+ confidence: c.confidence,
66
+ reason: c.reason,
67
+ proposedField: c.field || `home.${c.kind || 'field'}`,
68
+ })),
69
+ skippedElements: skipped.map((c) => ({
70
+ tag: c.tag,
71
+ kind: c.kind,
72
+ loc: c.loc,
73
+ reason: c.reason,
74
+ confidence: c.confidence,
75
+ })),
76
+ };
77
+
78
+ return explanation;
79
+ }
80
+
81
+ /**
82
+ * Pretty-prints the explanation to stdout.
83
+ */
84
+ function printExplanation(explanation) {
85
+ console.log(`\n\x1b[1m\x1b[36mDENEB ARC EXPLAIN\x1b[0m \x1b[90m— Component Analysis\x1b[0m`);
86
+ console.log(`\x1b[1mFile:\x1b[0m ${explanation.file}`);
87
+ console.log(`\x1b[90mFormat: ${explanation.isTypeScript ? 'TypeScript (TSX)' : 'JavaScript (JSX)'} | Editable: ${explanation.alreadyEditable ? 'Yes' : 'Pending'}\x1b[0m\n`);
88
+
89
+ console.log(`\x1b[1mDiscovered Elements (${explanation.totalCandidates} total, ${explanation.editableCount} editable):\x1b[0m`);
90
+ for (const el of explanation.editableElements) {
91
+ const kindTag = `\x1b[32m[${el.kind.toUpperCase()}]\x1b[0m`;
92
+ const locTag = `\x1b[90m(${el.loc})\x1b[0m`;
93
+ const val = typeof el.value === 'string' ? `"${el.value.slice(0, 40)}${el.value.length > 40 ? '...' : ''}"` : '';
94
+ console.log(` ${kindTag} <${el.tag}> ${locTag} ${val}`);
95
+ console.log(` \x1b[90m↳ Proposed path:\x1b[0m \x1b[33m${el.proposedField}\x1b[0m \x1b[90m(confidence: ${Math.round((el.confidence || 0.8) * 100)}%)\x1b[0m`);
96
+ }
97
+
98
+ if (explanation.skippedElements.length > 0) {
99
+ console.log(`\n\x1b[1mPreserved / Skipped Elements (${explanation.skippedElements.length}):\x1b[0m`);
100
+ for (const el of explanation.skippedElements.slice(0, 10)) {
101
+ console.log(` \x1b[90m[PRESERVED] <${el.tag}> (${el.loc}) — reason: ${el.reason}\x1b[0m`);
102
+ }
103
+ if (explanation.skippedElements.length > 10) {
104
+ console.log(` \x1b[90m... and ${explanation.skippedElements.length - 10} more\x1b[0m`);
105
+ }
106
+ }
107
+ console.log('');
108
+ }
109
+
110
+ module.exports = {
111
+ explainFile,
112
+ printExplanation,
113
+ };