@lakindu_perera/toren 1.0.4 → 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]
@@ -173,6 +227,8 @@ export function render(result, options = {}) {
173
227
  rootPath,
174
228
  projectType,
175
229
  entryPoints,
230
+ configs = [],
231
+ scripts = [],
176
232
  tree,
177
233
  flatFiles,
178
234
  totalFolders,
@@ -187,33 +243,70 @@ export function render(result, options = {}) {
187
243
  console.log('');
188
244
 
189
245
  // ── Summary ───────────────────────────────────────────────────────────────
190
- section('🔍', 'Project Summary');
246
+ section('Project Summary');
191
247
  row('Path: ', paint(relRoot, C.cyan));
192
248
  row('Project type: ', paint(projectType, C.bold, C.green));
193
249
  row('Total files: ', paint(String(flatFiles.length), C.yellow));
194
250
  row('Total folders:', paint(String(totalFolders), C.yellow));
195
- 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('');
196
264
 
197
265
  // ── Entry Points ──────────────────────────────────────────────────────────
198
- section('🚪', 'Entry Points');
266
+ section('Entry Points');
199
267
  if (entryPoints.length === 0) {
200
- 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));
201
269
  } else {
202
270
  for (const ep of entryPoints) {
203
- console.log(` ${paint('→', C.cyan)} ${paint(ep, C.white)}`);
271
+ console.log(paint(ep, C.white));
272
+ }
273
+ }
274
+ console.log('');
275
+
276
+ // ── Configuration Files ───────────────────────────────────────────────────
277
+ section('Configuration Files');
278
+ if (configs.length === 0) {
279
+ console.log(paint('No configuration files detected.', C.dim));
280
+ } else {
281
+ for (const c of configs) {
282
+ console.log(paint(c, C.white));
204
283
  }
205
284
  }
285
+ console.log('');
286
+
287
+ // ── Package Scripts ───────────────────────────────────────────────────────
288
+ section('Package Scripts');
289
+ if (scripts.length === 0) {
290
+ console.log(paint('No package scripts detected.', C.dim));
291
+ } else {
292
+ const maxNameLen = Math.max(...scripts.map(s => s.name.length));
293
+ for (const s of scripts) {
294
+ const paddedName = s.name.padEnd(maxNameLen, ' ');
295
+ console.log(`${paint(paddedName, C.white)} ${paint(s.command, C.dim)}`);
296
+ }
297
+ }
298
+ console.log('');
206
299
 
207
300
  // ── Structure Preview ─────────────────────────────────────────────────────
208
- section('📁', `Folder Structure ${paint(`(first ${PREVIEW_LIMIT} files)`, C.dim)}`);
301
+ section('Folder Structure');
209
302
 
210
- console.log(paint(`${tree.name || '.'}/`, C.bold, C.blue));
303
+ console.log(paint(`${tree.name || '.'}${path.sep}`, C.bold, C.blue));
211
304
 
212
305
  const counter = { count: 0, maxReached: false };
213
306
  const children = tree.children ?? [];
214
307
  for (let i = 0; i < children.length; i++) {
215
308
  if (counter.count >= PREVIEW_LIMIT) {
216
- console.log(`└── ${paint('...', C.dim)}`);
309
+ console.log(`${CHARS.corner}${paint('...', C.dim)}`);
217
310
  break;
218
311
  }
219
312
  renderTree(children[i], '', i === children.length - 1, counter);
@@ -222,13 +315,12 @@ export function render(result, options = {}) {
222
315
 
223
316
  if (flatFiles.length > PREVIEW_LIMIT) {
224
317
  const hidden = flatFiles.length - PREVIEW_LIMIT;
225
- console.log(paint(` … and ${hidden} more file(s) not shown`, C.dim));
318
+ console.log(paint(`… ${hidden} more file(s) not shown`, C.dim));
226
319
  }
227
320
 
228
321
  // ── Footer ────────────────────────────────────────────────────────────────
229
322
  console.log('');
230
- divider();
231
- console.log(paint(' ✅ Scan complete.', C.green));
323
+ console.log(paint(`Scan completed in ${formatDuration(scanDurationMs)}`, C.green));
232
324
  console.log('');
233
325
  }
234
326
 
@@ -240,8 +332,8 @@ export function render(result, options = {}) {
240
332
  export function renderStructure(result) {
241
333
  const { tree, flatFiles } = result;
242
334
 
243
- console.log(paint('Project Structure:', C.bold, C.white));
244
- 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));
245
337
 
246
338
  const counter = { count: 0, maxReached: false };
247
339
  const children = tree.children ?? [];
@@ -249,7 +341,7 @@ export function renderStructure(result) {
249
341
 
250
342
  for (let i = 0; i < children.length; i++) {
251
343
  if (counter.count >= limit) {
252
- console.log(`└── ${paint('...', C.dim)}`);
344
+ console.log(`${CHARS.corner}${paint('...', C.dim)}`);
253
345
  break;
254
346
  }
255
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
  }
@@ -625,12 +592,26 @@ const icon = {
625
592
  tree: () => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
626
593
  bar: () => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>`,
627
594
  info: () => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
595
+ terminal: () => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`,
628
596
  };
629
597
 
630
598
  // ---------------------------------------------------------------------------
631
599
  // Section builders (one function per report section)
632
600
  // ---------------------------------------------------------------------------
633
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
+
634
615
  /**
635
616
  * Render the gradient page header.
636
617
  *
@@ -695,7 +676,7 @@ function renderSummaryCards(result) {
695
676
  {
696
677
  label: 'Scan Duration',
697
678
  icon: icon.clock(),
698
- value: formatDuration(scanDurationMs),
679
+ value: esc(formatDuration(scanDurationMs)),
699
680
  isText: true,
700
681
  sub: 'wall-clock time',
701
682
  },
@@ -714,6 +695,41 @@ function renderSummaryCards(result) {
714
695
  return `<div class="cards">${cardHTML}</div>`;
715
696
  }
716
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
+
717
733
  /**
718
734
  * Render the entry points section.
719
735
  *
@@ -738,31 +754,108 @@ function renderEntryPoints(entryPoints) {
738
754
  : '';
739
755
 
740
756
  return `
741
- <div class="section">
757
+ <section class="section">
742
758
  <div class="section-header">
743
759
  <div class="section-icon">${icon.door()}</div>
744
- <span class="section-title">Entry Points</span>
760
+ <h2 class="section-title">Entry Points</h2>
745
761
  ${countBadge}
746
762
  </div>
747
763
  <div class="section-body">${body}</div>
748
- </div>`;
764
+ </section>`;
749
765
  }
750
766
 
751
767
  /**
752
- * Render the folder structure section with a dark <pre><code> tree.
768
+ * Render the configuration files section.
753
769
  *
770
+ * @param {string[]} configs
771
+ * @returns {string}
772
+ */
773
+ function renderConfigurationFiles(configs) {
774
+ const count = configs.length;
775
+
776
+ const body = count === 0
777
+ ? `<p class="empty-msg">No configuration files detected.</p>`
778
+ : `<ul class="entry-list">
779
+ ${configs.map(c => `
780
+ <li class="entry-item">
781
+ <span class="entry-dot"></span>
782
+ ${esc(c)}
783
+ </li>`).join('')}
784
+ </ul>`;
785
+
786
+ const countBadge = count > 0
787
+ ? `<span class="section-count">${count} found</span>`
788
+ : '';
789
+
790
+ return `
791
+ <section class="section">
792
+ <div class="section-header">
793
+ <div class="section-icon">${icon.file()}</div>
794
+ <h2 class="section-title">Configuration Files</h2>
795
+ ${countBadge}
796
+ </div>
797
+ <div class="section-body">${body}</div>
798
+ </section>`;
799
+ }
800
+
801
+ /**
802
+ * Render the package scripts section.
803
+ *
804
+ * @param {Array<{name: string, command: string}>} scripts
805
+ * @returns {string}
806
+ */
807
+ function renderPackageScripts(scripts) {
808
+ const count = scripts.length;
809
+
810
+ const body = count === 0
811
+ ? `<p class="empty-msg" style="padding: 1.5rem">No package scripts detected.</p>`
812
+ : `<table class="data-table">
813
+ <thead>
814
+ <tr>
815
+ <th>Script</th>
816
+ <th>Command</th>
817
+ </tr>
818
+ </thead>
819
+ <tbody>
820
+ ${scripts.map(s => `
821
+ <tr>
822
+ <td><code>${esc(s.name)}</code></td>
823
+ <td class="val-plain"><code>${esc(s.command)}</code></td>
824
+ </tr>`).join('')}
825
+ </tbody>
826
+ </table>`;
827
+
828
+ const countBadge = count > 0
829
+ ? `<span class="section-count">${count} found</span>`
830
+ : '';
831
+
832
+ return `
833
+ <section class="section">
834
+ <div class="section-header">
835
+ <div class="section-icon">${icon.terminal()}</div>
836
+ <h2 class="section-title">Package Scripts</h2>
837
+ ${countBadge}
838
+ </div>
839
+ <div class="section-body" style="padding:0">${body}</div>
840
+ </section>`;
841
+ }
842
+
843
+ /**
844
+ * Render the folder structure section.
845
+ *
846
+ * @param {import('../scanner/scan.js').DirNode} tree
754
847
  * @param {string[]} flatFiles
755
- * @param {string} rootName
848
+ * @param {string} rootName
756
849
  * @returns {string}
757
850
  */
758
- function renderFolderStructure(flatFiles, rootName) {
759
- const treeStr = buildTreeString(flatFiles, rootName);
851
+ function renderFolderStructure(tree, flatFiles, rootName) {
852
+ const treeStr = buildTreeString(tree, rootName, flatFiles.length);
760
853
 
761
854
  return `
762
- <div class="section">
855
+ <section class="section">
763
856
  <div class="section-header">
764
857
  <div class="section-icon">${icon.tree()}</div>
765
- <span class="section-title">Folder Structure</span>
858
+ <h2 class="section-title">Folder Structure</h2>
766
859
  ${flatFiles.length > 0 ? `<span class="section-count">${flatFiles.length} files</span>` : ''}
767
860
  </div>
768
861
  <div class="section-body">
@@ -770,7 +863,7 @@ function renderFolderStructure(flatFiles, rootName) {
770
863
  <pre><code>${esc(treeStr)}</code></pre>
771
864
  </div>
772
865
  </div>
773
- </div>`;
866
+ </section>`;
774
867
  }
775
868
 
776
869
  /**
@@ -791,14 +884,14 @@ function renderStats(result) {
791
884
  const rowsHTML = rows.map(([metric, value, isNum]) => `
792
885
  <tr>
793
886
  <td>${esc(metric)}</td>
794
- <td class="${isNum ? 'val' : 'val-plain'}">${value}</td>
887
+ <td class="${isNum ? 'val' : 'val-plain'}">${esc(value)}</td>
795
888
  </tr>`).join('');
796
889
 
797
890
  return `
798
- <div class="section">
891
+ <section class="section">
799
892
  <div class="section-header">
800
893
  <div class="section-icon">${icon.bar()}</div>
801
- <span class="section-title">Statistics</span>
894
+ <h2 class="section-title">Statistics</h2>
802
895
  </div>
803
896
  <div class="section-body">
804
897
  <table class="data-table">
@@ -811,7 +904,7 @@ function renderStats(result) {
811
904
  <tbody>${rowsHTML}</tbody>
812
905
  </table>
813
906
  </div>
814
- </div>`;
907
+ </section>`;
815
908
  }
816
909
 
817
910
  /**
@@ -832,17 +925,17 @@ function renderScanInfo() {
832
925
  </tr>`).join('');
833
926
 
834
927
  return `
835
- <div class="section">
928
+ <section class="section">
836
929
  <div class="section-header">
837
930
  <div class="section-icon">${icon.info()}</div>
838
- <span class="section-title">Scan Information</span>
931
+ <h2 class="section-title">Scan Information</h2>
839
932
  </div>
840
933
  <div class="section-body">
841
934
  <table class="data-table">
842
935
  <tbody>${rowsHTML}</tbody>
843
936
  </table>
844
937
  </div>
845
- </div>`;
938
+ </section>`;
846
939
  }
847
940
 
848
941
  /**
@@ -873,7 +966,7 @@ function renderFooter() {
873
966
  * @param {{ cwd?: string }} [options]
874
967
  */
875
968
  export function render(result, options = {}) {
876
- const { rootPath, projectType, entryPoints, flatFiles } = result;
969
+ const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles, tree } = result;
877
970
 
878
971
  const cwd = options.cwd ?? process.cwd();
879
972
  const relRoot = path.relative(cwd, rootPath) || '.';
@@ -897,9 +990,12 @@ export function render(result, options = {}) {
897
990
 
898
991
  <main>
899
992
  ${renderSummaryCards(result)}
993
+ ${renderFrameworks(projectType)}
900
994
  ${renderEntryPoints(entryPoints)}
901
- ${renderFolderStructure(flatFiles, rootName)}
995
+ ${renderConfigurationFiles(configs)}
996
+ ${renderPackageScripts(scripts)}
902
997
  ${renderStats(result)}
998
+ ${renderFolderStructure(tree, flatFiles, rootName)}
903
999
  ${renderScanInfo()}
904
1000
  </main>
905
1001