@lakindu_perera/toren 1.0.5 → 1.0.7

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.
@@ -4,7 +4,9 @@
4
4
  * Consumes a {@link ScanResult} and produces styled terminal output.
5
5
  *
6
6
  * Design contract:
7
- * - No business logic. Every value rendered is taken directly from ScanResult.
7
+ * - Presentation-only. Every value rendered comes from ScanResult.
8
+ * Presentation-level derivations (e.g. frameworks list from projectType) are
9
+ * permitted here; business logic is not.
8
10
  * - No imports from the scanner or any domain module.
9
11
  * - Stateless: render() may be called multiple times safely.
10
12
  * - The shape expected here matches the ScanResult typedef in scan.js.
@@ -42,6 +44,47 @@ const C = {
42
44
  /** Maximum files shown in the structure preview. */
43
45
  const PREVIEW_LIMIT = 20;
44
46
 
47
+ // ---------------------------------------------------------------------------
48
+ // Cross-platform capability detection
49
+ // ---------------------------------------------------------------------------
50
+
51
+ function shouldEnableColors() {
52
+ if ('FORCE_COLOR' in process.env) {
53
+ return process.env.FORCE_COLOR !== '0' && process.env.FORCE_COLOR !== 'false';
54
+ }
55
+ if ('NO_COLOR' in process.env) return false;
56
+ if (!process.stdout || !process.stdout.isTTY) return false;
57
+ if (process.env.TERM === 'dumb') return false;
58
+ return true;
59
+ }
60
+
61
+ function isUnicodeSupported() {
62
+ if (process.platform !== 'win32') {
63
+ return process.env.TERM !== 'linux';
64
+ }
65
+ return Boolean(
66
+ process.env.CI ||
67
+ process.env.WT_SESSION ||
68
+ process.env.TERMINUS_SUBLIME ||
69
+ process.env.ConEmuTask === '{cmd::Cmder}' ||
70
+ process.env.TERM_PROGRAM === 'Terminus-Sublime' ||
71
+ process.env.TERM_PROGRAM === 'vscode' ||
72
+ process.env.TERM === 'xterm-256color' ||
73
+ process.env.TERM === 'alacritty' ||
74
+ process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm'
75
+ );
76
+ }
77
+
78
+ const useColors = shouldEnableColors();
79
+ const useUnicode = isUnicodeSupported();
80
+
81
+ const CHARS = {
82
+ dash: useUnicode ? '─' : '-',
83
+ corner: useUnicode ? '└── ' : '\\-- ',
84
+ tee: useUnicode ? '├── ' : '+-- ',
85
+ pipe: useUnicode ? '│ ' : '| ',
86
+ };
87
+
45
88
  // ---------------------------------------------------------------------------
46
89
  // Low-level paint / layout helpers (private to this module)
47
90
  // ---------------------------------------------------------------------------
@@ -53,27 +96,19 @@ const PREVIEW_LIMIT = 20;
53
96
  * @returns {string}
54
97
  */
55
98
  function paint(text, ...codes) {
99
+ if (!useColors) return text;
56
100
  return `${codes.join('')}${text}${C.reset}`;
57
101
  }
58
102
 
59
103
  /**
60
- * Print a full-width horizontal rule (≤ 80 chars).
61
- * @param {string} [char='─']
62
- */
63
- function divider(char = '─') {
64
- const width = Math.min(process.stdout.columns ?? 72, 80);
65
- console.log(paint(char.repeat(width), C.dim));
66
- }
67
-
68
- /**
69
- * Print a titled section header followed by a divider.
70
- * @param {string} emoji
104
+ * Print a titled section header followed by a matched-length divider.
71
105
  * @param {string} title
72
106
  */
73
- function section(emoji, title) {
107
+ function section(title) {
108
+ const cleanTitle = title.replace(/\x1b\[[0-9;]*m/g, '');
109
+ console.log(paint(title, C.bold, C.white));
110
+ console.log(paint(CHARS.dash.repeat(cleanTitle.length), C.dim));
74
111
  console.log('');
75
- console.log(`${emoji} ${paint(title, C.bold, C.white)}`);
76
- divider();
77
112
  }
78
113
 
79
114
  /**
@@ -84,7 +119,32 @@ function section(emoji, title) {
84
119
  */
85
120
  function row(label, value, ...valueCodes) {
86
121
  const coloured = valueCodes.length ? paint(value, ...valueCodes) : value;
87
- console.log(` ${paint(label, C.dim)} ${coloured}`);
122
+ console.log(`${paint(label, C.dim)} ${coloured}`);
123
+ }
124
+
125
+ /**
126
+ * Format a scan duration into a human-readable string.
127
+ * Mirrors the formatDuration helpers in markdown-renderer and html-renderer.
128
+ * @param {number} ms
129
+ * @returns {string}
130
+ */
131
+ function formatDuration(ms) {
132
+ if (ms < 1) return '< 1 ms';
133
+ if (ms >= 1000) return `${(ms / 1000).toFixed(2)} s`;
134
+ return `${Math.round(ms)} ms`;
135
+ }
136
+
137
+ /**
138
+ * Derive a frameworks array from the projectType string.
139
+ * Returns [] when no specific framework is detected (projectType is falsy or 'Unknown').
140
+ * Mirrors the identical derivation in json-renderer.js — both must stay in sync.
141
+ *
142
+ * @param {string} projectType
143
+ * @returns {string[]}
144
+ */
145
+ function deriveFrameworks(projectType) {
146
+ if (!projectType || projectType === 'Unknown') return [];
147
+ return [projectType];
88
148
  }
89
149
 
90
150
  // ---------------------------------------------------------------------------
@@ -107,21 +167,21 @@ function renderTree(node, prefix, isLast, counter, limit = PREVIEW_LIMIT, depth
107
167
  if (counter.maxReached) return;
108
168
  if (depth >= maxDepth) return;
109
169
 
110
- const connector = isLast ? '└── ' : '├── ';
111
- const extension = isLast ? ' ' : '│ ';
170
+ const connector = isLast ? CHARS.corner : CHARS.tee;
171
+ const extension = isLast ? ' ' : CHARS.pipe;
112
172
 
113
173
  if (node.type === 'directory') {
114
- console.log(`${prefix}${connector}${paint(`${node.name}/`, C.bold, C.blue)}`);
174
+ console.log(`${prefix}${connector}${paint(`${node.name}${path.sep}`, C.bold, C.blue)}`);
115
175
  const children = node.children ?? [];
116
176
 
117
177
  if (depth === maxDepth - 1 && children.length > 0) {
118
- console.log(`${prefix}${extension}└── ${paint('...', C.dim)}`);
178
+ console.log(`${prefix}${extension}${CHARS.corner}${paint('...', C.dim)}`);
119
179
  return;
120
180
  }
121
181
 
122
182
  for (let i = 0; i < children.length; i++) {
123
183
  if (counter.count >= limit) {
124
- console.log(`${prefix}${extension}└── ${paint('...', C.dim)}`);
184
+ console.log(`${prefix}${extension}${CHARS.corner}${paint('...', C.dim)}`);
125
185
  counter.maxReached = true;
126
186
  break;
127
187
  }
@@ -137,11 +197,6 @@ function renderTree(node, prefix, isLast, counter, limit = PREVIEW_LIMIT, depth
137
197
  }
138
198
  }
139
199
 
140
- function formatDuration(ms) {
141
- if (ms < 1) return '< 1 ms';
142
- if (ms >= 1000) return `${(ms / 1000).toFixed(2)} s`;
143
- return `${Math.round(ms)} ms`;
144
- }
145
200
 
146
201
  // ---------------------------------------------------------------------------
147
202
  // Banner (private)
@@ -151,8 +206,7 @@ function printBanner() {
151
206
  const name = paint('Toren', C.bold, C.cyan);
152
207
  const version = paint(`v${pkg.version}`, C.dim);
153
208
  const tagline = paint('Codebase Onboarding Intelligence', C.dim);
154
- console.log('');
155
- console.log(` ${name} ${version} — ${tagline}`);
209
+ console.log(`${name} ${version} — ${tagline}`);
156
210
  }
157
211
 
158
212
  // ---------------------------------------------------------------------------
@@ -162,8 +216,8 @@ function printBanner() {
162
216
  /**
163
217
  * Render a ScanResult to the terminal.
164
218
  *
165
- * All sections read exclusively from the ScanResult; no derivations or
166
- * business decisions are made here.
219
+ * All sections read from the ScanResult. Presentation-level derivations
220
+ * (e.g. frameworks list) are computed here; no business logic is added.
167
221
  *
168
222
  * @param {import('../scanner/scan.js').ScanResult} result
169
223
  * @param {{ cwd?: string }} [options]
@@ -189,55 +243,70 @@ export function render(result, options = {}) {
189
243
  console.log('');
190
244
 
191
245
  // ── Summary ───────────────────────────────────────────────────────────────
192
- section('🔍', 'Project Summary');
246
+ section('Project Summary');
193
247
  row('Path: ', paint(relRoot, C.cyan));
194
248
  row('Project type: ', paint(projectType, C.bold, C.green));
195
249
  row('Total files: ', paint(String(flatFiles.length), C.yellow));
196
250
  row('Total folders:', paint(String(totalFolders), C.yellow));
197
- row('Scan duration:', paint(formatDuration(scanDurationMs), C.magenta));
251
+ console.log('');
252
+
253
+ // ── Frameworks ────────────────────────────────────────────────────────────
254
+ section('Frameworks');
255
+ const frameworks = deriveFrameworks(projectType);
256
+ if (frameworks.length === 0) {
257
+ console.log(paint('No frameworks detected.', C.dim));
258
+ } else {
259
+ for (const fw of frameworks) {
260
+ console.log(paint(fw, C.white));
261
+ }
262
+ }
263
+ console.log('');
198
264
 
199
265
  // ── Entry Points ──────────────────────────────────────────────────────────
200
- section('🚪', 'Entry Points');
266
+ section('Entry Points');
201
267
  if (entryPoints.length === 0) {
202
- console.log(paint('No entry points detected (this may be a library or utility project)', C.yellow));
268
+ console.log(paint('No entry points detected.', C.dim));
203
269
  } else {
204
270
  for (const ep of entryPoints) {
205
- console.log(` ${paint('→', C.cyan)} ${paint(ep, C.white)}`);
271
+ console.log(paint(ep, C.white));
206
272
  }
207
273
  }
274
+ console.log('');
208
275
 
209
276
  // ── Configuration Files ───────────────────────────────────────────────────
210
- section('⚙️', 'Configuration Files');
277
+ section('Configuration Files');
211
278
  if (configs.length === 0) {
212
- console.log(paint(' No configuration files detected', C.dim));
279
+ console.log(paint('No configuration files detected.', C.dim));
213
280
  } else {
214
281
  for (const c of configs) {
215
- console.log(` ${paint(c, C.white)}`);
282
+ console.log(paint(c, C.white));
216
283
  }
217
284
  }
285
+ console.log('');
218
286
 
219
287
  // ── Package Scripts ───────────────────────────────────────────────────────
220
- section('📜', 'Package Scripts');
288
+ section('Package Scripts');
221
289
  if (scripts.length === 0) {
222
- console.log(paint(' No package scripts detected', C.dim));
290
+ console.log(paint('No package scripts detected.', C.dim));
223
291
  } else {
224
292
  const maxNameLen = Math.max(...scripts.map(s => s.name.length));
225
293
  for (const s of scripts) {
226
294
  const paddedName = s.name.padEnd(maxNameLen, ' ');
227
- console.log(` ${paint(paddedName, C.white)} ${paint(s.command, C.dim)}`);
295
+ console.log(`${paint(paddedName, C.white)} ${paint(s.command, C.dim)}`);
228
296
  }
229
297
  }
298
+ console.log('');
230
299
 
231
300
  // ── Structure Preview ─────────────────────────────────────────────────────
232
- section('📁', `Folder Structure ${paint(`(first ${PREVIEW_LIMIT} files)`, C.dim)}`);
301
+ section('Folder Structure');
233
302
 
234
- console.log(paint(`${tree.name || '.'}/`, C.bold, C.blue));
303
+ console.log(paint(`${tree.name || '.'}${path.sep}`, C.bold, C.blue));
235
304
 
236
305
  const counter = { count: 0, maxReached: false };
237
306
  const children = tree.children ?? [];
238
307
  for (let i = 0; i < children.length; i++) {
239
308
  if (counter.count >= PREVIEW_LIMIT) {
240
- console.log(`└── ${paint('...', C.dim)}`);
309
+ console.log(`${CHARS.corner}${paint('...', C.dim)}`);
241
310
  break;
242
311
  }
243
312
  renderTree(children[i], '', i === children.length - 1, counter);
@@ -246,13 +315,12 @@ export function render(result, options = {}) {
246
315
 
247
316
  if (flatFiles.length > PREVIEW_LIMIT) {
248
317
  const hidden = flatFiles.length - PREVIEW_LIMIT;
249
- console.log(paint(` … and ${hidden} more file(s) not shown`, C.dim));
318
+ console.log(paint(`… ${hidden} more file(s) not shown`, C.dim));
250
319
  }
251
320
 
252
321
  // ── Footer ────────────────────────────────────────────────────────────────
253
322
  console.log('');
254
- divider();
255
- console.log(paint(' ✅ Scan complete.', C.green));
323
+ console.log(paint(`Scan completed in ${formatDuration(scanDurationMs)}`, C.green));
256
324
  console.log('');
257
325
  }
258
326
 
@@ -264,8 +332,8 @@ export function render(result, options = {}) {
264
332
  export function renderStructure(result) {
265
333
  const { tree, flatFiles } = result;
266
334
 
267
- console.log(paint('Project Structure:', C.bold, C.white));
268
- console.log(paint(`${tree.name || '.'}/`, C.bold, C.blue));
335
+ section('Folder Structure');
336
+ console.log(paint(`${tree.name || '.'}${path.sep}`, C.bold, C.blue));
269
337
 
270
338
  const counter = { count: 0, maxReached: false };
271
339
  const children = tree.children ?? [];
@@ -273,7 +341,7 @@ export function renderStructure(result) {
273
341
 
274
342
  for (let i = 0; i < children.length; i++) {
275
343
  if (counter.count >= limit) {
276
- console.log(`└── ${paint('...', C.dim)}`);
344
+ console.log(`${CHARS.corner}${paint('...', C.dim)}`);
277
345
  break;
278
346
  }
279
347
  renderTree(children[i], '', i === children.length - 1, counter, limit, 0, Infinity);
@@ -31,34 +31,6 @@ import path from 'node:path';
31
31
  // Tree builder (plain-text, HTML-safe — same algorithm as markdown-renderer)
32
32
  // ---------------------------------------------------------------------------
33
33
 
34
- /**
35
- * Build an in-memory nested tree from a flat list of relative file paths.
36
- *
37
- * @param {string[]} flatFiles - Relative file paths produced by scan()
38
- * @returns {{ type: string, children: Record<string, object> }}
39
- */
40
- function buildInternalTree(flatFiles) {
41
- const root = { type: 'directory', children: {} };
42
-
43
- for (const filePath of flatFiles) {
44
- const parts = filePath.split(/[/\\]/).filter(Boolean);
45
- let node = root;
46
-
47
- for (let i = 0; i < parts.length; i++) {
48
- const part = parts[i];
49
- const isLeaf = i === parts.length - 1;
50
-
51
- if (!node.children[part]) {
52
- node.children[part] = isLeaf
53
- ? { type: 'file', name: part }
54
- : { type: 'directory', name: part, children: {} };
55
- }
56
- node = node.children[part];
57
- }
58
- }
59
-
60
- return root;
61
- }
62
34
 
63
35
  /**
64
36
  * Recursively serialise a tree node into classic tree-connector lines.
@@ -80,10 +52,7 @@ function serializeNode(node, prefix, isLast, lines, depth = 0, maxDepth = 5) {
80
52
  lines.push(`${prefix}${connector}${label}`);
81
53
 
82
54
  if (node.type === 'directory') {
83
- const children = Object.values(node.children || {}).sort((a, b) => {
84
- if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
85
- return a.name.localeCompare(b.name);
86
- });
55
+ const children = node.children || [];
87
56
 
88
57
  if (depth === maxDepth - 1 && children.length > 0) {
89
58
  lines.push(`${prefix}${childPad}└── ...`);
@@ -104,21 +73,18 @@ function serializeNode(node, prefix, isLast, lines, depth = 0, maxDepth = 5) {
104
73
  }
105
74
 
106
75
  /**
107
- * Convert a flat file list into a plain-text tree string.
76
+ * Convert a ScanResult tree into a plain-text tree string.
108
77
  *
109
- * @param {string[]} flatFiles
78
+ * @param {import('../scanner/scan.js').DirNode} tree
110
79
  * @param {string} rootName
80
+ * @param {number} totalFiles
111
81
  * @returns {string}
112
82
  */
113
- function buildTreeString(flatFiles, rootName) {
114
- if (flatFiles.length === 0) return 'No files scanned.';
83
+ function buildTreeString(tree, rootName, totalFiles) {
84
+ if (totalFiles === 0) return 'No files scanned.';
115
85
 
116
- const root = buildInternalTree(flatFiles);
117
86
  const lines = [`${rootName}/`];
118
- const children = Object.values(root.children).sort((a, b) => {
119
- if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
120
- return a.name.localeCompare(b.name);
121
- });
87
+ const children = tree.children || [];
122
88
 
123
89
  for (let i = 0; i < children.length; i++) {
124
90
  serializeNode(children[i], '', i === children.length - 1, lines);
@@ -148,12 +114,13 @@ function esc(value) {
148
114
 
149
115
  /**
150
116
  * Format a scan duration in milliseconds to a human-readable string.
117
+ * Returns plain text only — callers are responsible for HTML-escaping via esc().
151
118
  *
152
119
  * @param {number} ms
153
120
  * @returns {string}
154
121
  */
155
122
  function formatDuration(ms) {
156
- if (ms < 1) return '&lt; 1 ms';
123
+ if (ms < 1) return '< 1 ms';
157
124
  if (ms >= 1000) return `${(ms / 1000).toFixed(2)} s`;
158
125
  return `${Math.round(ms)} ms`;
159
126
  }
@@ -632,6 +599,19 @@ const icon = {
632
599
  // Section builders (one function per report section)
633
600
  // ---------------------------------------------------------------------------
634
601
 
602
+ /**
603
+ * Derive a frameworks array from the projectType string.
604
+ * Returns [] when no specific framework is detected.
605
+ * Mirrors the identical derivation in console-renderer.js and json-renderer.js.
606
+ *
607
+ * @param {string} projectType
608
+ * @returns {string[]}
609
+ */
610
+ function deriveFrameworks(projectType) {
611
+ if (!projectType || projectType === 'Unknown') return [];
612
+ return [projectType];
613
+ }
614
+
635
615
  /**
636
616
  * Render the gradient page header.
637
617
  *
@@ -696,7 +676,7 @@ function renderSummaryCards(result) {
696
676
  {
697
677
  label: 'Scan Duration',
698
678
  icon: icon.clock(),
699
- value: formatDuration(scanDurationMs),
679
+ value: esc(formatDuration(scanDurationMs)),
700
680
  isText: true,
701
681
  sub: 'wall-clock time',
702
682
  },
@@ -715,6 +695,41 @@ function renderSummaryCards(result) {
715
695
  return `<div class="cards">${cardHTML}</div>`;
716
696
  }
717
697
 
698
+ /**
699
+ * Render the frameworks section.
700
+ *
701
+ * @param {string} projectType
702
+ * @returns {string}
703
+ */
704
+ function renderFrameworks(projectType) {
705
+ const frameworks = deriveFrameworks(projectType);
706
+ const count = frameworks.length;
707
+
708
+ const body = count === 0
709
+ ? `<p class="empty-msg">No frameworks detected.</p>`
710
+ : `<ul class="entry-list">
711
+ ${frameworks.map(fw => `
712
+ <li class="entry-item">
713
+ <span class="entry-dot"></span>
714
+ ${esc(fw)}
715
+ </li>`).join('')}
716
+ </ul>`;
717
+
718
+ const countBadge = count > 0
719
+ ? `<span class="section-count">${count} found</span>`
720
+ : '';
721
+
722
+ return `
723
+ <section class="section">
724
+ <div class="section-header">
725
+ <div class="section-icon">${icon.code()}</div>
726
+ <h2 class="section-title">Frameworks</h2>
727
+ ${countBadge}
728
+ </div>
729
+ <div class="section-body">${body}</div>
730
+ </section>`;
731
+ }
732
+
718
733
  /**
719
734
  * Render the entry points section.
720
735
  *
@@ -739,14 +754,14 @@ function renderEntryPoints(entryPoints) {
739
754
  : '';
740
755
 
741
756
  return `
742
- <div class="section">
757
+ <section class="section">
743
758
  <div class="section-header">
744
759
  <div class="section-icon">${icon.door()}</div>
745
- <span class="section-title">Entry Points</span>
760
+ <h2 class="section-title">Entry Points</h2>
746
761
  ${countBadge}
747
762
  </div>
748
763
  <div class="section-body">${body}</div>
749
- </div>`;
764
+ </section>`;
750
765
  }
751
766
 
752
767
  /**
@@ -773,14 +788,14 @@ function renderConfigurationFiles(configs) {
773
788
  : '';
774
789
 
775
790
  return `
776
- <div class="section">
791
+ <section class="section">
777
792
  <div class="section-header">
778
793
  <div class="section-icon">${icon.file()}</div>
779
- <span class="section-title">Configuration Files</span>
794
+ <h2 class="section-title">Configuration Files</h2>
780
795
  ${countBadge}
781
796
  </div>
782
797
  <div class="section-body">${body}</div>
783
- </div>`;
798
+ </section>`;
784
799
  }
785
800
 
786
801
  /**
@@ -815,31 +830,32 @@ function renderPackageScripts(scripts) {
815
830
  : '';
816
831
 
817
832
  return `
818
- <div class="section">
833
+ <section class="section">
819
834
  <div class="section-header">
820
835
  <div class="section-icon">${icon.terminal()}</div>
821
- <span class="section-title">Package Scripts</span>
836
+ <h2 class="section-title">Package Scripts</h2>
822
837
  ${countBadge}
823
838
  </div>
824
839
  <div class="section-body" style="padding:0">${body}</div>
825
- </div>`;
840
+ </section>`;
826
841
  }
827
842
 
828
843
  /**
829
- * Render the folder structure section with a dark <pre><code> tree.
844
+ * Render the folder structure section.
830
845
  *
846
+ * @param {import('../scanner/scan.js').DirNode} tree
831
847
  * @param {string[]} flatFiles
832
- * @param {string} rootName
848
+ * @param {string} rootName
833
849
  * @returns {string}
834
850
  */
835
- function renderFolderStructure(flatFiles, rootName) {
836
- const treeStr = buildTreeString(flatFiles, rootName);
851
+ function renderFolderStructure(tree, flatFiles, rootName) {
852
+ const treeStr = buildTreeString(tree, rootName, flatFiles.length);
837
853
 
838
854
  return `
839
- <div class="section">
855
+ <section class="section">
840
856
  <div class="section-header">
841
857
  <div class="section-icon">${icon.tree()}</div>
842
- <span class="section-title">Folder Structure</span>
858
+ <h2 class="section-title">Folder Structure</h2>
843
859
  ${flatFiles.length > 0 ? `<span class="section-count">${flatFiles.length} files</span>` : ''}
844
860
  </div>
845
861
  <div class="section-body">
@@ -847,7 +863,7 @@ function renderFolderStructure(flatFiles, rootName) {
847
863
  <pre><code>${esc(treeStr)}</code></pre>
848
864
  </div>
849
865
  </div>
850
- </div>`;
866
+ </section>`;
851
867
  }
852
868
 
853
869
  /**
@@ -868,14 +884,14 @@ function renderStats(result) {
868
884
  const rowsHTML = rows.map(([metric, value, isNum]) => `
869
885
  <tr>
870
886
  <td>${esc(metric)}</td>
871
- <td class="${isNum ? 'val' : 'val-plain'}">${value}</td>
887
+ <td class="${isNum ? 'val' : 'val-plain'}">${esc(value)}</td>
872
888
  </tr>`).join('');
873
889
 
874
890
  return `
875
- <div class="section">
891
+ <section class="section">
876
892
  <div class="section-header">
877
893
  <div class="section-icon">${icon.bar()}</div>
878
- <span class="section-title">Statistics</span>
894
+ <h2 class="section-title">Statistics</h2>
879
895
  </div>
880
896
  <div class="section-body">
881
897
  <table class="data-table">
@@ -888,7 +904,7 @@ function renderStats(result) {
888
904
  <tbody>${rowsHTML}</tbody>
889
905
  </table>
890
906
  </div>
891
- </div>`;
907
+ </section>`;
892
908
  }
893
909
 
894
910
  /**
@@ -909,17 +925,17 @@ function renderScanInfo() {
909
925
  </tr>`).join('');
910
926
 
911
927
  return `
912
- <div class="section">
928
+ <section class="section">
913
929
  <div class="section-header">
914
930
  <div class="section-icon">${icon.info()}</div>
915
- <span class="section-title">Scan Information</span>
931
+ <h2 class="section-title">Scan Information</h2>
916
932
  </div>
917
933
  <div class="section-body">
918
934
  <table class="data-table">
919
935
  <tbody>${rowsHTML}</tbody>
920
936
  </table>
921
937
  </div>
922
- </div>`;
938
+ </section>`;
923
939
  }
924
940
 
925
941
  /**
@@ -950,7 +966,7 @@ function renderFooter() {
950
966
  * @param {{ cwd?: string }} [options]
951
967
  */
952
968
  export function render(result, options = {}) {
953
- const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles } = result;
969
+ const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles, tree } = result;
954
970
 
955
971
  const cwd = options.cwd ?? process.cwd();
956
972
  const relRoot = path.relative(cwd, rootPath) || '.';
@@ -974,11 +990,12 @@ export function render(result, options = {}) {
974
990
 
975
991
  <main>
976
992
  ${renderSummaryCards(result)}
993
+ ${renderFrameworks(projectType)}
977
994
  ${renderEntryPoints(entryPoints)}
978
995
  ${renderConfigurationFiles(configs)}
979
996
  ${renderPackageScripts(scripts)}
980
- ${renderFolderStructure(flatFiles, rootName)}
981
997
  ${renderStats(result)}
998
+ ${renderFolderStructure(tree, flatFiles, rootName)}
982
999
  ${renderScanInfo()}
983
1000
  </main>
984
1001