@lakindu_perera/toren 1.0.7 → 1.0.9

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +120 -31
  3. package/dist/cli/toren.d.ts +24 -0
  4. package/dist/cli/toren.js +254 -0
  5. package/dist/cli/toren.js.map +1 -0
  6. package/dist/detectors/config-detector.d.ts +3 -0
  7. package/dist/detectors/config-detector.js +58 -0
  8. package/dist/detectors/config-detector.js.map +1 -0
  9. package/dist/detectors/health-detector.d.ts +12 -0
  10. package/dist/detectors/health-detector.js +53 -0
  11. package/dist/detectors/health-detector.js.map +1 -0
  12. package/dist/detectors/important-files-detector.d.ts +51 -0
  13. package/dist/detectors/important-files-detector.js +455 -0
  14. package/dist/detectors/important-files-detector.js.map +1 -0
  15. package/dist/detectors/package-manager-detector.d.ts +7 -0
  16. package/dist/detectors/package-manager-detector.js +35 -0
  17. package/dist/detectors/package-manager-detector.js.map +1 -0
  18. package/dist/detectors/project-info-detector.d.ts +13 -0
  19. package/dist/detectors/project-info-detector.js +97 -0
  20. package/dist/detectors/project-info-detector.js.map +1 -0
  21. package/dist/detectors/script-detector.d.ts +42 -0
  22. package/dist/detectors/script-detector.js +199 -0
  23. package/dist/detectors/script-detector.js.map +1 -0
  24. package/dist/focused-output.d.ts +27 -0
  25. package/dist/focused-output.js +201 -0
  26. package/dist/focused-output.js.map +1 -0
  27. package/dist/lifecycle.d.ts +2 -0
  28. package/dist/lifecycle.js +114 -0
  29. package/dist/lifecycle.js.map +1 -0
  30. package/dist/renderers/console-renderer.d.ts +19 -0
  31. package/dist/renderers/console-renderer.js +361 -0
  32. package/dist/renderers/console-renderer.js.map +1 -0
  33. package/dist/renderers/html-renderer.d.ts +14 -0
  34. package/{src → dist}/renderers/html-renderer.js +263 -201
  35. package/dist/renderers/html-renderer.js.map +1 -0
  36. package/dist/renderers/index.d.ts +6 -0
  37. package/dist/renderers/index.js +12 -0
  38. package/dist/renderers/index.js.map +1 -0
  39. package/dist/renderers/json-renderer.d.ts +10 -0
  40. package/dist/renderers/json-renderer.js +127 -0
  41. package/dist/renderers/json-renderer.js.map +1 -0
  42. package/dist/renderers/markdown-renderer.d.ts +13 -0
  43. package/dist/renderers/markdown-renderer.js +360 -0
  44. package/dist/renderers/markdown-renderer.js.map +1 -0
  45. package/dist/scanner/scan.d.ts +2 -0
  46. package/dist/scanner/scan.js +573 -0
  47. package/dist/scanner/scan.js.map +1 -0
  48. package/dist/types/index.d.ts +85 -0
  49. package/dist/types/index.js +2 -0
  50. package/dist/types/index.js.map +1 -0
  51. package/package.json +16 -7
  52. package/bin/toren.js +0 -308
  53. package/src/detectors/config-detector.js +0 -81
  54. package/src/detectors/script-detector.js +0 -32
  55. package/src/focused-output.js +0 -146
  56. package/src/lifecycle.js +0 -124
  57. package/src/renderers/console-renderer.js +0 -350
  58. package/src/renderers/index.js +0 -41
  59. package/src/renderers/json-renderer.js +0 -154
  60. package/src/renderers/markdown-renderer.js +0 -337
  61. package/src/scanner/scan.js +0 -532
package/src/lifecycle.js DELETED
@@ -1,124 +0,0 @@
1
- import fs from 'node:fs';
2
- import { execSync } from 'node:child_process';
3
- import readline from 'node:readline';
4
-
5
- const C = {
6
- reset: '\x1b[0m',
7
- cyan: '\x1b[36m',
8
- green: '\x1b[32m',
9
- yellow: '\x1b[33m',
10
- red: '\x1b[31m',
11
- };
12
-
13
- function paint(text, ...codes) {
14
- return `${codes.join('')}${text}${C.reset}`;
15
- }
16
-
17
- function getGlobalNpmRoot() {
18
- try {
19
- return execSync('npm root -g', { encoding: 'utf8', stdio: 'pipe' }).trim();
20
- } catch {
21
- return null;
22
- }
23
- }
24
-
25
- function getInstallStatus() {
26
- const status = {
27
- isGlobal: false,
28
- method: 'unknown',
29
- binaryPath: process.argv[1] || 'unknown',
30
- isBroken: false,
31
- };
32
-
33
- try {
34
- if (!status.binaryPath || !fs.existsSync(status.binaryPath)) {
35
- status.isBroken = true;
36
- return status;
37
- }
38
-
39
- const realPath = fs.realpathSync(status.binaryPath);
40
- const globalRoot = getGlobalNpmRoot();
41
-
42
- if (globalRoot) {
43
- if (realPath.includes(globalRoot)) {
44
- status.isGlobal = true;
45
- status.method = 'npm install -g';
46
- } else if (status.binaryPath !== realPath) {
47
- status.isGlobal = true;
48
- status.method = 'npm link';
49
- } else {
50
- status.isGlobal = false;
51
- status.method = 'local';
52
- }
53
- } else {
54
- if (status.binaryPath !== realPath) {
55
- status.isGlobal = true;
56
- status.method = 'npm link';
57
- } else {
58
- status.isGlobal = false;
59
- }
60
- }
61
- } catch (err) {
62
- status.isBroken = true;
63
- }
64
-
65
- return status;
66
- }
67
-
68
- export function runDoctor(pkgVersion) {
69
- const status = getInstallStatus();
70
-
71
- if (status.isBroken) {
72
- console.log(paint('⚠ Broken installation detected', C.red));
73
- console.log(`→ Run: ${paint('npm uninstall -g toren', C.cyan)}`);
74
- console.log('');
75
- return;
76
- }
77
-
78
- if (status.isGlobal) {
79
- console.log(`${paint('✔', C.green)} Toren installed globally`);
80
- } else {
81
- console.log(`${paint('✔', C.green)} Toren installed locally`);
82
- }
83
-
84
- console.log(`${paint('✔', C.green)} Binary path valid`);
85
- console.log(`${paint('✔', C.green)} Version match confirmed`);
86
- console.log('');
87
- }
88
-
89
- export function runUninstall() {
90
- const status = getInstallStatus();
91
-
92
- if (status.isBroken) {
93
- console.log(paint('⚠ Toren installation is corrupted. Please reinstall using npm install -g toren', C.yellow));
94
- process.exit(0);
95
- }
96
-
97
- const rl = readline.createInterface({
98
- input: process.stdin,
99
- output: process.stdout
100
- });
101
-
102
- rl.question('Are you sure you want to remove Toren from global environment? (y/n) ', (answer) => {
103
- rl.close();
104
- if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
105
- process.exit(0);
106
- }
107
-
108
- console.log('');
109
- console.log('Uninstalling Toren...');
110
-
111
- try {
112
- execSync('npm uninstall -g toren', { stdio: 'inherit' });
113
- console.log('');
114
- console.log(paint('✔ Successfully removed Toren from the global environment.', C.green));
115
- } catch (err) {
116
- console.log('');
117
- console.log(paint('❌ Failed to uninstall Toren.', C.red));
118
- console.log('Please try manually: npm uninstall -g toren');
119
- }
120
-
121
- console.log('');
122
- process.exit(0);
123
- });
124
- }
@@ -1,350 +0,0 @@
1
- /**
2
- * @fileoverview Toren — Console Renderer
3
- *
4
- * Consumes a {@link ScanResult} and produces styled terminal output.
5
- *
6
- * Design contract:
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.
10
- * - No imports from the scanner or any domain module.
11
- * - Stateless: render() may be called multiple times safely.
12
- * - The shape expected here matches the ScanResult typedef in scan.js.
13
- * When ScanResult grows new fields, add new render sections; never mutate data.
14
- *
15
- * Adding a new output format (JSON, Markdown, HTML …):
16
- * - Create src/renderers/<format>-renderer.js
17
- * - Export a render(result, options?) function with the same signature
18
- * - Import and call it from bin/toren.js based on a --format flag
19
- */
20
-
21
- import path from 'node:path';
22
- import { createRequire } from 'node:module';
23
-
24
- const require = createRequire(import.meta.url);
25
- const pkg = require('../../package.json');
26
-
27
- // ---------------------------------------------------------------------------
28
- // ANSI palette — zero external dependencies
29
- // ---------------------------------------------------------------------------
30
-
31
- const C = {
32
- reset: '\x1b[0m',
33
- bold: '\x1b[1m',
34
- dim: '\x1b[2m',
35
- cyan: '\x1b[36m',
36
- green: '\x1b[32m',
37
- yellow: '\x1b[33m',
38
- blue: '\x1b[34m',
39
- magenta: '\x1b[35m',
40
- red: '\x1b[31m',
41
- white: '\x1b[97m',
42
- };
43
-
44
- /** Maximum files shown in the structure preview. */
45
- const PREVIEW_LIMIT = 20;
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
-
88
- // ---------------------------------------------------------------------------
89
- // Low-level paint / layout helpers (private to this module)
90
- // ---------------------------------------------------------------------------
91
-
92
- /**
93
- * Wrap `text` with one or more ANSI codes, resetting after.
94
- * @param {string} text
95
- * @param {...string} codes
96
- * @returns {string}
97
- */
98
- function paint(text, ...codes) {
99
- if (!useColors) return text;
100
- return `${codes.join('')}${text}${C.reset}`;
101
- }
102
-
103
- /**
104
- * Print a titled section header followed by a matched-length divider.
105
- * @param {string} title
106
- */
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));
111
- console.log('');
112
- }
113
-
114
- /**
115
- * Print a single labelled key-value row.
116
- * @param {string} label - Left-hand label (dim)
117
- * @param {string} value - Right-hand value
118
- * @param {string} [valueColor] - Optional ANSI code(s) for the value
119
- */
120
- function row(label, value, ...valueCodes) {
121
- const coloured = valueCodes.length ? paint(value, ...valueCodes) : value;
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];
148
- }
149
-
150
- // ---------------------------------------------------------------------------
151
- // File-tree renderer (private)
152
- // ---------------------------------------------------------------------------
153
-
154
- /**
155
- * Recursively print a file tree with classic tree connectors.
156
- * Stops after PREVIEW_LIMIT files have been printed.
157
- *
158
- * @param {import('../scanner/scan.js').DirNode | import('../scanner/scan.js').FileNode} node
159
- * @param {string} prefix - Accumulated indentation
160
- * @param {boolean} isLast - Whether this is the last sibling
161
- * @param {{ count: number, maxReached: boolean }} counter - Shared mutable file counter
162
- * @param {number} limit - Max files to render
163
- * @param {number} depth - Current depth
164
- * @param {number} maxDepth - Max recursion depth
165
- */
166
- function renderTree(node, prefix, isLast, counter, limit = PREVIEW_LIMIT, depth = 0, maxDepth = 4) {
167
- if (counter.maxReached) return;
168
- if (depth >= maxDepth) return;
169
-
170
- const connector = isLast ? CHARS.corner : CHARS.tee;
171
- const extension = isLast ? ' ' : CHARS.pipe;
172
-
173
- if (node.type === 'directory') {
174
- console.log(`${prefix}${connector}${paint(`${node.name}${path.sep}`, C.bold, C.blue)}`);
175
- const children = node.children ?? [];
176
-
177
- if (depth === maxDepth - 1 && children.length > 0) {
178
- console.log(`${prefix}${extension}${CHARS.corner}${paint('...', C.dim)}`);
179
- return;
180
- }
181
-
182
- for (let i = 0; i < children.length; i++) {
183
- if (counter.count >= limit) {
184
- console.log(`${prefix}${extension}${CHARS.corner}${paint('...', C.dim)}`);
185
- counter.maxReached = true;
186
- break;
187
- }
188
- // Check if this child will be the last one we render due to limits
189
- let willBeLast = i === children.length - 1;
190
-
191
- renderTree(children[i], prefix + extension, willBeLast, counter, limit, depth + 1, maxDepth);
192
- if (counter.maxReached) break;
193
- }
194
- } else {
195
- console.log(`${prefix}${connector}${paint(node.name, C.white)}`);
196
- counter.count += 1;
197
- }
198
- }
199
-
200
-
201
- // ---------------------------------------------------------------------------
202
- // Banner (private)
203
- // ---------------------------------------------------------------------------
204
-
205
- function printBanner() {
206
- const name = paint('Toren', C.bold, C.cyan);
207
- const version = paint(`v${pkg.version}`, C.dim);
208
- const tagline = paint('Codebase Onboarding Intelligence', C.dim);
209
- console.log(`${name} ${version} — ${tagline}`);
210
- }
211
-
212
- // ---------------------------------------------------------------------------
213
- // Public API
214
- // ---------------------------------------------------------------------------
215
-
216
- /**
217
- * Render a ScanResult to the terminal.
218
- *
219
- * All sections read from the ScanResult. Presentation-level derivations
220
- * (e.g. frameworks list) are computed here; no business logic is added.
221
- *
222
- * @param {import('../scanner/scan.js').ScanResult} result
223
- * @param {{ cwd?: string }} [options]
224
- */
225
- export function render(result, options = {}) {
226
- const {
227
- rootPath,
228
- projectType,
229
- entryPoints,
230
- configs = [],
231
- scripts = [],
232
- tree,
233
- flatFiles,
234
- totalFolders,
235
- scanDurationMs,
236
- } = result;
237
-
238
- const cwd = options.cwd ?? process.cwd();
239
- const relRoot = path.relative(cwd, rootPath) || '.';
240
-
241
- // ── Banner ────────────────────────────────────────────────────────────────
242
- printBanner();
243
- console.log('');
244
-
245
- // ── Summary ───────────────────────────────────────────────────────────────
246
- section('Project Summary');
247
- row('Path: ', paint(relRoot, C.cyan));
248
- row('Project type: ', paint(projectType, C.bold, C.green));
249
- row('Total files: ', paint(String(flatFiles.length), C.yellow));
250
- row('Total folders:', paint(String(totalFolders), C.yellow));
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('');
264
-
265
- // ── Entry Points ──────────────────────────────────────────────────────────
266
- section('Entry Points');
267
- if (entryPoints.length === 0) {
268
- console.log(paint('No entry points detected.', C.dim));
269
- } else {
270
- for (const ep of entryPoints) {
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));
283
- }
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('');
299
-
300
- // ── Structure Preview ─────────────────────────────────────────────────────
301
- section('Folder Structure');
302
-
303
- console.log(paint(`${tree.name || '.'}${path.sep}`, C.bold, C.blue));
304
-
305
- const counter = { count: 0, maxReached: false };
306
- const children = tree.children ?? [];
307
- for (let i = 0; i < children.length; i++) {
308
- if (counter.count >= PREVIEW_LIMIT) {
309
- console.log(`${CHARS.corner}${paint('...', C.dim)}`);
310
- break;
311
- }
312
- renderTree(children[i], '', i === children.length - 1, counter);
313
- if (counter.maxReached) break;
314
- }
315
-
316
- if (flatFiles.length > PREVIEW_LIMIT) {
317
- const hidden = flatFiles.length - PREVIEW_LIMIT;
318
- console.log(paint(`… ${hidden} more file(s) not shown`, C.dim));
319
- }
320
-
321
- // ── Footer ────────────────────────────────────────────────────────────────
322
- console.log('');
323
- console.log(paint(`Scan completed in ${formatDuration(scanDurationMs)}`, C.green));
324
- console.log('');
325
- }
326
-
327
- /**
328
- * Render only the project structure for the --structure flag.
329
- *
330
- * @param {import('../scanner/scan.js').ScanResult} result
331
- */
332
- export function renderStructure(result) {
333
- const { tree, flatFiles } = result;
334
-
335
- section('Folder Structure');
336
- console.log(paint(`${tree.name || '.'}${path.sep}`, C.bold, C.blue));
337
-
338
- const counter = { count: 0, maxReached: false };
339
- const children = tree.children ?? [];
340
- const limit = flatFiles.length; // No preview limit for focused --structure
341
-
342
- for (let i = 0; i < children.length; i++) {
343
- if (counter.count >= limit) {
344
- console.log(`${CHARS.corner}${paint('...', C.dim)}`);
345
- break;
346
- }
347
- renderTree(children[i], '', i === children.length - 1, counter, limit, 0, Infinity);
348
- if (counter.maxReached) break;
349
- }
350
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * @fileoverview Toren — Renderer Registry
3
- *
4
- * Maps format-name strings to their render functions.
5
- *
6
- * ┌─────────────────────────────────────────────────────────┐
7
- * │ How to add a new output format: │
8
- * │ │
9
- * │ 1. Create src/renderers/<format>-renderer.js │
10
- * │ and export: render(result, options?) => void │
11
- * │ │
12
- * │ 2. Import it here and add one line to RENDERERS. │
13
- * │ │
14
- * │ No changes to bin/toren.js or scan.js are needed. │
15
- * └─────────────────────────────────────────────────────────┘
16
- *
17
- * @module renderers/index
18
- */
19
-
20
- import { render as renderConsole } from './console-renderer.js';
21
- import { render as renderJson } from './json-renderer.js';
22
- import { render as renderMarkdown } from './markdown-renderer.js';
23
- import { render as renderHtml } from './html-renderer.js';
24
-
25
- /**
26
- * Registry of all available output renderers.
27
- *
28
- * Keys — format names accepted by the `--format` flag.
29
- * Values — render functions with the signature:
30
- * render(result: ScanResult, options?: { cwd?: string }) => void
31
- *
32
- * @type {Record<string, function(import('../scanner/scan.js').ScanResult, object=): void>}
33
- */
34
- const renderers = {
35
- console: renderConsole,
36
- json: renderJson,
37
- markdown: renderMarkdown,
38
- html: renderHtml,
39
- };
40
-
41
- export default renderers;
@@ -1,154 +0,0 @@
1
- /**
2
- * @fileoverview Toren — JSON Renderer
3
- *
4
- * Consumes a ScanResult and produces a stable, consistently-ordered JSON
5
- * object suitable for programmatic consumption.
6
- *
7
- * Design contract (mirrors all other renderers):
8
- * - Accepts a ScanResult and an optional options object.
9
- * - Never scans files or modifies the data it receives.
10
- * - All arrays are always present — never undefined or missing.
11
- * - Property order is fixed and documented below.
12
- * - Backward compatible: no existing field is removed or renamed.
13
- *
14
- * Output property order:
15
- * 1. meta — schema version + generator provenance
16
- * 2. project — name (basename), path (relative), detected type
17
- * 3. frameworks — derived array of detected frameworks ([] when none)
18
- * 4. entryPoints — array of detected entry-point paths
19
- * 5. configs — array of detected configuration file paths
20
- * 6. scripts — array of { name, command } objects
21
- * 7. statistics — file/folder counts and scan duration (structured)
22
- * 8. structure — recursive file-tree array
23
- * 9. summary — retained for backward compatibility (same data as statistics)
24
- */
25
-
26
- import path from 'node:path';
27
- import { createRequire } from 'node:module';
28
-
29
- const require = createRequire(import.meta.url);
30
- const pkg = require('../../package.json');
31
-
32
- // ---------------------------------------------------------------------------
33
- // Tree mapper
34
- // ---------------------------------------------------------------------------
35
-
36
- /**
37
- * Recursively map an internal DirNode / FileNode to a clean JSON shape.
38
- * Directory nodes always include a `children` array (never undefined).
39
- *
40
- * @param {import('../scanner/scan.js').DirNode | import('../scanner/scan.js').FileNode} node
41
- * @returns {{ type: 'folder'|'file', name: string, children?: object[] }}
42
- */
43
- function mapTree(node) {
44
- if (node.type === 'directory') {
45
- return {
46
- type: 'folder',
47
- name: node.name,
48
- children: (node.children || []).map(mapTree),
49
- };
50
- }
51
- return {
52
- type: 'file',
53
- name: node.name,
54
- };
55
- }
56
-
57
- // ---------------------------------------------------------------------------
58
- // Helpers
59
- // ---------------------------------------------------------------------------
60
-
61
- /**
62
- * Derive a `frameworks` array from the scanner's `projectType` string.
63
- * Returns a single-element array when a framework is detected, empty otherwise.
64
- * This is a pure presentation decision — no business logic is added.
65
- *
66
- * @param {string} projectType
67
- * @returns {string[]}
68
- */
69
- function deriveFrameworks(projectType) {
70
- if (!projectType || projectType === 'Unknown') return [];
71
- return [projectType];
72
- }
73
-
74
- // ---------------------------------------------------------------------------
75
- // Public API
76
- // ---------------------------------------------------------------------------
77
-
78
- /**
79
- * Render a ScanResult as a stable JSON document to stdout.
80
- *
81
- * @param {import('../scanner/scan.js').ScanResult} result
82
- * @param {{ cwd?: string }} [options]
83
- */
84
- export function render(result, options = {}) {
85
- const {
86
- rootPath,
87
- projectType,
88
- entryPoints = [],
89
- configs = [],
90
- scripts = [],
91
- tree,
92
- flatFiles = [],
93
- totalFolders = 0,
94
- scanDurationMs,
95
- } = result;
96
-
97
- const cwd = options.cwd ?? process.cwd();
98
- const relRoot = path.relative(cwd, rootPath) || '.';
99
- const rootName = path.basename(rootPath) || relRoot;
100
-
101
- // Shared statistics values — computed once, used in both `statistics` and
102
- // the backward-compatible `summary` block.
103
- // Guard against NaN/undefined: JSON.stringify(NaN) produces null, breaking
104
- // the schema guarantee that durationMs is always a number.
105
- const totalFiles = flatFiles.length;
106
- const durationMs = Number.isFinite(scanDurationMs) ? Math.round(scanDurationMs) : 0;
107
-
108
- const output = {
109
- // 1. Provenance — lets consumers detect schema changes.
110
- meta: {
111
- generatedBy: 'Toren',
112
- version: pkg.version,
113
- schema: 1,
114
- },
115
-
116
- // 2. Project identity — name is the human-readable basename; path is the
117
- // relative path used for filesystem resolution.
118
- project: {
119
- name: rootName,
120
- path: relRoot,
121
- type: projectType,
122
- },
123
-
124
- // 3. Detected frameworks — always an array.
125
- frameworks: deriveFrameworks(projectType),
126
-
127
- // 4–6. Discovery results — all arrays, always present, never null.
128
- entryPoints: Array.isArray(entryPoints) ? entryPoints : [],
129
- configs: Array.isArray(configs) ? configs : [],
130
- scripts: Array.isArray(scripts) ? scripts : [],
131
-
132
- // 7. Structured statistics — always a complete object with numeric values.
133
- // durationMs is rounded to the nearest millisecond (integer).
134
- statistics: {
135
- files: totalFiles,
136
- folders: totalFolders,
137
- durationMs,
138
- },
139
-
140
- // 8. File-tree — array of root-level nodes; always present.
141
- // Empty array when the scanned directory is empty.
142
- structure: (tree?.children || []).map(mapTree),
143
-
144
- // 9. Backward-compatible summary block — preserved for existing consumers.
145
- // Contains the same data under the original field names.
146
- summary: {
147
- totalFiles,
148
- totalFolders,
149
- scanDurationMs: durationMs,
150
- },
151
- };
152
-
153
- console.log(JSON.stringify(output, null, 2));
154
- }