@lakindu_perera/toren 1.0.5 → 1.0.8

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]
@@ -175,6 +229,10 @@ export function render(result, options = {}) {
175
229
  entryPoints,
176
230
  configs = [],
177
231
  scripts = [],
232
+ packageManager,
233
+ importantFiles = [],
234
+ projectInfo = null,
235
+ health = [],
178
236
  tree,
179
237
  flatFiles,
180
238
  totalFolders,
@@ -189,55 +247,115 @@ export function render(result, options = {}) {
189
247
  console.log('');
190
248
 
191
249
  // ── Summary ───────────────────────────────────────────────────────────────
192
- section('🔍', 'Project Summary');
250
+ section('Project Summary');
193
251
  row('Path: ', paint(relRoot, C.cyan));
252
+ if (projectInfo && projectInfo.name) {
253
+ row('Name: ', paint(projectInfo.name, C.white));
254
+ }
194
255
  row('Project type: ', paint(projectType, C.bold, C.green));
256
+ if (packageManager) {
257
+ row('Pkg manager: ', paint(packageManager, C.white));
258
+ }
195
259
  row('Total files: ', paint(String(flatFiles.length), C.yellow));
196
260
  row('Total folders:', paint(String(totalFolders), C.yellow));
197
- row('Scan duration:', paint(formatDuration(scanDurationMs), C.magenta));
261
+ console.log('');
262
+
263
+ // ── Project Health ────────────────────────────────────────────────────────
264
+ section('Project Health');
265
+ if (health.length === 0) {
266
+ console.log(paint('No project health observations available.', C.dim));
267
+ } else {
268
+ for (const h of health) {
269
+ let icon = 'ℹ';
270
+ let color = C.white;
271
+ if (h.status === 'pass') {
272
+ icon = '✓';
273
+ color = C.green;
274
+ } else if (h.status === 'warning') {
275
+ icon = '⚠';
276
+ color = C.yellow;
277
+ }
278
+ console.log(`${paint(icon, color)} ${h.message}`);
279
+ }
280
+ }
281
+ console.log('');
282
+
283
+ // ── Important Files ───────────────────────────────────────────────────────
284
+ section('Important Files');
285
+ if (importantFiles.length === 0) {
286
+ console.log(paint('No important files detected.', C.dim));
287
+ } else {
288
+ importantFiles.forEach((f, idx) => {
289
+ console.log(`${idx + 1}. ${paint(f.path, C.white)}`);
290
+ console.log(` ${paint(f.reason, C.dim)}`);
291
+ if (idx < importantFiles.length - 1) console.log('');
292
+ });
293
+ }
294
+ console.log('');
295
+
296
+ // ── Frameworks ────────────────────────────────────────────────────────────
297
+ section('Frameworks');
298
+ const frameworks = deriveFrameworks(projectType);
299
+ if (frameworks.length === 0) {
300
+ console.log(paint('No frameworks detected.', C.dim));
301
+ } else {
302
+ for (const fw of frameworks) {
303
+ console.log(paint(fw, C.white));
304
+ }
305
+ }
306
+ console.log('');
198
307
 
199
308
  // ── Entry Points ──────────────────────────────────────────────────────────
200
- section('🚪', 'Entry Points');
309
+ section('Entry Points');
201
310
  if (entryPoints.length === 0) {
202
- console.log(paint(' ⚠ No entry points detected (this may be a library or utility project)', C.yellow));
311
+ console.log(paint('No entry points detected.', C.dim));
203
312
  } else {
204
313
  for (const ep of entryPoints) {
205
- console.log(` ${paint('→', C.cyan)} ${paint(ep, C.white)}`);
314
+ console.log(paint(ep, C.white));
206
315
  }
207
316
  }
317
+ console.log('');
208
318
 
209
319
  // ── Configuration Files ───────────────────────────────────────────────────
210
- section('⚙️', 'Configuration Files');
320
+ section('Configuration Files');
211
321
  if (configs.length === 0) {
212
- console.log(paint(' No configuration files detected', C.dim));
322
+ console.log(paint('No configuration files detected.', C.dim));
213
323
  } else {
214
324
  for (const c of configs) {
215
- console.log(` ${paint(c, C.white)}`);
325
+ console.log(paint(c, C.white));
216
326
  }
217
327
  }
328
+ console.log('');
218
329
 
219
330
  // ── Package Scripts ───────────────────────────────────────────────────────
220
- section('📜', 'Package Scripts');
331
+ section('Package Scripts');
221
332
  if (scripts.length === 0) {
222
- console.log(paint(' No package scripts detected', C.dim));
333
+ console.log(paint('No package scripts detected.', C.dim));
223
334
  } else {
224
- const maxNameLen = Math.max(...scripts.map(s => s.name.length));
225
- for (const s of scripts) {
226
- const paddedName = s.name.padEnd(maxNameLen, ' ');
227
- console.log(` ${paint(paddedName, C.white)} ${paint(s.command, C.dim)}`);
335
+ for (let i = 0; i < scripts.length; i++) {
336
+ const s = scripts[i];
337
+ const usage = s.usage || `npm run ${s.name}`;
338
+ console.log(paint(usage, C.white));
339
+ if (s.description) {
340
+ console.log(` ${paint(s.description, C.dim)}`);
341
+ } else {
342
+ console.log(` ${paint(s.command, C.dim)}`);
343
+ }
344
+ if (i < scripts.length - 1) console.log('');
228
345
  }
229
346
  }
347
+ console.log('');
230
348
 
231
349
  // ── Structure Preview ─────────────────────────────────────────────────────
232
- section('📁', `Folder Structure ${paint(`(first ${PREVIEW_LIMIT} files)`, C.dim)}`);
350
+ section('Folder Structure');
233
351
 
234
- console.log(paint(`${tree.name || '.'}/`, C.bold, C.blue));
352
+ console.log(paint(`${tree.name || '.'}${path.sep}`, C.bold, C.blue));
235
353
 
236
354
  const counter = { count: 0, maxReached: false };
237
355
  const children = tree.children ?? [];
238
356
  for (let i = 0; i < children.length; i++) {
239
357
  if (counter.count >= PREVIEW_LIMIT) {
240
- console.log(`└── ${paint('...', C.dim)}`);
358
+ console.log(`${CHARS.corner}${paint('...', C.dim)}`);
241
359
  break;
242
360
  }
243
361
  renderTree(children[i], '', i === children.length - 1, counter);
@@ -246,13 +364,12 @@ export function render(result, options = {}) {
246
364
 
247
365
  if (flatFiles.length > PREVIEW_LIMIT) {
248
366
  const hidden = flatFiles.length - PREVIEW_LIMIT;
249
- console.log(paint(` … and ${hidden} more file(s) not shown`, C.dim));
367
+ console.log(paint(`… ${hidden} more file(s) not shown`, C.dim));
250
368
  }
251
369
 
252
370
  // ── Footer ────────────────────────────────────────────────────────────────
253
371
  console.log('');
254
- divider();
255
- console.log(paint(' ✅ Scan complete.', C.green));
372
+ console.log(paint(`Scan completed in ${formatDuration(scanDurationMs)}`, C.green));
256
373
  console.log('');
257
374
  }
258
375
 
@@ -264,8 +381,8 @@ export function render(result, options = {}) {
264
381
  export function renderStructure(result) {
265
382
  const { tree, flatFiles } = result;
266
383
 
267
- console.log(paint('Project Structure:', C.bold, C.white));
268
- console.log(paint(`${tree.name || '.'}/`, C.bold, C.blue));
384
+ section('Folder Structure');
385
+ console.log(paint(`${tree.name || '.'}${path.sep}`, C.bold, C.blue));
269
386
 
270
387
  const counter = { count: 0, maxReached: false };
271
388
  const children = tree.children ?? [];
@@ -273,7 +390,7 @@ export function renderStructure(result) {
273
390
 
274
391
  for (let i = 0; i < children.length; i++) {
275
392
  if (counter.count >= limit) {
276
- console.log(`└── ${paint('...', C.dim)}`);
393
+ console.log(`${CHARS.corner}${paint('...', C.dim)}`);
277
394
  break;
278
395
  }
279
396
  renderTree(children[i], '', i === children.length - 1, counter, limit, 0, Infinity);