@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.
- package/CHANGELOG.md +19 -0
- package/README.md +172 -63
- package/bin/toren.js +66 -44
- package/package.json +7 -3
- package/src/detectors/health-detector.js +72 -0
- package/src/detectors/important-files-detector.js +515 -0
- package/src/detectors/package-manager-detector.js +112 -0
- package/src/detectors/project-info-detector.js +89 -0
- package/src/detectors/script-detector.js +226 -10
- package/src/focused-output.js +161 -41
- package/src/renderers/console-renderer.js +170 -53
- package/src/renderers/html-renderer.js +212 -73
- package/src/renderers/json-renderer.js +133 -23
- package/src/renderers/markdown-renderer.js +127 -104
- package/src/scanner/scan.js +51 -10
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Consumes a {@link ScanResult} and produces styled terminal output.
|
|
5
5
|
*
|
|
6
6
|
* Design contract:
|
|
7
|
-
* -
|
|
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
|
|
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(
|
|
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(
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
166
|
-
*
|
|
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('
|
|
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
|
-
|
|
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('
|
|
309
|
+
section('Entry Points');
|
|
201
310
|
if (entryPoints.length === 0) {
|
|
202
|
-
console.log(paint('
|
|
311
|
+
console.log(paint('No entry points detected.', C.dim));
|
|
203
312
|
} else {
|
|
204
313
|
for (const ep of entryPoints) {
|
|
205
|
-
console.log(
|
|
314
|
+
console.log(paint(ep, C.white));
|
|
206
315
|
}
|
|
207
316
|
}
|
|
317
|
+
console.log('');
|
|
208
318
|
|
|
209
319
|
// ── Configuration Files ───────────────────────────────────────────────────
|
|
210
|
-
section('
|
|
320
|
+
section('Configuration Files');
|
|
211
321
|
if (configs.length === 0) {
|
|
212
|
-
console.log(paint('
|
|
322
|
+
console.log(paint('No configuration files detected.', C.dim));
|
|
213
323
|
} else {
|
|
214
324
|
for (const c of configs) {
|
|
215
|
-
console.log(
|
|
325
|
+
console.log(paint(c, C.white));
|
|
216
326
|
}
|
|
217
327
|
}
|
|
328
|
+
console.log('');
|
|
218
329
|
|
|
219
330
|
// ── Package Scripts ───────────────────────────────────────────────────────
|
|
220
|
-
section('
|
|
331
|
+
section('Package Scripts');
|
|
221
332
|
if (scripts.length === 0) {
|
|
222
|
-
console.log(paint('
|
|
333
|
+
console.log(paint('No package scripts detected.', C.dim));
|
|
223
334
|
} else {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
console.log(
|
|
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('
|
|
350
|
+
section('Folder Structure');
|
|
233
351
|
|
|
234
|
-
console.log(paint(`${tree.name || '.'}
|
|
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(
|
|
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(
|
|
367
|
+
console.log(paint(`… ${hidden} more file(s) not shown`, C.dim));
|
|
250
368
|
}
|
|
251
369
|
|
|
252
370
|
// ── Footer ────────────────────────────────────────────────────────────────
|
|
253
371
|
console.log('');
|
|
254
|
-
|
|
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
|
-
|
|
268
|
-
console.log(paint(`${tree.name || '.'}
|
|
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(
|
|
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);
|