@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.
@@ -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
  /**
@@ -203,19 +177,48 @@ function sectionTitle(out) {
203
177
  * @param {string} relRoot
204
178
  */
205
179
  function sectionSummary(out, result, relRoot) {
206
- const { projectType, flatFiles, totalFolders } = result;
180
+ const { projectType, flatFiles, totalFolders, packageManager, projectInfo } = result;
207
181
 
208
182
  out.push('');
209
- out.push('## Project Summary');
183
+ out.push('## Project');
210
184
  out.push('');
211
- out.push(markdownTable([
212
- ['Project Type', projectType],
213
- ['Scan Path', relRoot],
214
- ['Total Files', String(flatFiles.length)],
215
- ['Total Folders', String(totalFolders)],
216
- ]));
185
+
186
+ const rows = [];
187
+ if (projectInfo && projectInfo.name) rows.push(['Name', projectInfo.name]);
188
+ rows.push(['Project Type', projectType]);
189
+ if (packageManager) rows.push(['Package Manager', packageManager]);
190
+ if (projectInfo && projectInfo.runtime) rows.push(['Runtime', projectInfo.runtime]);
191
+ if (projectInfo && projectInfo.language) rows.push(['Language', projectInfo.language]);
192
+ if (projectInfo && projectInfo.architecture) rows.push(['Architecture', projectInfo.architecture]);
193
+ if (projectInfo && projectInfo.framework) rows.push(['Framework', projectInfo.framework]);
194
+ if (projectInfo && projectInfo.entryPoint) rows.push(['Entry Point', projectInfo.entryPoint]);
195
+ if (projectInfo && projectInfo.sourceDirectory) rows.push(['Source Directory', projectInfo.sourceDirectory]);
196
+
197
+ rows.push(['Scan Path', relRoot]);
198
+ rows.push(['Total Files', String(flatFiles.length)]);
199
+ rows.push(['Total Folders', String(totalFolders)]);
200
+
201
+ out.push(markdownTable(rows));
202
+ }
203
+
204
+ /**
205
+ * @param {string[]} out
206
+ * @param {string} projectType
207
+ */
208
+ function sectionFrameworks(out, projectType) {
209
+ const frameworks = deriveFrameworks(projectType);
210
+
211
+ out.push('');
212
+ out.push('## Frameworks');
217
213
  out.push('');
218
- out.push('---');
214
+
215
+ if (frameworks.length === 0) {
216
+ out.push('No frameworks detected.');
217
+ } else {
218
+ for (const fw of frameworks) {
219
+ out.push(`- ${fw}`);
220
+ }
221
+ }
219
222
  }
220
223
 
221
224
  /**
@@ -224,7 +227,7 @@ function sectionSummary(out, result, relRoot) {
224
227
  */
225
228
  function sectionConfigurationFiles(out, configs) {
226
229
  out.push('');
227
- out.push('## Configuration Files');
230
+ out.push('## Configurations');
228
231
  out.push('');
229
232
 
230
233
  if (configs.length === 0) {
@@ -234,32 +237,71 @@ function sectionConfigurationFiles(out, configs) {
234
237
  out.push(`- ${c}`);
235
238
  }
236
239
  }
237
-
238
- out.push('');
239
- out.push('---');
240
240
  }
241
241
 
242
242
  /**
243
243
  * @param {string[]} out
244
- * @param {Array<{name: string, command: string}>} scripts
244
+ * @param {Array<{name: string, command: string, description: string, usage: string}>} scripts
245
245
  */
246
246
  function sectionPackageScripts(out, scripts) {
247
247
  out.push('');
248
- out.push('## Package Scripts');
248
+ out.push('## Scripts');
249
249
  out.push('');
250
250
 
251
251
  if (scripts.length === 0) {
252
252
  out.push('No package scripts detected.');
253
253
  } else {
254
- out.push('| Script | Command |');
255
- out.push('|--------|---------|');
256
254
  for (const s of scripts) {
257
- out.push(`| \`${s.name}\` | \`${s.command}\` |`);
255
+ const usage = s.usage || `npm run ${s.name}`;
256
+ out.push(`- **${usage}**`);
257
+ if (s.description) {
258
+ out.push(` ${s.description} `);
259
+ out.push(` \`${s.command}\``);
260
+ } else {
261
+ out.push(` \`${s.command}\``);
262
+ }
258
263
  }
259
264
  }
265
+ }
260
266
 
267
+ /**
268
+ * @param {string[]} out
269
+ * @param {Array<{path: string, reason: string}>} importantFiles
270
+ */
271
+ function sectionImportantFiles(out, importantFiles) {
272
+ out.push('');
273
+ out.push('## Important Files');
261
274
  out.push('');
262
- out.push('---');
275
+
276
+ if (importantFiles.length === 0) {
277
+ out.push('No important files detected.');
278
+ } else {
279
+ for (const f of importantFiles) {
280
+ out.push(`- **${f.path}** `);
281
+ out.push(` ${f.reason}`);
282
+ }
283
+ }
284
+ }
285
+
286
+ /**
287
+ * @param {string[]} out
288
+ * @param {Array<{id: string, status: string, message: string}>} health
289
+ */
290
+ function sectionProjectHealth(out, health) {
291
+ out.push('');
292
+ out.push('## Project Health');
293
+ out.push('');
294
+
295
+ if (health.length === 0) {
296
+ out.push('No project health observations available.');
297
+ } else {
298
+ for (const h of health) {
299
+ let icon = 'ℹ';
300
+ if (h.status === 'pass') icon = '✓';
301
+ else if (h.status === 'warning') icon = '⚠';
302
+ out.push(`- ${icon} ${h.message}`);
303
+ }
304
+ }
263
305
  }
264
306
 
265
307
  /**
@@ -275,35 +317,29 @@ function sectionEntryPoints(out, entryPoints) {
275
317
  out.push('No entry points detected.');
276
318
  } else {
277
319
  for (const ep of entryPoints) {
278
- out.push(`- \`${ep}\``);
320
+ out.push(`- ${ep}`);
279
321
  }
280
322
  }
281
-
282
- out.push('');
283
- out.push('---');
284
323
  }
285
324
 
286
325
  /**
287
326
  * @param {string[]} out
288
- * @param {string[]} flatFiles
289
- * @param {string} rootName
327
+ * @param {import('../scanner/scan.js').DirNode} tree
328
+ * @param {number} totalFiles
329
+ * @param {string} rootName
290
330
  */
291
- function sectionFolderStructure(out, flatFiles, rootName) {
331
+ function sectionFolderStructure(out, tree, totalFiles, rootName) {
332
+ // Contract: omit section entirely when no files were scanned.
333
+ if (totalFiles === 0) return;
334
+
292
335
  out.push('');
293
- out.push('## Folder Structure');
336
+ out.push('## Structure');
294
337
  out.push('');
295
338
 
296
- if (flatFiles.length === 0) {
297
- out.push('No files scanned.');
298
- } else {
299
- const tree = buildTreeString(flatFiles, rootName);
300
- out.push('```text');
301
- out.push(tree);
302
- out.push('```');
303
- }
304
-
305
- out.push('');
306
- out.push('---');
339
+ const treeStr = buildTreeString(tree, rootName, totalFiles);
340
+ out.push('```text');
341
+ out.push(treeStr);
342
+ out.push('```');
307
343
  }
308
344
 
309
345
  /**
@@ -321,21 +357,6 @@ function sectionStatistics(out, result) {
321
357
  ['Folders', totalFolders],
322
358
  ['Scan Duration', formatDuration(scanDurationMs)],
323
359
  ]));
324
- out.push('');
325
- out.push('---');
326
- }
327
-
328
- /**
329
- * @param {string[]} out
330
- */
331
- function sectionScanInfo(out) {
332
- out.push('');
333
- out.push('## Scan Information');
334
- out.push('');
335
- out.push('Generated by **Toren**');
336
- out.push('');
337
- out.push('Output Format: Markdown');
338
- out.push('');
339
360
  }
340
361
 
341
362
  // ---------------------------------------------------------------------------
@@ -352,23 +373,25 @@ function sectionScanInfo(out) {
352
373
  * @param {{ cwd?: string }} [options]
353
374
  */
354
375
  export function render(result, options = {}) {
355
- const { rootPath, entryPoints, configs = [], scripts = [], flatFiles } = result;
376
+ const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles, tree, importantFiles = [], health = [] } = result;
356
377
 
357
378
  const cwd = options.cwd ?? process.cwd();
358
379
  const relRoot = path.relative(cwd, rootPath) || '.';
359
- const rootName = path.basename(rootPath) || relRoot;
380
+ const rootName = relRoot === '.' ? path.basename(rootPath) : relRoot;
360
381
 
361
382
  /** @type {string[]} */
362
383
  const out = [];
363
384
 
364
385
  sectionTitle(out);
365
386
  sectionSummary(out, result, relRoot);
387
+ sectionProjectHealth(out, health);
388
+ sectionImportantFiles(out, importantFiles);
389
+ sectionFrameworks(out, projectType);
366
390
  sectionEntryPoints(out, entryPoints);
367
391
  sectionConfigurationFiles(out, configs);
368
392
  sectionPackageScripts(out, scripts);
369
- sectionFolderStructure(out, flatFiles, rootName);
370
393
  sectionStatistics(out, result);
371
- sectionScanInfo(out);
394
+ sectionFolderStructure(out, tree, flatFiles.length, rootName);
372
395
 
373
396
  console.log(out.join('\n'));
374
397
  }
@@ -20,8 +20,12 @@
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
+ import { detectConfigs } from '../detectors/config-detector.js';
24
+ import { detectScripts } from '../detectors/script-detector.js';
25
+ import { detectPackageManager } from '../detectors/package-manager-detector.js';
26
+ import { detectImportantFiles } from '../detectors/important-files-detector.js';
27
+ import { detectProjectInfo } from '../detectors/project-info-detector.js';
28
+ import { detectHealth } from '../detectors/health-detector.js';
25
29
 
26
30
  // ---------------------------------------------------------------------------
27
31
  // Constants
@@ -99,7 +103,8 @@ const PROJECT_TYPE_MARKERS = [
99
103
  * @property {string} projectType - Detected project type label
100
104
  * @property {Array<string>} entryPoints - Relative paths of detected entry points
101
105
  * @property {Array<string>} configs - Relative paths of detected config files
102
- * @property {Array<{name: string, command: string}>} scripts - Parsed package scripts
106
+ * @property {Array<{name: string, command: string, category: string|null, description: string|null, usage: string}>} scripts - Parsed and enriched package scripts
107
+ * @property {string|null} packageManager - Detected package manager, or null if none/ambiguous
103
108
  * @property {DirNode} tree - Full in-memory file tree
104
109
  * @property {Array<string>} flatFiles - All relative file paths (flat list)
105
110
  * @property {number} totalFolders - Total number of directories walked
@@ -186,7 +191,11 @@ function walkDirectory(dirPath, rootPath, flatFiles, includeHidden, maxFiles) {
186
191
  node.children.push(childNode);
187
192
  } else if (dirent.isFile()) {
188
193
  if (flatFiles.length >= maxFiles) {
189
- throw new Error(`Max file scan limit exceeded (${maxFiles} files). Use --max-files <number> to increase the limit.`);
194
+ const err = new Error(`Max file scan limit exceeded (${maxFiles} files).`);
195
+ err.title = 'Scan limit exceeded';
196
+ err.detailLabel = 'Hint';
197
+ err.detailValue = 'Use --max-files <number> to increase the limit.';
198
+ throw err;
190
199
  }
191
200
  const relFilePath = toPosix(path.relative(rootPath, childPath));
192
201
 
@@ -449,8 +458,18 @@ export function scan(targetPath, options = {}) {
449
458
  let stat;
450
459
  try {
451
460
  stat = fs.statSync(rootPath);
452
- } catch {
453
- throw new Error(`Path does not exist: ${rootPath}`);
461
+ } catch (err) {
462
+ const error = new Error(`Path does not exist: ${rootPath}`);
463
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
464
+ error.title = 'Permission denied';
465
+ error.message = 'You do not have permission to access the specified path.';
466
+ } else {
467
+ error.title = 'Invalid directory';
468
+ error.message = 'The specified path does not exist.';
469
+ }
470
+ error.detailLabel = 'Path';
471
+ error.detailValue = rootPath;
472
+ throw error;
454
473
  }
455
474
 
456
475
  /** @type {Array<string>} */
@@ -461,12 +480,17 @@ export function scan(targetPath, options = {}) {
461
480
  let configs = [];
462
481
  /** @type {Array<{name: string, command: string}>} */
463
482
  let scripts = [];
483
+ /** @type {string|null} */
484
+ let packageManager = null;
485
+ let importantFiles = [];
486
+ let projectInfo = null;
487
+ let health = [];
464
488
 
465
489
  const startTime = performance.now();
466
490
  let tree;
467
491
  let projectType = 'Unknown';
468
492
  let totalFolders = 0;
469
-
493
+
470
494
  if (stat.isDirectory()) {
471
495
  tree = walkDirectory(rootPath, rootPath, flatFiles, includeHidden, maxFiles);
472
496
  projectType = detectProjectType(rootPath);
@@ -474,7 +498,11 @@ export function scan(targetPath, options = {}) {
474
498
  totalFolders = countFolders(tree) - 1;
475
499
  entryPoints = findEntryPoints(projectType, flatFiles, rootPath);
476
500
  configs = detectConfigs(flatFiles).configs;
477
- scripts = detectScripts(rootPath).scripts;
501
+ packageManager = detectPackageManager(flatFiles).packageManager;
502
+ scripts = detectScripts(rootPath, packageManager).scripts;
503
+ importantFiles = detectImportantFiles({ flatFiles, projectType, entryPoints, configs }).importantFiles;
504
+ projectInfo = detectProjectInfo({ rootPath, projectType, flatFiles, entryPoints, packageManager, scripts }).projectInfo;
505
+ health = detectHealth({ flatFiles, configs, importantFiles, scripts, projectType }).health;
478
506
  } else if (stat.isFile()) {
479
507
  const relFilePath = toPosix(path.basename(rootPath));
480
508
  tree = {
@@ -492,9 +520,18 @@ export function scan(targetPath, options = {}) {
492
520
  flatFiles.push(relFilePath);
493
521
  entryPoints = [relFilePath]; // A single file is its own entry point
494
522
  configs = detectConfigs(flatFiles).configs;
495
- scripts = detectScripts(path.dirname(rootPath)).scripts;
523
+ packageManager = detectPackageManager(flatFiles).packageManager;
524
+ scripts = detectScripts(path.dirname(rootPath), packageManager).scripts;
525
+ importantFiles = detectImportantFiles({ flatFiles, projectType, entryPoints, configs }).importantFiles;
526
+ projectInfo = detectProjectInfo({ rootPath, projectType, flatFiles, entryPoints, packageManager, scripts }).projectInfo;
527
+ health = detectHealth({ flatFiles, configs, importantFiles, scripts, projectType }).health;
496
528
  } else {
497
- throw new Error(`Path is neither a file nor a directory: ${rootPath}`);
529
+ const err = new Error(`Path is neither a file nor a directory: ${rootPath}`);
530
+ err.title = 'Unsupported path type';
531
+ err.message = 'The specified path is neither a file nor a directory.';
532
+ err.detailLabel = 'Path';
533
+ err.detailValue = rootPath;
534
+ throw err;
498
535
  }
499
536
 
500
537
  const scanDurationMs = performance.now() - startTime;
@@ -505,6 +542,10 @@ export function scan(targetPath, options = {}) {
505
542
  entryPoints,
506
543
  configs,
507
544
  scripts,
545
+ packageManager,
546
+ importantFiles,
547
+ projectInfo,
548
+ health,
508
549
  tree,
509
550
  flatFiles,
510
551
  totalFolders,