@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.
@@ -1,53 +1,153 @@
1
1
  /**
2
2
  * @fileoverview Toren — JSON Renderer
3
3
  *
4
- * Consumes a ScanResult and produces a clean JSON object for programmatic use.
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)
5
24
  */
6
25
 
7
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
+ // ---------------------------------------------------------------------------
8
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
+ */
9
43
  function mapTree(node) {
10
44
  if (node.type === 'directory') {
11
45
  return {
12
- type: 'folder',
13
- name: node.name,
14
- children: (node.children || []).map(mapTree)
15
- };
16
- } else {
17
- return {
18
- type: 'file',
19
- name: node.name
46
+ type: 'folder',
47
+ name: node.name,
48
+ children: (node.children || []).map(mapTree),
20
49
  };
21
50
  }
51
+ return {
52
+ type: 'file',
53
+ name: node.name,
54
+ };
22
55
  }
23
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
+ */
24
84
  export function render(result, options = {}) {
25
85
  const {
26
86
  rootPath,
27
87
  projectType,
28
- entryPoints,
88
+ entryPoints = [],
89
+ configs = [],
90
+ scripts = [],
29
91
  tree,
30
- flatFiles,
31
- totalFolders,
92
+ flatFiles = [],
93
+ totalFolders = 0,
32
94
  scanDurationMs,
33
95
  } = result;
34
96
 
35
- const cwd = options.cwd ?? process.cwd();
36
- const relRoot = path.relative(cwd, rootPath) || '.';
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;
37
107
 
38
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.
39
118
  project: {
119
+ name: rootName,
40
120
  path: relRoot,
41
121
  type: projectType,
42
- framework: projectType,
43
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.
44
146
  summary: {
45
- totalFiles: flatFiles.length,
147
+ totalFiles,
46
148
  totalFolders,
47
- scanDurationMs: Math.round(scanDurationMs),
149
+ scanDurationMs: durationMs,
48
150
  },
49
- entryPoints,
50
- structure: (tree.children || []).map(mapTree)
51
151
  };
52
152
 
53
153
  console.log(JSON.stringify(output, null, 2));
@@ -28,37 +28,7 @@ import path from 'node:path';
28
28
  // Plain-text tree builder (Markdown-safe, no ANSI, no emoji)
29
29
  // ---------------------------------------------------------------------------
30
30
 
31
- /**
32
- * Build an in-memory nested tree from a flat list of relative file paths.
33
- * This avoids repeating the walk already done by the scanner while keeping
34
- * the markdown renderer completely self-contained.
35
- *
36
- * @param {string[]} flatFiles - Relative file paths produced by scan()
37
- * @returns {{ name: string, type: 'directory'|'file', children: object }[]}
38
- */
39
- function buildTree(flatFiles) {
40
- /** @type {{ type: string, children: Record<string, object> }} */
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
31
 
60
- return root;
61
- }
62
32
 
63
33
  /**
64
34
  * Recursively serialise a tree node into classic tree-connector lines.
@@ -81,11 +51,7 @@ function serializeNode(node, prefix, isLast, lines, depth = 0, maxDepth = 5) {
81
51
  lines.push(`${prefix}${connector}${label}`);
82
52
 
83
53
  if (node.type === 'directory') {
84
- const children = Object.values(node.children || {}).sort((a, b) => {
85
- // Directories before files, then alphabetically.
86
- if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
87
- return a.name.localeCompare(b.name);
88
- });
54
+ const children = node.children || [];
89
55
 
90
56
  // Truncate deep directories with an ellipsis rather than cutting silently.
91
57
  if (depth === maxDepth - 1 && children.length > 0) {
@@ -107,21 +73,18 @@ function serializeNode(node, prefix, isLast, lines, depth = 0, maxDepth = 5) {
107
73
  }
108
74
 
109
75
  /**
110
- * Convert a flat file list into a Markdown-safe tree string.
76
+ * Convert a ScanResult tree into a Markdown-safe tree string.
111
77
  *
112
- * @param {string[]} flatFiles
78
+ * @param {import('../scanner/scan.js').DirNode} tree
113
79
  * @param {string} rootName - Display name for the root node (e.g. "my-app/")
80
+ * @param {number} totalFiles
114
81
  * @returns {string}
115
82
  */
116
- function buildTreeString(flatFiles, rootName) {
117
- if (flatFiles.length === 0) return '';
83
+ function buildTreeString(tree, rootName, totalFiles) {
84
+ if (totalFiles === 0) return '';
118
85
 
119
- const root = buildTree(flatFiles);
120
86
  const lines = [`${rootName}/`];
121
- const children = Object.values(root.children).sort((a, b) => {
122
- if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
123
- return a.name.localeCompare(b.name);
124
- });
87
+ const children = tree.children || [];
125
88
 
126
89
  for (let i = 0; i < children.length; i++) {
127
90
  serializeNode(children[i], '', i === children.length - 1, lines);
@@ -165,6 +128,19 @@ function markdownTable(rows) {
165
128
  return [head, hr, ...body].join('\n');
166
129
  }
167
130
 
131
+ /**
132
+ * Derive a frameworks array from the projectType string.
133
+ * Returns [] when no specific framework is detected.
134
+ * Mirrors the identical derivation in console-renderer.js and json-renderer.js.
135
+ *
136
+ * @param {string} projectType
137
+ * @returns {string[]}
138
+ */
139
+ function deriveFrameworks(projectType) {
140
+ if (!projectType || projectType === 'Unknown') return [];
141
+ return [projectType];
142
+ }
143
+
168
144
  /**
169
145
  * Build a right-aligned Markdown table (used for statistics).
170
146
  *
@@ -190,11 +166,9 @@ function statsTable(rows) {
190
166
 
191
167
  /** @param {string[]} out */
192
168
  function sectionTitle(out) {
193
- out.push('# Project Analysis Report');
169
+ out.push('# Toren Report');
194
170
  out.push('');
195
171
  out.push('Generated by **Toren** — Codebase Onboarding Intelligence');
196
- out.push('');
197
- out.push('---');
198
172
  }
199
173
 
200
174
  /**
@@ -206,7 +180,7 @@ function sectionSummary(out, result, relRoot) {
206
180
  const { projectType, flatFiles, totalFolders } = result;
207
181
 
208
182
  out.push('');
209
- out.push('## Project Summary');
183
+ out.push('## Project');
210
184
  out.push('');
211
185
  out.push(markdownTable([
212
186
  ['Project Type', projectType],
@@ -214,52 +188,100 @@ function sectionSummary(out, result, relRoot) {
214
188
  ['Total Files', String(flatFiles.length)],
215
189
  ['Total Folders', String(totalFolders)],
216
190
  ]));
191
+ }
192
+
193
+ /**
194
+ * @param {string[]} out
195
+ * @param {string} projectType
196
+ */
197
+ function sectionFrameworks(out, projectType) {
198
+ const frameworks = deriveFrameworks(projectType);
199
+
217
200
  out.push('');
218
- out.push('---');
201
+ out.push('## Frameworks');
202
+ out.push('');
203
+
204
+ if (frameworks.length === 0) {
205
+ out.push('No frameworks detected.');
206
+ } else {
207
+ for (const fw of frameworks) {
208
+ out.push(`- ${fw}`);
209
+ }
210
+ }
219
211
  }
220
212
 
221
213
  /**
222
214
  * @param {string[]} out
223
- * @param {string[]} entryPoints
215
+ * @param {string[]} configs
224
216
  */
225
- function sectionEntryPoints(out, entryPoints) {
217
+ function sectionConfigurationFiles(out, configs) {
226
218
  out.push('');
227
- out.push('## Entry Points');
219
+ out.push('## Configurations');
228
220
  out.push('');
229
221
 
230
- if (entryPoints.length === 0) {
231
- out.push('No entry points detected.');
222
+ if (configs.length === 0) {
223
+ out.push('No configuration files detected.');
232
224
  } else {
233
- for (const ep of entryPoints) {
234
- out.push(`- \`${ep}\``);
225
+ for (const c of configs) {
226
+ out.push(`- ${c}`);
235
227
  }
236
228
  }
229
+ }
237
230
 
231
+ /**
232
+ * @param {string[]} out
233
+ * @param {Array<{name: string, command: string}>} scripts
234
+ */
235
+ function sectionPackageScripts(out, scripts) {
238
236
  out.push('');
239
- out.push('---');
237
+ out.push('## Scripts');
238
+ out.push('');
239
+
240
+ if (scripts.length === 0) {
241
+ out.push('No package scripts detected.');
242
+ } else {
243
+ for (const s of scripts) {
244
+ out.push(`- ${s.name}: ${s.command}`);
245
+ }
246
+ }
240
247
  }
241
248
 
242
249
  /**
243
250
  * @param {string[]} out
244
- * @param {string[]} flatFiles
245
- * @param {string} rootName
251
+ * @param {string[]} entryPoints
246
252
  */
247
- function sectionFolderStructure(out, flatFiles, rootName) {
253
+ function sectionEntryPoints(out, entryPoints) {
248
254
  out.push('');
249
- out.push('## Folder Structure');
255
+ out.push('## Entry Points');
250
256
  out.push('');
251
257
 
252
- if (flatFiles.length === 0) {
253
- out.push('No files scanned.');
258
+ if (entryPoints.length === 0) {
259
+ out.push('No entry points detected.');
254
260
  } else {
255
- const tree = buildTreeString(flatFiles, rootName);
256
- out.push('```text');
257
- out.push(tree);
258
- out.push('```');
261
+ for (const ep of entryPoints) {
262
+ out.push(`- ${ep}`);
263
+ }
259
264
  }
265
+ }
260
266
 
267
+ /**
268
+ * @param {string[]} out
269
+ * @param {import('../scanner/scan.js').DirNode} tree
270
+ * @param {number} totalFiles
271
+ * @param {string} rootName
272
+ */
273
+ function sectionFolderStructure(out, tree, totalFiles, rootName) {
274
+ // Contract: omit section entirely when no files were scanned.
275
+ if (totalFiles === 0) return;
276
+
277
+ out.push('');
278
+ out.push('## Structure');
261
279
  out.push('');
262
- out.push('---');
280
+
281
+ const treeStr = buildTreeString(tree, rootName, totalFiles);
282
+ out.push('```text');
283
+ out.push(treeStr);
284
+ out.push('```');
263
285
  }
264
286
 
265
287
  /**
@@ -277,21 +299,6 @@ function sectionStatistics(out, result) {
277
299
  ['Folders', totalFolders],
278
300
  ['Scan Duration', formatDuration(scanDurationMs)],
279
301
  ]));
280
- out.push('');
281
- out.push('---');
282
- }
283
-
284
- /**
285
- * @param {string[]} out
286
- */
287
- function sectionScanInfo(out) {
288
- out.push('');
289
- out.push('## Scan Information');
290
- out.push('');
291
- out.push('Generated by **Toren**');
292
- out.push('');
293
- out.push('Output Format: Markdown');
294
- out.push('');
295
302
  }
296
303
 
297
304
  // ---------------------------------------------------------------------------
@@ -308,21 +315,23 @@ function sectionScanInfo(out) {
308
315
  * @param {{ cwd?: string }} [options]
309
316
  */
310
317
  export function render(result, options = {}) {
311
- const { rootPath, entryPoints, flatFiles } = result;
318
+ const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles, tree } = result;
312
319
 
313
320
  const cwd = options.cwd ?? process.cwd();
314
321
  const relRoot = path.relative(cwd, rootPath) || '.';
315
- const rootName = path.basename(rootPath) || relRoot;
322
+ const rootName = relRoot === '.' ? path.basename(rootPath) : relRoot;
316
323
 
317
324
  /** @type {string[]} */
318
325
  const out = [];
319
326
 
320
327
  sectionTitle(out);
321
328
  sectionSummary(out, result, relRoot);
329
+ sectionFrameworks(out, projectType);
322
330
  sectionEntryPoints(out, entryPoints);
323
- sectionFolderStructure(out, flatFiles, rootName);
331
+ sectionConfigurationFiles(out, configs);
332
+ sectionPackageScripts(out, scripts);
324
333
  sectionStatistics(out, result);
325
- sectionScanInfo(out);
334
+ sectionFolderStructure(out, tree, flatFiles.length, rootName);
326
335
 
327
336
  console.log(out.join('\n'));
328
337
  }
@@ -20,6 +20,8 @@
20
20
 
21
21
  import fs from 'node:fs';
22
22
  import path from 'node:path';
23
+ import { detectConfigs } from '../detectors/config-detector.js';
24
+ import { detectScripts } from '../detectors/script-detector.js';
23
25
 
24
26
  // ---------------------------------------------------------------------------
25
27
  // Constants
@@ -96,6 +98,8 @@ const PROJECT_TYPE_MARKERS = [
96
98
  * @property {string} rootPath - Absolute path that was scanned
97
99
  * @property {string} projectType - Detected project type label
98
100
  * @property {Array<string>} entryPoints - Relative paths of detected entry points
101
+ * @property {Array<string>} configs - Relative paths of detected config files
102
+ * @property {Array<{name: string, command: string}>} scripts - Parsed package scripts
99
103
  * @property {DirNode} tree - Full in-memory file tree
100
104
  * @property {Array<string>} flatFiles - All relative file paths (flat list)
101
105
  * @property {number} totalFolders - Total number of directories walked
@@ -182,7 +186,11 @@ function walkDirectory(dirPath, rootPath, flatFiles, includeHidden, maxFiles) {
182
186
  node.children.push(childNode);
183
187
  } else if (dirent.isFile()) {
184
188
  if (flatFiles.length >= maxFiles) {
185
- throw new Error(`Max file scan limit exceeded (${maxFiles} files). Use --max-files <number> to increase the limit.`);
189
+ const err = new Error(`Max file scan limit exceeded (${maxFiles} files).`);
190
+ err.title = 'Scan limit exceeded';
191
+ err.detailLabel = 'Hint';
192
+ err.detailValue = 'Use --max-files <number> to increase the limit.';
193
+ throw err;
186
194
  }
187
195
  const relFilePath = toPosix(path.relative(rootPath, childPath));
188
196
 
@@ -445,14 +453,28 @@ export function scan(targetPath, options = {}) {
445
453
  let stat;
446
454
  try {
447
455
  stat = fs.statSync(rootPath);
448
- } catch {
449
- throw new Error(`Path does not exist: ${rootPath}`);
456
+ } catch (err) {
457
+ const error = new Error(`Path does not exist: ${rootPath}`);
458
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
459
+ error.title = 'Permission denied';
460
+ error.message = 'You do not have permission to access the specified path.';
461
+ } else {
462
+ error.title = 'Invalid directory';
463
+ error.message = 'The specified path does not exist.';
464
+ }
465
+ error.detailLabel = 'Path';
466
+ error.detailValue = rootPath;
467
+ throw error;
450
468
  }
451
469
 
452
470
  /** @type {Array<string>} */
453
471
  const flatFiles = [];
454
472
  /** @type {Array<string>} */
455
473
  let entryPoints = [];
474
+ /** @type {Array<string>} */
475
+ let configs = [];
476
+ /** @type {Array<{name: string, command: string}>} */
477
+ let scripts = [];
456
478
 
457
479
  const startTime = performance.now();
458
480
  let tree;
@@ -465,6 +487,8 @@ export function scan(targetPath, options = {}) {
465
487
  // Count all directory nodes in the tree (excluding root itself).
466
488
  totalFolders = countFolders(tree) - 1;
467
489
  entryPoints = findEntryPoints(projectType, flatFiles, rootPath);
490
+ configs = detectConfigs(flatFiles).configs;
491
+ scripts = detectScripts(rootPath).scripts;
468
492
  } else if (stat.isFile()) {
469
493
  const relFilePath = toPosix(path.basename(rootPath));
470
494
  tree = {
@@ -481,8 +505,15 @@ export function scan(targetPath, options = {}) {
481
505
  };
482
506
  flatFiles.push(relFilePath);
483
507
  entryPoints = [relFilePath]; // A single file is its own entry point
508
+ configs = detectConfigs(flatFiles).configs;
509
+ scripts = detectScripts(path.dirname(rootPath)).scripts;
484
510
  } else {
485
- throw new Error(`Path is neither a file nor a directory: ${rootPath}`);
511
+ const err = new Error(`Path is neither a file nor a directory: ${rootPath}`);
512
+ err.title = 'Unsupported path type';
513
+ err.message = 'The specified path is neither a file nor a directory.';
514
+ err.detailLabel = 'Path';
515
+ err.detailValue = rootPath;
516
+ throw err;
486
517
  }
487
518
 
488
519
  const scanDurationMs = performance.now() - startTime;
@@ -491,6 +522,8 @@ export function scan(targetPath, options = {}) {
491
522
  rootPath,
492
523
  projectType,
493
524
  entryPoints,
525
+ configs,
526
+ scripts,
494
527
  tree,
495
528
  flatFiles,
496
529
  totalFolders,